作为一名开发者,我们经常会遇到各种数据库问题。其中,MySQL排序结果不一致问题是一个比较常见的问题。当我们在使用MySQL进行排序时,有时候会发现相同的查询多次执行,但排序结果却不一致。这个问题可能会给我们的业务带来困扰和不确定性。
首先我们来还原下现象,然后我们在分析下原因,找到解决办法。
CREATE TABLE `tb1` (
`id` BIGINT ( 20 ) NOT NULL AUTO_INCREMENT,
`a` DECIMAL ( 19, 2 ) NOT NULL,
`acid` BIGINT ( 20 ) NOT NULL,
`prid` BIGINT ( 20 ) NOT NULL,
PRIMARY KEY ( `id` ),
KEY `idx_prid` ( `prid` ),
KEY `idx_acid` ( `acid` )
) ENGINE = INNODB AUTO_INCREMENT = 0 DEFAULT CHARSET = utf8
注意字段a 上面是没有索引的。
INSERT INTO `tb1` (`id`, `a`, `acid`, `prid`)
VALUES
(1,2.00,3,2),(2,3.00,3,2),(3,4.00,2,3),
(4,5.00,2,3),(5,6.00,2,3),(6,8.00,2,3),
(7,10.00,2,3),(8,12.00,2,3),(9,16.00,2,3),
(10,20.00,2,3),(11,6.00,2,4),(12,8.00,2,4),
(13,10.00,2,4),(14,12.00,2,4),(15,5.00,2,2),
(16,6.00,2,2);
mysql> select * from tb1 order by a desc limit 4;
+----+-------+------+------+
| id | a | acid | prid |
+----+-------+------+------+
| 10 | 20.00 | 2 | 3 |
| 9 | 16.00 | 2 | 3 |
| 14 | 12.00 | 2 | 4 |
| 8 | 12.00 | 2 | 3 |
+----+-------+------+------+
4 rows in set (0.00 sec)
得到id 为10, 9, 14, 8 的结果集
mysql> select * from tb1 order by a desc limit 3;
+----+-------+------+------+
| id | a | acid | prid |
+----+-------+------+------+
| 10 | 20.00 | 2 | 3 |
| 9 | 16.00 | 2 | 3 |
| 8 | 12.00 | 2 | 3 |
+----+-------+------+------+
3 rows in set (0.00 sec)
得到id 为10 9 8 的结果集
mysql> alter table tb1 add key ind_tb1a(a);
Query OK, 0 rows affected (0.00 sec)
Records: 0 Duplicates: 0 Warnings: 0
mysql> select * from tb1 order by a desc limit 3;
+----+-------+------+------+
| id | a | acid | prid |
+----+-------+------+------+
| 10 | 20.00 | 2 | 3 |
| 9 | 16.00 | 2 | 3 |
| 8 | 12.00 | 2 | 3 |
+----+-------+------+------+
3 rows in set (0.00 sec)
得到id 为10 9 8 的结果集
mysql> select * from tb1 order by a desc limit 4;
+----+-------+------+------+
| id | a | acid | prid |
+----+-------+------+------+
| 10 | 20.00 | 2 | 3 |
| 9 | 16.00 | 2 | 3 |
| 14 | 12.00 | 2 | 4 |
| 8 | 12.00 | 2 | 3 |
+----+-------+------+------+
4 rows in set (0.00 sec)
得到id 为10, 9, 14, 8 的结果集
从上面的测试来看对于一个非唯一字段 无论是否含有索引,结果集都是不确定的。
分析不同limit N下返回的数据,发现顺序不一致的结果集有一个共同特点——顺序不一致的这几条数据,他们用来排序的参数值rank相同;
也就是说,带limit的order by查询,只保证排序值rank不同的结果集的绝对有序,而排序值rank相同的结果不保证顺序;推测MySQL对order by limit进行了优化;limit n, m不需返回全部数据,只需返回前n项或前n + m项;
上面的推测在MySQL官方文档中找到了相关的说明
If an index is not used for ORDER BY but a LIMIT clause is also present,
the optimizer may be able to avoid using a merge file and
sort the rows in memory using an in-memory filesort operation.
For details, see The In-Memory filesort Algorithm.
也就是说在ORDER BY + LIMIT的查询语句中,如果ORDER BY不能使用索引的话,优化器可能会使用in-memory sort操作;
其实排序算法中,适用于取前n的算法也只有堆排序了;
参考The In-Memory filesort Algorithm可知MySQL的filesort有3种优化算法,分别是:
基本filesort
改进filesort
In memory filesort
而上面的提到的In memory filesort,官方文档这么说明:
The sort buffer has a size of sort_buffer_size.
If the sort elements for N rows are small enough to fit in the sort buffer (M+N rows if M was specified),
the server can avoid using a merge file and performs an in-memory sort by treating the sort buffer as a priority queue.
这里提到了一个关键词——优先级队列;而优先级队列的原理就是二叉堆,也就是堆排序;
我们看下堆排序步骤
(1)建堆:建堆结束后,数组中的数据已经是按照大顶堆的特性组织的;数组中的第一个元素就是堆顶;
(2)取出最大值(类似删除操作):将堆顶元素a[1]与最后一个元素a[n]交换,这时,最大元素就放到了下标为n的位置;
(3)重新堆化:交换后新的堆顶可能违反堆的性质,需要重新进行堆化;
(4)重复(2)(3)操作,直到最后堆中只剩下下标为1的元素,排序就完成了;
简单说下堆排序是什么,想要深入了解的同学可以查找相关资料看下。
堆排序是指利用堆这种数据结构进行排序的一种算法;
排序过程中,只需要个别临时存储空间,所以堆排序是原地排序算法,空间复杂度为O(1);
堆排序的过程分为建堆和排序两大步骤;建堆过程的时间复杂度为O(n),排序过程的时间复杂度为O(nlogn),所以,堆排序整体的时间复杂度为O(nlogn);
堆排序不是稳定的算法,因为在排序的过程中,每取出一次堆顶元素,都需要将堆的最后一个节点跟堆顶节点互换的操作(也就是上面提到的"堆顶的移出操作"),所以可能把值相同数据中,原本在数组序列后面的元素,通过交换到堆顶,从而改变了这些值相同的元素的原始相对顺序;因此是不稳定的;
这也就是为什么当改变SQL的limit的大小,返回的排序结果中,相同排序值rank的记录的相对顺序发生变化的根本原因。
所以会出现SQL查询语句同时包含order by和limit时,当修改limit的值,可能导致 “相同排序值的元素之间的现对顺序发生改变”
原因就是MySQL对limit的优化,导致当取到指定limit的数量的元素时,就不再继续添加参与排序的记录了,因此参与排序的元素的数量变化了;而MySQL排序使用的In memory filesort是基于优先级队列,也就是堆排序,而堆排序时不稳定的,会改变排序结果中,相同排序值rank的记录的相对顺序;
我们以上面的表为例来说明解决办法。
如果业务属性确保 a 字段不能唯一,则需要针对排序结果再加上 一个唯一字段的排序 比如id
mysql> select * from tb1 order by a desc ,id desc limit 4;
+----+-------+------+------+
| id | a | acid | prid |
+----+-------+------+------+
| 10 | 20.00 | 2 | 3 |
| 9 | 16.00 | 2 | 3 |
| 14 | 12.00 | 2 | 4 |
| 8 | 12.00 | 2 | 3 |
+----+-------+------+------+
4 rows in set (0.00 sec)
mysql> select * from tb1 order by a desc ,id desc limit 3;
+----+-------+------+------+
| id | a | acid | prid |
+----+-------+------+------+
| 10 | 20.00 | 2 | 3 |
| 9 | 16.00 | 2 | 3 |
| 14 | 12.00 | 2 | 4 |
+----+-------+------+------+
3 rows in set (0.00 sec)
使用order by id/unique_key 排序之后,前三个结果集是一致的10,9,14 。 结果集满足我们的需求。从而解决不确定性带来的问题。
排序值带上主键id,即order by rank 改为 order by rank, id 即可;