https://tidb.net/book/tidb-monthly/2022/2022-04/usercase/index-666

背景

最近在给一个物流系统做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)0time: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.15table:job_cm_data, keep order:false258733 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吧。

[转帖]在 TiDB 中正确使用索引,性能提升 666 倍的更多相关文章

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

    背景 最近在给一个物流系统做TiDB POC测试,这个系统是基于MySQL开发的,本次投入测试的业务数据大概10个库约900张表,最大单表6千多万行. 这个规模不算大,测试数据以及库表结构是用Dump ...

  2. 在MongoDB中创建一个索引而性能提升1000倍的小例子

    在https://www.cnblogs.com/xuliuzai/p/9965229.html的博文中我们介绍了MongoDB的常见索引的创建语法.部分同学还想看看MongoDB的威力到底有多大,所 ...

  3. 在PYTHON中使用StringIO的性能提升实测(更新list-join对比)

    刚开始学习PYTHON,感觉到这个语言真的是很好用,可以快速完成功能实现. 最近试着用它完成工作中的一个任务:在Linux服务器中完成对.xml.gz文件的解析,生成.csv文件,以供SqlServe ...

  4. .NET 7 中 LINQ 的疯狂性能提升

    LINQ 是 Language INtegrated Query 单词的首字母缩写,翻译过来是语言集成查询.它为查询跨各种数据源和格式的数据提供了一致的模型,所以叫集成查询.由于这种查询并没有制造新的 ...

  5. 0103MySQL中的B-tree索引 USINGWHERE和USING INDEX同时出现

    转自博客http://www.amogoo.com/article/4 前提1,为了与时俱进,文中数据库环境为MySQL5.6版本2,为了通用,更为了避免造数据的痛苦,文中所涉及表.数据,均来自于My ...

  6. (译)MySQL 8.0实验室---MySQL中的倒序索引(Descending Indexes)

    译者注:MySQL 8.0之前,不管是否指定索引建的排序方式,都会忽略创建索引时候指定的排序方式(语法上不会报错),最终都会创建为ASC方式的索引,在执行查询的时候,只存在forwarded(正向)方 ...

  7. Oracle中索引的使用 索引性能优化调整

    索引是由Oracle维护的可选结构,为数据提供快速的访问.准确地判断在什么地方需要使用索引是困难的,使用索引有利于调节检索速度. 当建立一个索引时,必须指定用于跟踪的表名以及一个或多个表列.一旦建立了 ...

  8. 正确使用索引(sql优化),limit分页优化,执行计划,慢日志查询

    查看表相关命令 - 查看表结构   desc 表名- 查看生成表的SQL   show create table 表名- 查看索引   show index from  表名 使用索引和不使用索引 由 ...

  9. MySQL之索引以及正确使用索引

    一.MySQL中常见索引类型 普通索引:仅加速查询 主键索引:加速查询.列值唯一.表中只有一个(不可有null) 唯一索引:加速查询.列值唯一(可以有null) 组合索引:多列值组成一个索引,专门用于 ...

  10. sql server中使用组合索引需要注意的地方

    一.使用组合索引需要注意的地方 1.索引应该建在选择性高的字段上(键值唯一的记录数/总记录条数),选择性越高索引的效果越好.价值越大,唯一索引的选择性最高: 2.组合索引中字段的顺序,选择性越高的字段 ...

随机推荐

  1. CSS3学习笔记-文字特效

    CSS3中提供了许多有趣和实用的文字特效,可以让我们的文本内容更加生动有趣,下面介绍一些常用的文字特效. 文本阴影 使用text-shadow属性可以为文本添加阴影效果,语法如下: text-shad ...

  2. 斯坦福 UE4 C++ ActionRoguelike游戏实例教程 04.角色感知组件PawnSensingComponent和更平滑的转身

    斯坦福课程 UE4 C++ ActionRoguelike游戏实例教程 0.绪论 概述 本文章对应课程第十一章 43.44节.本文讲述PawnSensingComponent中的视觉感知的使用,以及对 ...

  3. AnimatedList 实现动态列表

    AnimatedList实现动画  AnimatedList 和 ListView 的功能大体相似,不同的是, AnimatedList 可以在列表中插入或删除节点 时执行一个动画,在需要添加或删除列 ...

  4. 为AR&VR黑科技:以“自由视角”360度尽展舞台唯美

    摘要:看华为的黑科技,如何用"自由视角"让观众感受舞台"风暴"的魅力所在. "风暴"降临 2021年1月9日晚上,我坐在电视机前,等待湖南卫 ...

  5. Python图像处理丨带你掌握图像几何变换

    摘要:本篇文章主要讲解图像仿射变换和图像透视变换,通过Python调用OpenCV函数实. 本文分享自华为云社区<[Python图像处理] 十二.图像几何变换之图像仿射变换.图像透视变换和图像校 ...

  6. Kubernetes(K8S) helm chart

    感觉和放到一个 yaml 文件中,用 ---- 分隔,操作繁琐程度上,没有太大区别 创建自定义 Chart # 创建自定义的 chart 名为 mychart [root@k8smaster ~]# ...

  7. Kubernetes(K8S) Deployment 拉取阿里云镜像部署

    Docker Image 推到阿里云仓库,可以看 SpringBoot Docker 发布到 阿里仓库 1. 阿里镜像仓库加了授权,所以 K8S 拉之前要做下授权处理 [root@k8smaster ...

  8. docker-compose部署SpringCloud

    1.安装 docker-compose 将 docker-compose-Linux-x86_64 传到  /usr/local/bin 目录下,并改名为 docker-compose 2.设置权限  ...

  9. WebApi 接口请求耗时记录

    .Net Core NLog 配置 通过日志,记录每个接口请求的耗时情况 结合  <logger name="*" level="Trace" write ...

  10. loguru python中记录日志

    loguru python中记录日志 安装 pip install loguru 使用 from loguru import logger # logger.add('ck/test_log.log' ...