MySQL线上SQL优化实战案例(一)

Author:Jason豪

一、 SQL业务背景与线上表数据环境

两条问题SQL均涉及“日级数据上传表”,cm_settle_partner_day_data,该表目前的生产库数据量是1550926,每日新增数据量在4000左右,目前除主键外已有四个单列索引。

建表语句如下:

CREATE TABLE `cm_settle_partner_day_data` (
  `modified` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  `created` timestamp NOT NULL DEFAULT '1971-01-01 00:00:00' COMMENT '创建时间',
  `modifier` varchar(64) NOT NULL DEFAULT '' COMMENT '更新人',
  `creator` varchar(64) NOT NULL DEFAULT '' COMMENT '创建人',
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '自增id',
  `settle_partner_day_id` bigint(20) NOT NULL COMMENT '业务唯一id',
  `channel_id` bigint(20) NOT NULL COMMENT '渠道id',
  `agent_id` bigint(20) NOT NULL COMMENT '企业id',
  `dimension_value` tinyint(4) NOT NULL COMMENT '维度值:1-Fr维度,2-账户维度,3-非标维度',
  `status` tinyint(4) NOT NULL COMMENT '数据状态,1-有效,2-覆盖',
  `business_date` date NOT NULL DEFAULT '1971-01-01' COMMENT '日期',
  `account` varchar(32) NOT NULL DEFAULT '' COMMENT '前台账号',
  `fr_code` varchar(32) NOT NULL DEFAULT '' COMMENT 'fr',
  `show_value` bigint(20) NOT NULL DEFAULT '0' COMMENT '展示数据',
  `click_value` bigint(20) NOT NULL DEFAULT '0' COMMENT '点击数据',
  `consume_value` decimal(20,2) NOT NULL DEFAULT '0.00' COMMENT '消耗数据(元)',
  `cash_value` decimal(20,2) NOT NULL DEFAULT '0.00' COMMENT '现金(元)',
  `file_id` bigint(20) NOT NULL DEFAULT '0' COMMENT '上传的文件ID',
  PRIMARY KEY (`id`),
  KEY `idx_channel_id` (`channel_id`),
  KEY `idx_settle_partner_day_id` (`settle_partner_day_id`),
  KEY `idx_business_date` (`business_date`),
  KEY `idx_fr_code` (`fr_code`)
) ENGINE=InnoDB AUTO_INCREMENT=1041177 DEFAULT CHARSET=utf8 COMMENT='日级数据上传表'

二、两条sql的具体优化思路与方案

1.第一条SQL是一个单表查询,未优化前执行计划走了全表扫描,该SQL在生产库执行时间是0.5秒,线上3秒。最先考虑可以建立一个三字段的联合索引,该三字段均在该SQL中的where条件中,从索引中取出目标数据行主键ID后,在回表取出查询数据。
具体SQL如下:

SELECT
	sum( cash_value )* 100 AS expectSettleAmount,
	agent_id AS agentId 
	from cm_settle_partner_day_data 
WHERE
	STATUS = 1 
	AND dimension_value IN ( 1, 3 ) 
	AND date_format( business_date, '%Y-%m' ) = '2020-07' 
GROUP BY
	agent_id;

更多考量后,该表是“日级数据上传表”,每天除了查询外,每日还有4000左右数据量的INSERT操作。如果不考虑INSERT操作,建立该查询SQL中涉及的五个字段的联合索引形成覆盖索引会是最优解,这样就可以直接从二级索引(新建的五个字段的覆盖索引)中查找出所有数据,省去回表操作,大大提升SQL执行效率,但是代价是五个字段的联合索引会很大(比三个字段的联合索引要大),除了日常MySQL数据库的维护,还会极大拖累INSERT与Update操作(因为每INSERT一条新数据到表时同时也会修改索引B+TREE数据结构),需要在这两个方案中做个权衡。SQL调优和索引使用的本质就是在查询与修改数据之间的权衡。
建立索引语句:
建立where条件的三列索引:

ALTER table cm_settle_partner_day_data add INDEX three_sdb_index  (`status `,`dimension_value`,`business_date`);

建立where条件与查询数据的全索引:

ALTER table fpd_icash_hk_dev_a.cm_settle_partner_day_data 
add INDEX five_sdbac_index (`status`,`dimension_value`,`business_date`,`agent_id`,`cash_value`);

注意:建立联合索引时,要遵循联合索引的最左匹配使用原则,为了最大提升联合索引的使用效率与频率,将where条件中的字段优先放在左边(SQL的执行会先过滤where条件然后拿数据,sql语句的执行顺序from-where-groupby-having-select-orderby-limit),然后再将查询select字段依次放入。
将最频繁使用的字段放在联合索引建立时的最左边开始处,然后依据字段使用频率依次安排。
安插知识点:单列索引和联合索引的数据结构分别是什么样的?B+TREE数据结构在查询数据时如何增大查询效率?

2.第二条SQL是一个多表级联查询SQL,涉及到5张表的多表级联查询,均使用INNER JOIN级联,cm_settle_partner_day_data(1550926),cm_channel_admittance_form(923),cm_channel(1528),cm_sub_category(50),cm_category(21),目前生产库执行时间是1.7秒左右,线上执行时间是5秒左右。除主表外,各个级联表中也有常用单列索引。
原SQL如下:

SELECT
	p.channel_id channelId,
	sum( p.show_value ) showValue,
	sum( p.click_value ) clickValue,
	sum( p.consume_value ) consumeValue,
	sum( p.cash_value ) cashValue,
	ch.channel_name channel_name  FROM  cm_settle_partner_day_data p
	INNER JOIN cm_channel_admittance_form a ON ( a.channel_id = p.channel_id AND a.agent_id = p.agent_id )
	INNER JOIN cm_channel ch ON a.channel_id = ch.channel_id
	INNER JOIN cm_sub_category sc ON ch.sub_category_id = sc.sub_category_id
	INNER JOIN cm_category ca ON sc.category_id = ca.category_id 
WHERE
	a.deleted = 0 
	AND a.biz_type = 1 
	AND ca.category_id = 2 
	AND p.agent_id = 296 
	AND '2020-07' = date_format( business_date, '%Y-%m' ) 
	AND p.STATUS = 1 
	AND p.dimension_value IN ( 1, 3 ) 
GROUP BY
	p.channel_id,
	ch.channel_name;

最初该SQL的执行计划显示查询均有走索引,但是都走的单列索引,因为该SQL涉及的字段较多,很多查询结果与where条件中的字段均没有索引可走,导致SQL执行计划与实际SQL执行效果不符,SQL很慢。最初执行计划截图如下:
在这里插入图片描述
在这里插入图片描述

多次sql语法改写尝试,发现除去where条件后的前三个字段条件后

p.agent_id = 296 
	AND '2020-07' = date_format( business_date, '%Y-%m' ) 
	AND p.STATUS = 1

SQL的查询效率会大幅提升(从1.7秒上升到0.6秒),随即决定,将主表外的其余四张级联表单独组合级联成一张临时表,并把where条件中的前三个条件归入临时表的查询中,完成SQL改写后,执行SQL,发现速度并没有快,查看SQL执行计划,发现主表仍走了效率极差的单列索引idx_channel_id,这是问题所在。(sql与执行计划截图如下)

SELECT
	p.channel_id channelId,
	sum( p.show_value ) showValue,
	sum( p.click_value ) clickValue,
	sum( p.consume_value ) consumeValue,
	sum( p.cash_value ) cashValue,
	four.channel_name channel_name  
	from cm_settle_partner_day_data p 
	,
	(SELECT a.channel_id,a.agent_id,ch.channel_name,a.deleted,a.biz_type,ca.category_id from cm_channel_admittance_form a 
	INNER JOIN cm_channel ch ON a.channel_id = ch.channel_id
	INNER JOIN cm_sub_category sc ON ch.sub_category_id = sc.sub_category_id
	INNER JOIN cm_category ca ON sc.category_id = ca.category_id
	WHERE a.deleted = 0 
	AND a.biz_type = 1 
	AND ca.category_id = 2) four 
WHERE
    p.STATUS = 1 
	AND p.dimension_value IN ( 1, 3 ) 
	AND '2020-07' = date_format( business_date, '%Y-%m' ) 
	AND p.agent_id = 296 
	AND p.agent_id = four.agent_id 
	AND p.channel_id = four.channel_id
GROUP BY
	p.channel_id,
	four.channel_name;

在这里插入图片描述
在这里插入图片描述

因为此时改写后的SQL的where条件中,均使用了主表cm_settle_partner_day_data的字段,考虑到该SQL查询使用的列较多,不易建联合索引(这样的联合索引会特别大,随着数据量的增多,一定会导致问题),所以考虑新建一个where条件中涉及到字段的五列索引,又考虑到此次SQL优化涉及两条线上业务sql,索引建议建立第一条sql中的全列优化索引,一条联合索引优化两条SQL。

ALTER table fpd_icash_hk_dev_a.cm_settle_partner_day_data 
add INDEX five_sdbac_index (`status`,`dimension_value`,`business_date`,`agent_id`,`cash_value`);

建立好索引并测试后,sql查询速度并没有提升,在看SQL的执行计划,主表仍然走了idx_channel_id单列索引,这是MySQL的CBO判断错误导致为SQL选择了一条错误的执行计划,从而导致sql执行效率慢。此处,我又使用了
FORCE INDEX (five_test_index) 语法,强制该条sql走新建的五列联合索引,再次执行sql后,从1.7秒提升到了0.015秒,再次查看改写后的sql执行计划,最新改写的sql使用了新建的联合索引,sql优化完成。(最终优化SQL与截图如下)

SELECT
	p.channel_id channelId,
	sum( p.show_value ) showValue,
	sum( p.click_value ) clickValue,
	sum( p.consume_value ) consumeValue,
	sum( p.cash_value ) cashValue,
	four.channel_name channel_name  
	from cm_settle_partner_day_data p FORCE INDEX (five_test_index)
	,
	(SELECT a.channel_id,a.agent_id,ch.channel_name,a.deleted,a.biz_type,ca.category_id from cm_channel_admittance_form a 
	INNER JOIN cm_channel ch ON a.channel_id = ch.channel_id
	INNER JOIN cm_sub_category sc ON ch.sub_category_id = sc.sub_category_id
	INNER JOIN cm_category ca ON sc.category_id = ca.category_id
	WHERE a.deleted = 0 
	AND a.biz_type = 1 
	AND ca.category_id = 2) four 
WHERE
p.STATUS = 1 
	AND p.dimension_value IN ( 1, 3 ) 
	AND '2020-07' = date_format( business_date, '%Y-%m' ) 
	AND p.agent_id = 296 
	AND p.agent_id = four.agent_id 
	AND p.channel_id = four.channel_id
GROUP BY
	p.channel_id,
	four.channel_name;

在这里插入图片描述
在这里插入图片描述

三、 最终结果

在生产库建立五列联合索引,并引用SQL的新语法后,第二条级联SQL从原来的线上5秒生产库2秒,查询速度提升到0.03秒。而第一条SQL也从线上3秒提升到了0.02秒。查看两条SQL的执行计划,均走新建的联合索引。

四、 后记

在最终上线的索引版本时,有了更多业务上的考量,第二条SQL的查询效率高低远比第一条重要,所以最终上线的SQL索引版本中的最后字段改为了channel_id,最大方便第二条SQL中where条件的索引使用,放弃了第一条SQL的全列覆盖索引最优解的使用。

ALTER table fpd_icash_hk_dev_a.cm_settle_partner_day_data 
add INDEX five_sdbac_index (`status`,`dimension_value`,`business_date`,`agent_id`,` channel_id `);

在本次SQL优化过程中,曾考虑将business_date放在联合索引的最左边(因为该字段比较常用),但导致SQL一直走全表扫描,不再走索引即使加了force index的语法也无济于事,分析原因,大概率是business_date的值比较多,以该字段为首列建立联合索引,会导致二级B+Tree索引过大CBO会判断走全表扫描会更快(事实也确实如此, 线上每条business_date值都不同,有150w条左右)。

建立联合索引时,字段的位置很重要,要详细了解索引底层各类B+Tree数据结构的实现与使用以及SQL的执行机制,即可做出精准判断。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

豪AI冰

感谢支持

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值