前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >用一个性能提升了666倍的小案例说明在TiDB中正确使用索引的重要性

用一个性能提升了666倍的小案例说明在TiDB中正确使用索引的重要性

作者头像
HOHO
发布于 2022-06-12 09:20:07
发布于 2022-06-12 09:20:07
47300
代码可运行
举报
运行总次数:0
代码可运行

背景

最近在给一个物流系统做TiDB POC测试,这个系统是基于MySQL开发的,本次投入测试的业务数据大概10个库约900张表,最大单表6千多万行。

这个规模不算大,测试数据以及库表结构是用Dumpling从MySQL导出,再用Lightning导入到TiDB中,整个过程非常顺利。

系统在TiDB上跑起来后,通过Dashboard观察到有一条SQL非常规律性地出现在慢查询页面中,打开SQL一看只是个单表查询并不复杂,感觉必有蹊跷。

问题现象

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

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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;
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
	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特别大的要引起重视了。

深入分析

根据经验推断,回表多说明索引效果不好,先看一下这个表的总行数是多少:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
mysql> select count(1) from job_cm_data;
+----------+
| count(1) |
+----------+
|   311994 |
+----------+
1 row in set (0.05 sec)

从回表数量来看,这个索引字段的区分度肯定不太行,进一步验证这个推断:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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个条件创建索引:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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需要特别看一下。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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%以上的数据。 有点意思,那就建个索引试试。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
ALTER TABLE `test`.`job_cm_data` 
ADD INDEX `idx_muti`(`pre_excutetime`,`flag`);

这个结果好像和预期的不太对呀,怎么搞成扫描31万行索引了?

别忘了,复合索引有个最左匹配原则,而这个pre_excutetime刚好是范围查询,所以实际只用到了pre_excutetime这个索引,而偏偏整个表的数据都符合筛选的时间段,其实就相当于IndexFullScan了。 那行,再把字段顺序换个位置:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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吧。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2022-06-10,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
Java 8 Streams map() 示例
在Java 8中stream().map(),您可以将对象转换为其他对象。查看以下示例:
三产
2021/01/12
7070
JDK8中的新特性(Lambda、函数式接口、方法引用、Stream)
<font color=red>Java 8</font> (又称为 JDK 8或JDK1.8) 是 Java 语言开发的一个主要版本。 Java 8 是oracle公司于2014年3月发布,可以看成是自Java 5 以来最具革命性的版本。Java 8为Java语言、编译器、类库、开发工具与JVM带来了大量新特性。
鱼找水需要时间
2023/06/01
8250
JDK8中的新特性(Lambda、函数式接口、方法引用、Stream)
Java1.8之Lambda表达式、流Stream学习
答:每个人对函数式编程的理解不尽相同。但其核心是:在思考问题时,使用不可变值和函数,函数对一个值进行处理,映射成另一个值。 2、Java 8的最大变化是引入了Lambda表达式——一种紧凑的、传递行为的方式。
别先生
2021/01/13
7850
Java集合中的ConcurrentModificationException异常
当底层集合在迭代过程中被修改时,快速失败(fail-fast)迭代器可能会抛出ConcurrentModificationException。
崔认知
2024/09/13
1170
Java集合中的ConcurrentModificationException异常
Lambda表达式和流(Streams)简化代码:提升Java开发效率
在现代Java开发中,Lambda表达式和Streams API已经成为不可或缺的工具,它们可以大幅度简化集合操作和数据处理代码,提升代码的可读性和可维护性。本篇博客将详细介绍如何使用Lambda表达式简化集合处理,以及如何利用Streams API进行流式数据处理,让你在编写Java代码时更加高效。即使你是一个初学者,本文也会通过简单的代码示例帮助你快速上手。
默 语
2025/01/20
970
Java函数式编程神器 VAVR(vavr - turns java™ upside down)
什么是函数式编程 基本概念:他是一种编程范式,对于函数式编程来说,它只关心定义输入数据和输出数据相关的关系,数学表达式里面其实是在做一种映射(mapping),输入的数据和输出的数据关系是什么样的,是用函数来定义的。 特征: stateless:函数不维护任何状态。函数式编程的核心精神是 stateless,简而言之就是它不能存在状态,打个比方,你给我数据我处理完扔出来。里面的数据是不变的。 immutable:输入数据是不能动的,动了输入数据就有危险,所以要返回新的数据集。(不可变的) Jav
袁新栋-jeff.yuan
2021/12/07
7900
Java函数式编程神器 VAVR(vavr - turns java™ upside down)
IO密集型任务使用Java的parallelStream并行流,提高性能及隔离故障,如何自定义线程池
在Java中,parallelStream 是 Java 8 引入的 Stream API 的一部分,它允许并行处理集合中的元素。默认情况下,parallelStream 共享使用默认的 ForkJoinPool 作为其线程池,可能对你的业务影响性能,而且起不到隔离的作用。所以我们需要自定义其使用的线程池。
崔认知
2024/09/13
6540
IO密集型任务使用Java的parallelStream并行流,提高性能及隔离故障,如何自定义线程池
Java避坑指南:不要使用双括号初始化技巧,非静态匿名内部类可能导致内存泄露,发生OOM
你是否在浏览MyBatis官方文档的时候,注意到 https://mybatis.org/mybatis-3/statement-builders.html 介绍The SQL Builder Class的时候提到了SQL类,SQL类为我们编写sql或者动态拼接sql提供了便利。但是官方的举的几个例子使用了Java的双括号初始化技巧。
崔认知
2023/06/20
5920
Java避坑指南:不要使用双括号初始化技巧,非静态匿名内部类可能导致内存泄露,发生OOM
java 1.8 stream使用总结(个人总结有一些经典文章的集合)
☀️相关笔记章节: 🌹java 1.8 stream使用总结(个人总结有一些经典文章的集合) 🌹遍历 ArrayList和遍历 Map的几种方式 🌹Java对象,Map,List,Set数组等相互转换大全(详细讲解,附代码,讲解案例) 🌹List,Map多层循环嵌套Demo及其理解 🌹List,Map 三种遍历方式:(总结理解) 🌹List<Map<String, Object>>,Map<String,List<Map<String, Object>>>多方式循环遍历
默 语
2024/11/20
1820
java 1.8 stream使用总结(个人总结有一些经典文章的集合)
Lambda 表达式带来的复杂性的破解之道
但是由于 Lambda 表达式的滥用,代码可读性会变差,那么该如何解决? 本文会讨论一下这个问题,并给出自己的几个解决办法。
明明如月学长
2022/02/15
1.4K0
Java 8 Streams简介-Java快速入门教程
Java 8 中的主要新功能之一是引入了流功能 - java.util.stream,其中包含用于处理元素序列的类。
jack.yang
2025/04/05
1000
java8第二篇
Predicate断言式,判断是否符合指定的条件。个人感觉这个函数式接口,常用于集合的过滤操作,我们可以看下其提供的方法都有哪些。
码农王同学
2020/03/25
3010
聊聊lambda
公众号链接:https://mp.weixin.qq.com/s/MFXRBr16LuGn6G2rlOFFEw
阿超
2022/09/27
5930
聊聊lambda
Java避坑指南:多线程批量调用下游接口,如何正确设置总超时时间
多线程批量调用下游接口,设置总超时时间是一种常见的需求,特别是在需要保证程序在预定时间内必须返回,否则超时设置不合理,导致接口变慢。
崔认知
2024/09/13
4550
Java避坑指南:多线程批量调用下游接口,如何正确设置总超时时间
Java避坑指南:java.util.Arrays工具类避坑记
因为Arrays.asList的参数是一个泛型变长参数,但是java中基本类型是不能泛型化的。
崔认知
2023/06/19
1910
Java避坑指南:java.util.Arrays工具类避坑记
Java Stream 优雅编程
Stream 流式编程的出现,显著推进了Java对函数式编程的支持,它允许开发者可以用声明式的方式处理数据集合(比如列表、数组等),还能有效利用多核处理器进行并行操作,提升应用程序的性能,同时保持代码简洁易读。
测试蔡坨坨
2024/06/18
1840
Java Stream 优雅编程
深入理解Java8 Lambda表达式
匿名函数的应用场景是: 通常是在需要一个函数,但是又不想费神去命名一个函数的场合下使用Lambda表达式。lambda表达式所表示的匿名函数的内容应该是很简单的,如果复杂的话,干脆就重新定义一个函数了,使用lambda就有点过于执拗了。
ZhangXianSheng
2019/09/05
4400
使用Java 注解处理器(Annotation Processor)来避免Spring事务抛出受检异常Exception无法回滚
Java 注解处理器(Annotation Processor)是 Java 编译器的一部分,用于处理源代码中的注解信息。它可以在编译时扫描和处理注解,并生成额外的代码或者进行其它的操作。注解处理器可以用来自动生成代码、进行代码检查、生成文档等。
崔认知
2024/01/22
7000
使用Java 注解处理器(Annotation Processor)来避免Spring事务抛出受检异常Exception无法回滚
Java11~Java17新特性: Text Blocks(文本块)-跨越多行字符串书写的好帮手
在Java在代码中嵌入 HTML, XML, SQL, 或 JSON等多行字符串时,我们需要使用换行符"\n"或者字符串拼接"+"操作符以及转义符,可读性大大降低。或者使用StringBuilder优化长字符串拼接操作。
崔认知
2023/06/20
2.9K0
Java11~Java17新特性: Text Blocks(文本块)-跨越多行字符串书写的好帮手
最常用的 Java 8 中的 Lambda 函数(项目中实用笔记)
Java 8 中的新特性,虽然现在都出到了Java14版本,不过在日常的开发过程中,8的版本是足够使用了,再说现在的8以上的版本也都面向商业收费了,很多新手,我所接触到的,像我那时候一样,追求船新版本,一上来就去学java14的东西,当成一个爱好还行,重心还是要放在实用上
拾荒者的笔记
2020/07/04
3830
推荐阅读
相关推荐
Java 8 Streams map() 示例
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档