用一个性能提升了666倍的小案例说明在TiDB中正确使用索引的重要性
背景
最近在给一个物流系统做TiDB POC测试,这个系统是基于MySQL开发的,本次投入测试的业务数据大概10个库约900张表,最大单表6千多万行。
这个规模不算大,测试数据以及库表结构是用Dumpling从MySQL导出,再用Lightning导入到TiDB中,整个过程非常顺利。
系统在TiDB上跑起来后,通过Dashboard观察到有一条SQL非常规律性地出现在慢查询页面中,打开SQL一看只是个单表查询并不复杂,感觉必有蹊跷。

问题现象
以下是从Dashboard中抓出来的原始SQL和执行计划,总共消耗了1.2s,其中绝大部分时间都花在了Coprocessor扫描数据中:

SELECT {31个字段}
FROM
  job_cm_data
WHERE
  (
    group_id = 'GROUP_MATERIAL'
    AND cur_thread = 1
    AND pre_excutetime < '2022-04-27 11:55:00.018'
    AND ynflag = 1
    AND flag = 0
  )
ORDER BY
  id
LIMIT
  200;
	id                         	task     	estRows	operator info                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                	actRows	execution info                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               	memory 	disk
	Projection_7               	root     	200    	test_ba.job_cm_data.id, test_ba.job_cm_data.common_job_type, test_ba.job_cm_data.org_code, test_ba.job_cm_data.key_one, test_ba.job_cm_data.key_two, test_ba.job_cm_data.key_three, test_ba.job_cm_data.key_four, test_ba.job_cm_data.key_five, test_ba.job_cm_data.key_six, test_ba.job_cm_data.key_seven, test_ba.job_cm_data.key_eight, test_ba.job_cm_data.permission_one, test_ba.job_cm_data.permission_two, test_ba.job_cm_data.permission_three, test_ba.job_cm_data.cur_thread, test_ba.job_cm_data.group_id, test_ba.job_cm_data.max_execute_count, test_ba.job_cm_data.remain_execute_count, test_ba.job_cm_data.total_execute_count, test_ba.job_cm_data.pre_excutetime, test_ba.job_cm_data.related_data, test_ba.job_cm_data.delay_time, test_ba.job_cm_data.error_message, test_ba.job_cm_data.flag, test_ba.job_cm_data.ynflag, test_ba.job_cm_data.create_time, test_ba.job_cm_data.update_time, test_ba.job_cm_data.create_user, test_ba.job_cm_data.update_user, test_ba.job_cm_data.ip, test_ba.job_cm_data.version_num	0      	time:1.17s, loops:1, Concurrency:OFF                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         	83.8 KB	N/A
	└─Limit_14                 	root     	200    	offset:0, count:200                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          	0      	time:1.17s, loops:1                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          	N/A    	N/A
	  └─Selection_31           	root     	200    	eq(test_ba.job_cm_data.ynflag, 1)                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              	0      	time:1.17s, loops:1                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          	16.3 KB	N/A
	    └─IndexLookUp_41       	root     	200    	                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             	0      	time:1.17s, loops:1, index_task: {total_time: 864.6ms, fetch_handle: 26.1ms, build: 53.3ms, wait: 785.2ms}, table_task: {total_time: 4.88s, num: 17, concurrency: 5}                                                                                                                                                                                                                                                                                                                                                                                         	4.06 MB	N/A
	      ├─IndexRangeScan_38  	cop[tikv]	7577.15	table:job_cm_data, index:idx_group_id(group_id), range:["GROUP_MATERIAL","GROUP_MATERIAL"], keep order:true                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  	258733 	time:3.34ms, loops:255, cop_task: {num: 1, max: 2.45ms, proc_keys: 0, rpc_num: 1, rpc_time: 2.43ms, copr_cache_hit_ratio: 1.00}, tikv_task:{time:146ms, loops:257}                                                                                                                                                                                                                                                                                                                                                                                           	N/A    	N/A
	      └─Selection_40       	cop[tikv]	200    	eq(test_ba.job_cm_data.cur_thread, 1), eq(test_ba.job_cm_data.flag, 0), lt(test_ba.job_cm_data.pre_excutetime, 2022-04-27 11:55:00.018000)                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         	0      	time:4.68s, loops:17, cop_task: {num: 18, max: 411.4ms, min: 15.1ms, avg: 263ms, p95: 411.4ms, max_proc_keys: 20480, p95_proc_keys: 20480, tot_proc: 4.41s, tot_wait: 6ms, rpc_num: 18, rpc_time: 4.73s, copr_cache_hit_ratio: 0.00}, tikv_task:{proc max:382ms, min:12ms, p80:376ms, p95:382ms, iters:341, tasks:18}, scan_detail: {total_process_keys: 258733, total_process_keys_size: 100627600, total_keys: 517466, rocksdb: {delete_skipped_count: 0, key_skipped_count: 258733, block: {cache_hit_count: 1296941, read_count: 0, read_byte: 0 Bytes}}}	N/A    	N/A
	        └─TableRowIDScan_39	cop[tikv]	7577.15	table:job_cm_data, keep order:false                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          	258733 	tikv_task:{proc max:381ms, min:12ms, p80:375ms, p95:381ms, iters:341, tasks:18}                                                                                                                                                                                                                                                                                                                                                                                                                                                                              	N/A    	N/A
这个执行计划比较简单,稍微分析一下可以看出它的执行流程:
- 先用
IndexRangeScan算子扫描idx_group_id这个索引,得到了258733行符合条件的rowid - 接着拿rowid去做
TableRowIDScan扫描每一行数据并进行过滤,得到了0行数据 - 以上两步组成了一个
IndexLookUp回表操作,返回结果交给TiDB节点做Limit,得到0行数据 - 最后做一个字段投影
Projection得到最终结果 
从execution info中看到主要的时间都花在Selection_40这一步,初步判断为大量回表导致性能问题。
小技巧:看到IndexRangeScan中Loops特别大的要引起重视了。
深入分析
根据经验推断,回表多说明索引效果不好,先看一下这个表的总行数是多少:
mysql> select count(1) from job_cm_data;
+----------+
| count(1) |
+----------+
|   311994 |
+----------+
1 row in set (0.05 sec)
从回表数量来看,这个索引字段的区分度肯定不太行,进一步验证这个推断:
mysql> select group_id,count(1) from job_cm_data group by group_id;
+------------------------------+----------+
| group_id                     | count(1) |
+------------------------------+----------+
| GROUP_HOUSELINK              |       20 |
| GROUP_LMSMATER               |    37667 |
| GROUP_MATERIAL               |   258733 |
| GROUP_MATERISYNC             |    15555 |
| GROUP_WAREHOUSE_CONTRACT     |        7 |
| GROUP_WAREHOUSE_CONTRACT_ADD |       12 |
+------------------------------+----------+
6 rows in set (0.01 sec)

从上面两个结果可以判断出idx_group_id这个索引有以下问题:
- 区分度非常差,只有6个不同值
 - 数据分布非常不均匀,GROUP_MATERIAL这个值占比超过了80%
 
所以这是一个非常失败的索引。
对于本文中的SQL而言,首先要从索引中扫描出258733个rowid,再拿这258733个rowid去查原始数据,不仅不能提高查询效率,反而让查询变的更慢了。
不信的话,我们把这个索引删掉再执行一遍SQL。
mysql> alter table job_cm_data drop index idx_group_id;
Query OK, 0 rows affected (0.52 sec)

从这个执行计划看到现在已经变成了全表扫描,但是执行时间却比之前缩短了一倍多,而且当命中Coprocessor Cache的时候那速度就更快了:
正当我觉得删掉索引就万事大吉的时候,监控里的Duration 99线突然升高到了200多ms,满脸问号赶紧查一下慢日志是什么情况。
发现这条SQL执行时间虽然变短了,但是慢SQL突然就变多了:
仔细对比SQL后发现,这些SQL是分别查询了group_id的6个值,而且频率还很高。也就是说除了前面贴出来的那条SQL变快,其他group_id的查询都变慢了。
其实这个也在预期内,group_id比较少的数据就算走了索引它的回表次数也很少,这个时间仍然比全表扫描要快的多。
因此要解决这个问题仅仅删掉索引是不行的,不仅慢查询变多duration变高,全表扫描带来的后果导致TiKV节点的读请求压力特别大。
初始情况下这个表只有2个region,而且leader都在同一个store上,导致该节点CPU使用量暴增,读热点问题非常明显。
经过手动切分region后把请求分摊到3个TiKV节点中,但Unified Readpool CPU还是都达到了80%左右,热力图最高每分钟流量6G。

继续盘它。
解决思路
既然全表扫描行不通,那解决思路还是想办法让它用上索引。
经过和业务方沟通,得知这是一个存储定时任务元数据的表,虽然查询很频繁但是每次返回的结果集很少,真实业务中没有那多需要处理的任务。
基于这个背景,我联想到可以通过查索引得出最终符合条件的rowid,再拿这个小结果集去回表就可以大幅提升性能了。
那么很显然,我们需要一个复合索引,也称为联合索引、组合索引,即把多个字段放在一个索引中。对于本文中的案例,可以考虑把where查询字段组成一个复合索引。
但怎么去组合字段其实是大有讲究的,很多人可能会一股脑把5个条件创建索引:
ALTER TABLE `test`.`job_cm_data`
ADD INDEX `idx_muti`(`group_id`, `cur_thread`,`pre_excutetime`,`ynflag`,`flag`);

确实,从这个执行计划可以看到性能有了大幅提升,比全表扫描快了10倍。那是不是可以收工了?还不行。
这个索引存在两个问题:
- 5个索引字段有点太多了,维护成本大
 - 5万多个索引扫描结果也有点太多(因为只用到了3个字段)
 
基于前面贴出来的表统计信息和索引创建原则,索引字段的区分度一定要高,这5个查询字段里面pre_excutetime有35068个不同的值比较适合建索引,group_id从开始就已经排除了,cur_thread有6个不同值每个值数量都很均匀也不适合,ynflag列所有数据都是1可以直接放弃,最后剩下flag需要特别看一下。
mysql> select flag,count(1) from job_cm_data group by flag;
+------+----------+
| flag | count(1) |
+------+----------+
|    2 |   277832 |
|    4 |       30 |
|    1 |    34132 |
+------+----------+
3 rows in set (0.06 sec)
从上面这个输出结果来看,它也算不上一个好的索引字段,但巧就巧在实际业务都是查询flag=0的数据,也就是说如果给它建了索引,在索引里就能排除掉99%以上的数据。
有点意思,那就建个索引试试。
ALTER TABLE `test`.`job_cm_data`
ADD INDEX `idx_muti`(`pre_excutetime`,`flag`);

这个结果好像和预期的不太对呀,怎么搞成扫描31万行索引了?
别忘了,复合索引有个最左匹配原则,而这个pre_excutetime刚好是范围查询,所以实际只用到了pre_excutetime这个索引,而偏偏整个表的数据都符合筛选的时间段,其实就相当于IndexFullScan了。
那行,再把字段顺序换个位置:
ALTER TABLE `test`.`job_cm_data`
ADD INDEX `idx_muti`(`flag``pre_excutetime`);
看到执行时间这下满足了,在没有使用Coprocessor Cache的情况下执行时间也只需要1.8ms。一个小小的索引调整,性能提升666倍。

建复合索引其实还有个原则,就是区分度高的字段要放在前面。因为复合索引是从左往右去对比,区分区高的字段放前面就能大幅减少后面字段对比的范围,从而让索引的效率最大化。
这就相当于层层过滤器,大家都希望每一层都尽可能多的过滤掉无效数据,而不希望10万行进来的时候到最后一层还是10万行,那前面的过滤就都没意义了。在这个例子中,flag就是一个最强的过滤器,放在前面再合适不过。
不过这也要看实际场景,当查询flag的值不为0时,会引起一定量的回表,我们以4(30行)和1(34132行)做下对比:


真实业务中,flag=0的数据不会超过50行,参考上面的结果,50次回表也就10ms以内,性能依然不错,完全符合要求。
我觉得应用层面允许调整SQL的话,再限制下pre_excutetime的最小时间,就可以算是个最好的解决方案了。
最后上一组图看看优化前后的对比。



nice~
总结
这个例子就是提示大家,索引是个好东西但并不是银弹,加的不好就难免适得其反。
本文涉及到的索引知识点:
- 索引字段的区分区要足够高,最佳示例就是唯一索引
 - 使用索引查询的效率不一定比全表扫描快
 - 充分利用索引特点减少回表次数
 - 复合索引的最左匹配原则
 - 复合索引区分度高的字段放在前面
 
碰到问题要能够具体情况具体分析,索引的使用原则估计很多人都背过,怎么能融会贯通去使用还是需要多思考。
索引不规范,DBA两行泪,珍惜身边每一个帮你调SQL的DBA吧。
用一个性能提升了666倍的小案例说明在TiDB中正确使用索引的重要性的更多相关文章
- 我是如何将一个老系统的kafka消费者服务的性能提升近百倍的
		
☞☞☞ 我是如何将一个老系统的kafka消费者服务的性能提升近百倍的 ☜☜☜ ○○○○○○○○○○○○○○○ 大家好,又见面了~ kafka作为一种高吞吐量的分布式发布订阅消息系统,在业务系统中被广泛 ...
 - JavaScript变量提升(Hoisting)的小案例
		
变量提升(Hoisting)的小案例 执行以下代码的结果是什么?为什么? 答案 这段代码的执行结果是undefined 和 2. 这个结果的原因是,变量和函数都被提升(hoisted) 到了函数体的顶 ...
 - 修改一行SQL代码 性能提升了N倍
		
在PostgreSQL中修改了一行不明显的代码,把(ANY(ARRAY[...]) 改成 ANY(VALUES(...))),结果查询时间从20s变为0.2s.最初我们学习使用EXPLAN ANALY ...
 - 修改一行SQL代码 性能提升了100倍
		
在PostgreSQL中修改了一行不明显的代码,把(ANY(ARRAY[...]) 改成 ANY(VALUES(...))),结果查询时间从20s变为0.2s.最初我们学习使用 EXPLAN ANAL ...
 - 【笔记】直接使用protocol buffers的底层库,对特定场景的PB编解码进行处理,编码性能提升2.4倍,解码性能提升4.8倍
		
接上一篇文章:[笔记]golang中使用protocol buffers的底层库直接解码二进制数据 最近计划优化prometheus的remote write协议,因为业务需要,实现了一个remote ...
 - Nacos 2.0 正式发布,性能提升了 10 倍!!
		
前不久,在3月20号,Nacos 2.0.0 正式发布了!我简单看了下官方的介绍,可能nacos未来逐渐会成为各大公司作为服务治理和配置中心的主要中间件. Nacos 简介:一个更易于构建云原生应用的 ...
 - Linux 性能分析调优 (四)——案例篇:系统中出现大量不可中断进程和僵尸进程怎么办
		
之前讲到 CPU 使用率的类型.除了上一节提到的用户 CPU 之外,它还包括系统 CPU(比如上下文切换).等待 I/O 的 CPU(比如等待磁盘的响应)以及中断 CPU(包括软中断和硬中断)等. 在 ...
 - Vue 自定义一个插件的用法、小案例及在项目中的应用
		
1.开发插件 install有两个参数,第一个是Vue构造器,第二个参数是一个可选的选项对象 MyPlugin.install = function (Vue, options) { // 1 ...
 - 存算分离下写性能提升10倍以上,EMR Spark引擎是如何做到的?
		
引言 随着大数据技术架构的演进,存储与计算分离的架构能更好的满足用户对降低数据存储成本,按需调度计算资源的诉求,正在成为越来越多人的选择.相较 HDFS,数据存储在对象存储上可以节约存储成本,但与此 ...
 
随机推荐
- css让文字显示特定行数,多余的显示省略号
			
/*css*/ .p{ width: 200px; word-break: break-all; text-overflow: ellipsis; display: -webkit-box; /** ...
 - python的编译和解释
			
编译和解释 1.编译: 将源代码一次性转换成目标代码的过程 源代码 → 编辑器 →目标代码 →程序执行(同时程序输入)→结果输出 2.解释: 将源代码逐条转换成目标代码同时逐条运行的过程 源代码+程序 ...
 - Java---变量和基本数据类型
			
变量 在Java中,变量分为两种:基本类型的变量和引用类型的变量. 在Java中变量必须先定义后使用,在定义变量的时候可以给它一个初始值.如果不写初始值,默认为0或空. 变量的一个重要特点是可以重新赋 ...
 - print,printf,println的区别,以及\r,\n,\r\n的区别
			
1.常用的是println,就是换行输出 2.print,不换行输出 3.printf常使用于格式转化 public class Print { public static void main(Str ...
 - vue elementUI 之 this.$confirm 用法
			
this.$confirm('您确定退出当前账号吗?', '提示', { confirmButtonText: '确定', ...
 - Android四大组件——Activity——Activity之间通信下
			
显式意图:一般是用于应用内组件跳转.(如从ActivityA跳转到ActivityB) 隐式意图:一半用于应用之间的跳转.(如从ActivityA跳转到拨号) 隐式意图跳转到百度: 只需将前面Main ...
 - 02. 树莓派初始配置——安装rz命令
			
安装rz命令 如果你的ssh工具不带xftp文件上传功能,则需要安装使用ZMODEM协议的rz.sz命令. 1. 用root权限登录 2. 创建下载的目录地址(不创建也行,创建后方便以后文件管理) m ...
 - python学习-Day27
			
目录 今日内容详细 动态方法与静态方法 动态方法 绑定给对象的方法 绑定给类的方法 静态方法 继承 继承的含义 继承的目的 继承的基本使用 继承的本质 名字的查找顺序 不继承的情况下 单继承的情况下 ...
 - python学习-Day20
			
目录 今日内容详细 作业讲解 re模块补充说明 findall的优先级查询 通过索引的方式单独获取分组内匹配到的数据 分组之后还可以给组起别名 split的优先级查询 collections模块 具名 ...
 - 《Streaming Systems》第三章: Watermarks
			
定义 对于一个处理无界数据流的 pipeline 而言,非常需要一个衡量数据完整度的指标,用于标识什么时候属于某个窗口的数据都已到齐,窗口可以执行聚合运算并放心清理,我们暂且就给它起名叫 waterm ...