tidb 执行计划的order by limit 疑问

版本v5.3.3
请问一下
利用sysbench 导入了表数据到tidb以后,对sbtest1表进行了如下查询select c from sbtest1 where k>1 order by id limit 2;
表结构


执行计划
mysql> explain analyze select c from sbtest1 where k>1 order by id limit 2;
±-------------------------------±--------±--------±----------±--------------±----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------±------------------------±----------±-----+
| id | estRows | actRows | task | access object | execution info | operator info | memory | disk |
±-------------------------------±--------±--------±----------±--------------±----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------±------------------------±----------±-----+
| Projection_8 | 2.00 | 2 | root | | time:1.87ms, loops:2, Concurrency:OFF | sbtest.sbtest1.c | 646 Bytes | N/A |
| └─Limit_12 | 2.00 | 2 | root | | time:1.87ms, loops:2 | offset:0, count:2 | N/A | N/A |
| └─TableReader_25 | 2.00 | 2 | root | | time:1.86ms, loops:1, cop_task: {num: 1, max: 1.81ms, proc_keys: 32, rpc_num: 1, rpc_time: 1.8ms, copr_cache_hit_ratio: 0.00} | data:Limit_24 | 2.10 KB | N/A |
| └─Limit_24 | 2.00 | 2 | cop[tikv] | | tikv_task:{time:0s, loops:1}, scan_detail: {total_process_keys: 32, total_process_keys_size: 7168, total_keys: 80, rocksdb: {delete_skipped_count: 5, key_skipped_count: 84, block: {cache_hit_count: 2, read_count: 0, read_byte: 0 Bytes}}} | offset:0, count:2 | N/A | N/A |
| └─Selection_23 | 2.00 | 32 | cop[tikv] | | tikv_task:{time:0s, loops:1} | gt(sbtest.sbtest1.k, 1) | N/A | N/A |
| └─TableFullScan_22 | 2.00 | 32 | cop[tikv] | table:sbtest1 | tikv_task:{time:0s, loops:1} | keep order:true | N/A | N/A |
±-------------------------------±--------±--------±----------±--------------±----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------±------------------------±----------±-----+

这里为什么选择table full scan的原因我猜想如下
1.因为id是主键,在全表扫描的过程中如果遇到k>1的话,直接就可以取出c字段的值,这个时候id本身就是有序的了,不需要额外排序,所以只要遇到2次k>1的值,这条语句就可以跑完了,这个时候全表扫描并不慢,但我认为这是建立在k>1的值全表扫描很快就能找到
但是有可能存在这么一个情况,假设满足k>1的值需要全表扫描很久才能找到,那是不是说明这个时候全表扫描并不是最优的解决方案?这个时候,可能利用k字段的索引去查找反而是更优的解决方案,不知道这样的猜想对不对
对比MySQL的执行计划,MySQL优化器的默认行为也是类似tidb这种全表扫描(直接扫描整个主键索引),但是通过参数调整,可以让优化器选择走k字段索引

2.不是很能够理解,tidb的执行计划,actRows是32?这里请问怎么解释

1.第一个全表的执行计划没有问题,tidb可以加hit来控制走索引
2.32的这个是看着像是一个固定的算子输出,当limit小于32时astrows 32,当limit大于32时是一个32的倍数(观察只在order by 中有限制),具体的源码分析等资料没有找到具体的解释;

  1. 看下k>1的有多少条,和全量对比下
  2. show table sbtest1 regions看下region数量

1.我自己实验了下,和这个原因应该是有关系的,如果通过索引去扫很多数据,再加上回表的代价就不如全表扫描来的快,这边请问下老师,tidb 优化器怎么知道通过索引扫描的代价更大,是结合哪部分的统计信息的来的?

谢谢老师,确实是这样,但是不是很理解为什么这么设计,按理来说这里actrows是实际扫描的行数,但是这里actrows并不是实际扫描的行数?

Actrows 是实际返回行数,当前这个是返回了32条给tidb

你看sql执行每次返回的数据一样吗,怎么感觉有点不对呢?两个sql分别trace下看看region id一样吗


为什么实际返回32条呢?

1、 1个region会构建一个cop task查询请求,表中有4个region ,所以trace能看到4个
2、 因为SQL是按ID排序后limit 而ID列正好是也是rowid列,虽然全表扫描trace显示 4个cop task,但是实际只有1个cop task下发执行,其他的都被终止了
3、数据返回是以chunk为单位,32应该是默认初始大小当请求,每next调用按倍数增长直到max_chunk_size,当Limit 32时只有一个请求就满足要求了 actrows 32,当limit 33时就需要2个next请求,返回数量32+64为96,换成limit 97后就会成为224

老哥,我是这么理解的。对于limit情况分两种:
1、select * from A limit N,这种简单语句如果limit下推,N>=10万则采用tidb_distsql_scan_concurrency对并发cop_task请求,如果N<10万则串行执行cop_task 请求,直到算子limit满足查询条件后终止后续cop_task请求。
2、其它复杂情况分两种:一种是keeporder=false的,采用tidb_distsql_scan_concurrency做并发,另一种是keeporder=true的,采用2做并发。这里走的是索引,keeporder=true,采用第二种情况。
第一个chunk也就是newFirstChunk初始值在limit算子这里是min(limit offset+count,max_chunk_size默认1024行),所以只要是limit N,这个N小于1024,那么第一次取得数据都是N条记录作为requireRows,但是接下来的我理解的都是按照min(剩余记录数,max_chunk_size)来取。因此这里的limit 32或者33 都应该在limit算子Open中的newFirstChunk调用一次next,然后在limit算子的Next中调用一次Next,但不需要读取数据了(因此执行信息中应该看到limit算子loop2次,table_reader算子loop1次)。
在做每个算子open操作的时候会调用newFirstChunk,如果算子需要的记录数比open时候newFirstChunk中的requiredRows小那么无需并行,直接一个cop_task完成,如果多那么会用到上述说的并行规则。

我的理解和这里的现象有一些出路,等待专家们协助解释解释。

补充下我对楼主的相同执行计划的模拟情况的explain analyze执行情况。limitN的Next调用行为.txt (21.9 KB)

可以看到不管limit32还是33 都是一样的loop(next)行为,但是在大于max_chunk_size后,比如1025那么loop增加一个,设置为2049,loop又增加一个。对于cop_task大于3的情况可以看到采用的concurrency=2的并行策略(cop_task中的num*avg/2 近似等于time)

2、 因为SQL是按ID排序后limit 而ID列正好是也是rowid列,虽然全表扫描trace显示 4个cop task,但是实际只有1个cop task下发执行,其他的都被终止了
大佬请问下,确实是1个cop task,但是为啥---->但是实际只有1个cop task下发执行,其他的都被终止了,这怎么理解呢?

看楼主这里limit 2还是limit 32需要tikv上的full table scan都是需要扫描32行记录,这个想不通。。。 数据完全一样的情况下,limit 32肯定要扫描的更多才对。

因为你一个cop_task就能获取所有想要的结果了,当limit算子得到想要的结果后就停止取数,然后剩下的cop_task就取消了。cop_task总数是按照full table scan来做的,完成第一个的时候就满足数据提取任务,后续的提取计划就终止啦。


forever 老哥说的,感觉这个解释比较合理

看到了,原来limit 2那里的k>1,后面的limit 32那里k>2。条件不一样。:innocent:

k>1 limit 32 actRows 也是32

帮忙发下,limit 分别为3,1023,1025,2049这几个条件下的执行信息?