MySQL实践——分析MySQL中使用order by和limit每次返回结果不同的问题及解决办法

作为一名开发者,我们经常会遇到各种数据库问题。其中,MySQL排序结果不一致问题是一个比较常见的问题。当我们在使用MySQL进行排序时,有时候会发现相同的查询多次执行,但排序结果却不一致。这个问题可能会给我们的业务带来困扰和不确定性。

首先我们来还原下现象,然后我们在分析下原因,找到解决办法。

一、环境准备

  1. 创建表
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 上面是没有索引的。

  1. 初始化数据
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);

二 、复现现象

  1. 执行两个 根据非索引字段且有重复值的 order by 排序
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 的结果集

  1. 为a字段加上索引
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 即可;

你可能感兴趣的:(mysql,数据库,堆排序,Oder,by,LIMIT)