记一次 MySQL 的查询优化

背景

当前业务中存在一个接口耗时比较高的功能模块(这里简称:【查询面板】)。其中最高耗时可达 50s +,造成该功能几乎无法使用,严重影响用户使用的体验。这里将记录如何将查询时延从 50s + 降低到 10s 内的过程

场景

目前所有表都使用 TDSQL (分布式 MySQLhttps://cloud.tencent.com/document/product/557/8765)来存储,由多个分片组成。

查询时主要用到了 2 张表:

image.png

广播表:在每个分片都存在完整的数据集

分区表:即:TDSQL-MySQL 中的一级分区,通过对分区键进行 hash 之后,dml 操作将发往分区键对应的分片中执行。如下图所示:

image.png

需要注意的是:在查询分区表时,需要指定明确的分区键(等值查询),否则会将所有请求发往所有的分片执行完之后,在 proxy 按原始 sql 语义进行对应处理再返回给客户端

对应表结构如下(已省略部分字段)

CREATE TABLE `ad_conf`
(
    `id`                   bigint          NOT NULL AUTO_INCREMENT,
    `med_account_id`       bigint unsigned NOT NULL COMMENT '媒体账户id',
    `creative_id`          bigint unsigned NOT NULL DEFAULT '0' COMMENT '创意id',
    `title`                varchar(1024)   NOT NULL DEFAULT '' COMMENT '标题',
    `status`               varchar(50)     NOT NULL DEFAULT '' COMMENT '创意状态',
    `opt_status`           varchar(50)     NOT NULL DEFAULT '' COMMENT '创意操作状态',
    `isdelete`             tinyint(1)      NOT NULL DEFAULT '2' COMMENT '是否已删除。1:是,2否',
    `creative_create_time` datetime                 DEFAULT NULL COMMENT '媒体侧创意创建时间, 格式: yyyy-mm-dd',
    `creative_modify_time` datetime                 DEFAULT NULL COMMENT '媒体侧创意修改时间, 格式:yyyy-mm-dd',
    PRIMARY KEY (`id`),
    KEY `creative_id_index` (`creative_id`),
    constraint account_creative unique (med_account_id, creative_id)
) ENGINE = InnoDB
  DEFAULT CHARSET = utf8mb4
  COLLATE = utf8mb4_0900_ai_ci shardkey=noshardkey_allset;
  
  CREATE TABLE `ad_panel`
(
    `id`               int         NOT NULL AUTO_INCREMENT,
    `statis_date`      int         NOT NULL COMMENT '统计日期,格式:yyyyMMdd',
    `med_account_id`   bigint      NOT NULL COMMENT '媒体账号 ID',
    `med_account_name` varchar(60) NOT NULL COMMENT '媒体账号名称',
    `game_id`          int         NOT NULL DEFAULT '0' COMMENT 'O2游戏ID',
    `game_name`        varchar(64) NOT NULL DEFAULT '' COMMENT '游戏名称',
    `agent_id`         int         NOT NULL DEFAULT '-1' COMMENT '代投方ID, 0 - 未知',
    `agent_name`       varchar(32) NOT NULL DEFAULT '' COMMENT '代投方名称',
    `med_id`           int         NOT NULL DEFAULT '-1' COMMENT '媒体ID',
    `med_name`         varchar(64) NOT NULL DEFAULT '' COMMENT '媒体名称',
    `plat_id`          bigint      NOT NULL DEFAULT '0' COMMENT '平台ID ( 1:Android,2:IOS)',
    `plat_name`        varchar(128)         DEFAULT '' COMMENT '平台名称',
    `ad_id`            bigint      NOT NULL COMMENT '广告计划id',
    `cost`             bigint               DEFAULT '0' COMMENT '消费,单位: 分',
    `shows`            int                  DEFAULT '0' COMMENT '展现',
  -- 省略 300+ 字段
    `click`            int                  DEFAULT '0' COMMENT '点击',
    `cpm`              bigint               DEFAULT '0' COMMENT '千次展现消费(头条接口:avg_show_cost),单位: 分',
    `cpc`              bigint               DEFAULT '0' COMMENT '平均点击价格(头条接口:avg_click_cost),单位: 分',
    PRIMARY KEY (`id`),
    UNIQUE KEY `date_creative_unique_index` (`statis_date`, `med_account_id`, `ad_id`),
    KEY `index_creative_id` (`ad_id`)
) ENGINE = InnoDB
  DEFAULT CHARSET = utf8mb4
  COLLATE = utf8mb4_0900_ai_ci shardkey=statis_date;

查询过程分析

这里需要过滤 ad_conf 表筛选出对应的【广告配置】,以及获取到该【广告配置】在 ad_panel 表保存的【效果数据】,另外还可以根据所有效果数据的字段进行排序,比如:按消耗、曝光、ROI、CPA、LTV 等。

注意:

ad_panel 表只保存了 ad_conf 配置表有效果数据的信息,【然而如果没有效果数据的配置,则只会存在 ad_conf 表】

阶段一:直接 join

直接 join , 由于 ad_panel 是分区表,使用关联的字段 ad_id 无法使用索引,导致 ad_conf 的结果集在 ad_panel 中进行全表扫描

select l.*, sum(r.cost) as sum_cost, r.*
from ad_conf l
         left join ad_panel r on l.ad_id = r.ad_id and r.game_id = ?
    and r.statis_date >= ? and r.statis_date <= ?
where l.status = ?
  and l.med_account_id in ?
  and l.status = ?
  and l.isdelete = ?
group by l.ad_id
order by sum_cost desc, l.ad_id desc;

阶段二:聚合函数下推

如果 ad_panel 与 ad_conf 直接 join 造成全表扫描在数据量小的情况没什么问题,但如果 ad_panel 数据量达到 50w 之后也会非常慢,这里做了一个优化点,将聚合下推到子查询中,减少 left join 时扫描 ad_panel 的数据行。

select l.*, r.*
from ad_conf l
         left join (select sum(cost) as sum_cost, ad_id as ad_id_ext, *
                    from ad_panel
                    where statis_date >= ?
                      and statis_date <= ?
                      and game_id = ?
                    group by ad_id) r
                   on l.ad_id = r.ad_id_ext and r.statis_date >= ? and r.statis_date <= ?
where l.status = ?
  and l.med_account_id in ?
  and l.status = ?
  and l.isdelete = ?
order by sum_cost desc, l.ad_id desc;

可以通过对比两条 sql 行数的差距来看能否优化目的

select count(1)
from (
         select *
         from ad_panel
         where statis_date >= ?
           and statis_date <= ?
           and game_id = ?
         group by ad_id
     ) a ;
     
select count(1)
from (
         select *
         from ad_panel
         where statis_date >= ?
           and statis_date <= ?
           and game_id = ?
     ) a ;

阶段三:减少 join 时产生临时文件

通过阶段二在通过提前 group by 之后减少了 join 时所需数据量, 但当 group by 减少的数据量与【阶段一】相同时,速度上也会受到影响。

此时可以根据 SQL 执行的过程来分析

1. join

join 是在 join_buffer 内存做的,如果匹配的数据量比较小或者 join_buffer 设置的比较大,速度也不会太慢。但是,如果 join 的数据量比较大时,mysql 会采用在硬盘上创建临时表的方式进行多张表的关联匹配,这种显然效率就极低,本来磁盘的 IO 就不快,还要关联。

优化点:

降低 join 时的数据量

2. order by

order by 排序,分为全字段排序和 rowid 排序。它是拿 max_length_for_sort_data (4KB)和结果行数据长度对比,如果结果行数据长度超过 max_length_for_sort_data 这个值,就会走 rowid 排序,相反,则走全字段排序。

  • rowid 排序:仅在 sort buffer 中保存排序的字段和主键id,当完成排序后再通过主键 ID 回表查询其他列
  • 全字段排序:在 sort buffer 中保存所有需要查询的列,排序完成后直接返回,不需要二次回表

排序时借助 sort_buffer 来完成

  • 如果要排序的数据小于 sort_buffer_size,排序在 sort_buffer 内存中完成
  • 如果要排序的数据大于 sort_buffer_size,则借助磁盘文件来进行排序

优化点:

  1. 超过 max_length_for_sort_data(4KB)大小就会导致走 rowid 排序,需要二次回表查其他字段,
  2. 超过 sort_buffer_size (2M)则需要通过文件排序

目前通过查询 ad_panel 表信息,一行大小 2KB, 因此会走全字段排序,但是由于查出来的数据量已经超过 sort_buffer_size 因此会走

假设每次通过 group by 查询 5W 行数据,每行 2KB,那么一次需要对 50000*2/1024=97M 数据排序

优化方向:

  1. 建立二级索引,依赖二级索引有序的特性。因为分区表,且分区键 range 查询导致无法使用索引所以放弃该种方式

  2. 根据 rowid 排序思想,将排序列与数据列分两次查询。具体为:

  3. 第一次查询 id (8 byte)+ 排序列(4 byte ) 大小 12 byte ,此时有 50000*12/1024/1024=0.57M 数据排序

  4. 第二次根据查询的 id 回表查(业务上实现)

image.png
image.png

最后优化点:

1. 排序字段在左表

select l.ad_id, l.*
from task_log.ad_conf l
where l.status = ?
  and l.med_account_id in ?
  and l.status = ?
  and l.isdelete = ?
order by l.creative_create_time desc, l.ad_id desc;

2. 排序字段在右表

select l.ad_id, l.*
from task_log.ad_conf l
         left join (select sum(cost) as sum_cost, ad_id as ad_id_ext, *
                    from task_log.ad_panel
                    where statis_date >= ?
                      and statis_date <= ?
                      and game_id = ?
                    group by ad_id) r
                   on l.ad_id = r.ad_id_ext and r.statis_date >= ? and r.statis_date <= ?
where l.status = ?
  and l.med_account_id in ?
  and l.status = ?
  and l.isdelete = ?
order by sum_cost desc, l.ad_id desc;
  1. 通过 1\2 中的 ad_id 回表查询(此时转为单表查询),再通过 ad_id 在业务层去拼接
select ad_id as ad_id_ext, *
from task_log.ad_panel
where statis_date >= ?
  and statis_date <= ?
  and game_id = ?
  and ad_id in (?)

未来方向

目前主要的问题:

  1. 分区键存在 range 查询导致索引失效,所以造成扫全表
  2. join 和 排序让广播表和分片表进行关联,不是同一类型表存在分片的 ID 不一致

所以一方面需要重新根据业务主体定义分区键,如果按 game_id 分区,另一方面将配置表需要过滤列冗余到宽表,避免 join

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,732评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,496评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,264评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,807评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,806评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,675评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,029评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,683评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,704评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,666评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,773评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,413评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,016评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,204评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,083评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,503评论 2 343

推荐阅读更多精彩内容