研究一条distinct+order by+limit的SQL执行过程,发现limit影响排序结果的彩蛋

版权声明:本文原创,转载请注明出处。 https://blog.csdn.net/weixin_39004901/article/details/88970169

假设有如下表:

mysql> select * from tab;
+----+------+
| id | col1 |
+----+------+
|  1 |    2 |
|  2 |    2 |
|  3 |    5 |
|  4 |    3 |
|  5 |    3 |
|  6 |    4 |
+----+------+
6 rows in set (0.01 sec)

mysql> desc tab;
+-------+---------+------+-----+---------+-------+
| Field | Type    | Null | Key | Default | Extra |
+-------+---------+------+-----+---------+-------+
| id    | int(11) | NO   | PRI | NULL    |       |
| col1  | int(11) | YES  |     | NULL    |       |
+-------+---------+------+-----+---------+-------+
2 rows in set (0.00 sec)

那么,下面的SQL是怎样的执行过程呢?
select distinct id from tab order by col1 desc limit 1,2;
1.先执行select distinct id from tab order by col1 desc,在筛选limit 1,2;?等效于select * from (select distinct id from tab order by col1 desc) a limit 1,2
2.先执行select * from tab order by col1 desc limit 1,2;,再选出distinct id?等效于select distinct id from (select * from tab order by col1 desc limit 1,2) a
3.先执行select * from tab order by col1 desc,再从结果集中第一行数据进行去重,直到取到3个数值,然后拿后面2个?

我们先从实际数据来验证一下:
首先,先执行一下select distinct id from tab order by col1 desc limit 1,2;,看看实际的返回结果是什么:

mysql> select distinct id from tab order by col1 desc limit 1,2;
+----+
| id |
+----+
|  6 |
|  5 |
+----+
2 rows in set (0.00 sec)

对于第一种猜想:

mysql> select * from (select distinct id from tab order by col1 desc) a limit 1,2;
+----+
| id |
+----+
|  6 |
|  4 |
+----+
2 rows in set (0.00 sec)

对于第二种猜想:

mysql> select distinct id from (select * from tab order by col1 desc limit 1,2) a;
+----+
| id |
+----+
|  6 |
|  5 |
+----+
2 rows in set (0.00 sec)

对于第三种猜想:

mysql> select * from tab order by col1 desc;
+----+------+
| id | col1 |
+----+------+
|  3 |    5 |
|  6 |    4 |
|  4 |    3 |
|  5 |    3 |
|  1 |    2 |
|  2 |    2 |
+----+------+
6 rows in set (0.00 sec)
--然后根据以上结果,执行limit 1,2,从第一行数据开始,取三个唯一id,即3,6,4,取后面两个数即是6,4

从上面三个测试来看,只有第二种猜想得到的结果才是正确,那么,这一种猜想就是MySQL真实的执行方式了吗?答案是否定的。再看下面的例子。
同样是上面的测试表tab,数据不变,我们将上面SQL中的col1和id互换位置,即:
select distinct col1 from tab order by id desc limit 1,2;
该SQL结果是:

mysql> select distinct col1 from tab order by id desc limit 1,2;
+------+
| col1 |
+------+
|    3 |
|    5 |
+------+
2 rows in set (0.00 sec)

那么,按照上面的第二种猜想,该SQL等效于select distinct col1 from (select * from tab order by id desc limit 1,2) a;结果是:

mysql> select distinct col1 from (select * from tab order by id desc limit 1,2) a;
+------+
| col1 |
+------+
|    3 |
+------+
1 row in set (0.00 sec)

显然,结果并不一致,第二种猜想也沦陷了。
反而,按照第三种猜想,结果才是正确的:

mysql> select * from tab order by id desc;
+----+------+
| id | col1 |
+----+------+
|  6 |    4 |
|  5 |    3 |
|  4 |    3 |
|  3 |    5 |
|  2 |    2 |
|  1 |    2 |
+----+------+
6 rows in set (0.00 sec)
--然后取得三个唯一col1值:4,3,5,取后面两个即3,5,与原SQL结果一致。

那么到底哪一种才是呢,我认为是第三种,那为什么第一条实例SQL对于第三种猜想行不通呢?听我解释。
对于本文的第一条实例SQLselect distinct id from tab order by col1 desc limit 1,2;,第三种猜想之所以得到的结果不一致,是因为limit 字句导致了select * from tab order by col1 desc排序结果有了变化,看数据:

--对于没有limit字句的排序结果是下面的样子,注意id=4排在id=5前面:
mysql> select * from tab order by col1 desc;           
+----+------+
| id | col1 |
+----+------+
|  3 |    5 |
|  6 |    4 |
|  4 |    3 |
|  5 |    3 |
|  1 |    2 |
|  2 |    2 |
+----+------+
6 rows in set (0.00 sec)
--加了limit字句之后,id=5却排在id=4前面:
mysql> select * from tab order by col1 desc limit 1,2;
+----+------+
| id | col1 |
+----+------+
|  6 |    4 |
|  5 |    3 |
+----+------+
2 rows in set (0.00 sec)
也就是说,加了limit 1,2这个子句之后,实际的排序结果应该是:
+----+------+
| id | col1 |
+----+------+
|  3 |    5 |
|  6 |    4 |
|  5 |    3 |
|  4 |    3 |
...
+----+------+

也就是说,select distinct id from tab order by col1 desc limit 1,2;的具体执行过程是:
按照以下加了limit 1,2子句后导致的排序结果:

+----+------+
| id | col1 |
+----+------+
|  3 |    5 |
|  6 |    4 |
|  5 |    3 |
|  4 |    3 |
...
+----+------+

从结果集第一行开始,将id放入临时表中,临时表的结构大概是一个只一列的表,且该列上有一个唯一索引,在从结果集取数据插入临时表的过程中,由唯一索引来过滤重复的数据,来达到去重的效果。那么整个过程就是:
1.取id=3放入临时表第一行;
2.取id=6,与id=3不重复,放入临时表第二行;
3.取id=5,与id=3,id=6不重复,放入临时第三行;
4.对于limit 1,2,需要放入临时表3条数据,并顺序取出后面两条,即取出id=6,id=5,与原SQL结果一致。

那么整个过程,即使limit 1,2影响了排序,但始终还是要遍历整个表,按照col1来排序,需要读取6行数据,再加上临时表的3行,整个过程应该是读了9行数据。我们通过slow log的Rows_examined可以验证这个数据:

# Time: 2019-04-02T05:30:23.462551Z
# User@Host: root[root] @ localhost []  Id:     9
# Query_time: 0.000503  Lock_time: 0.000178 Rows_sent: 2  Rows_examined: 9
SET timestamp=1554183023;
select distinct id from tab order by col1 desc limit 1,2;

以上证明了对于第一个示例SQL,第三种猜想成立的理由。
那么,为什么第二个示例SQL,第三种猜想就直接成立了呢?因为在第二个示例SQL中,order by id desc中的id是主键,即是limit子句会影响排序结果,也是在相同排序列值的结果受影响,而id是唯一的,也就是说排序结果也是唯一的,是不受limit影响的。

所以,对于select distinct … from tab order by … limit …;这类SQL,执行方式应该是如下:
按照受limit子句影响的排序结果,从结果集的第一行开始遍历,将不重复的值放入临时表中,直到数据的数量满足limit的取数要求。

SQL的执行逻辑解决了,那么limit子句为什么会影响排序结果呢?这个问题还没研究透,有做过一些测试,但是得到的答案还是不那么确定,估计是跟优化器在执行这种SQL时对主键索引的访问方式有关。

再说一个额外的话题,上面已经解释了select distinct id from tab order by col1 desc limit 1,2;读数据的行数是9,那么对于第二个示例SQLselect distinct col1 from tab order by id desc limit 1,2;读数据的行数也是9吗?我们看一下slow log的信息:

# Time: 2019-04-02T05:42:59.280637Z
# User@Host: root[root] @ localhost []  Id:     9
# Query_time: 0.000513  Lock_time: 0.000132 Rows_sent: 2  Rows_examined: 7
SET timestamp=1554183779;
select distinct col1 from tab order by id desc limit 1,2;

可以看到,Rows_examined是7,也就是说,这个SQL只读了7行数据。为什么不一样呢?看我解释。
解释的过程也是需要结合SQL执行过程:

--第一步,看排序结果
--首先由于order by 主键,所以排序结果并不受影响,直接看一下排序结果:
mysql> select * from tab order by id desc;
+----+------+
| id | col1 |
+----+------+
|  6 |    4 |
|  5 |    3 |
|  4 |    3 |
|  3 |    5 |
|  2 |    2 |
|  1 |    2 |
+----+------+
6 rows in set (0.00 sec)
--第二步,取唯一值到临时表中,对于上述的排序结果及SQL的limit子句需要3个唯一值,所以应该取的数是:
+----+------+
| id | col1 |
+----+------+
|  6 |    4 |
|  5 |    3 |
|  4 |    3 |
|  3 |    5 |
+----+------+
所以临时表里的数据应该是:
+------+
| col1 |
+------+
|    4 |
|    3 |
|    5 |
+----+------+

那么从上面的过程来看,Rows_examined应该是6+3=9才对啊?为什么slow log显示是7呢?
其实,对于第一步的排序,由于是按照主键倒序排序,并不需要遍历整个表,只需要从主键索引最右端一直往左读,直到读取到足够数量的col1值即可。
即主键索引上读4行+临时表3行=7行。

猜你喜欢

转载自blog.csdn.net/weixin_39004901/article/details/88970169