MySQL 中 order by 与 limit 混用,分页结果与预期不符

  在 MySQL 中我们常常用 order by 来进行排序,使用 limit 来进行分页,当需要先排序后分页时我们往往使用类似的写法 “select * from 表名 order by 排序字段 limit M,N”。但是这种写法却隐藏着较深的使用陷阱。在排序字段有数据重复的情况下,会很容易出现排序结果与预期不一致的问题。

一、异常现象

  比如在 5.6.17 版本的 MySQL 数据库中,有一张 tbl_mgm_tour 表,表结构如下:

mysql> show full columns from tbl_mgm_tour;
+---------+--------------+-----------------+------+-----+---------+-------+---------------------------------+--------------+
| Field   | Type         | Collation       | Null | Key | Default | Extra | Privileges                      | Comment      |
+---------+--------------+-----------------+------+-----+---------+-------+---------------------------------+--------------+
| tour_id | char(15)     | utf8_general_ci | NO   | PRI |         |       | select,insert,update,references | 景区编号     |
| name    | varchar(100) | utf8_general_ci | NO   |     |         |       | select,insert,update,references | 景区名称     |
| grade   | varchar(10)  | utf8_general_ci | NO   |     |         |       | select,insert,update,references | 景区等级     |
+---------+--------------+-----------------+------+-----+---------+-------+---------------------------------+--------------+
3 rows in set (0.03 sec)

  表数据如下:

mysql> select * from tbl_mgm_tour;
+---------+----------------------------------------------+-------+
| tour_id | name                                         | grade |
+---------+----------------------------------------------+-------+
| 001     | 东方明珠广播电视塔                           | 5A    |
| 002     | 上海野生动物园                               | 5A    |
| 003     | 上海科技馆                                   | 5A    |
| 005     | 上海博物馆                                   | 4A    |
| 006     | 上海佘山国家森林公园·东佘山园                | 4A    |
| 007     | 上海佘山国家森林公园·西佘山园                | 4A    |
| 008     | 上海豫园                                     | 4A    |
| 009     | 金茂大厦88层观光厅                           | 4A    |
| 056     | 上海南汇桃花村                               | 3A    |
| 057     | 大宁郁金香公园                               | 3A    |
| 058     | 东方假日田园                                 | 3A    |
| 059     | 廊下生态园                                   | 3A    |
| 060     | 中国农民画村                                 | 3A    |
+---------+----------------------------------------------+-------+
13 rows in set (0.00 sec)

  现在想根据景区等级降序查询 tbl_mgm_tour 表,并且分页查询,每页 5 条,那很容易写出 sql 语句为:

SELECT * FROM tbl_mgm_tour ORDER BY grade DESC LIMIT 0, 5;

  在执行查询过程中会发现,查询第一页数据时,结果为:

mysql> SELECT * FROM tbl_mgm_tour ORDER BY grade DESC LIMIT 0, 5;
+---------+----------------------------------------------+-------+
| tour_id | name                                         | grade |
+---------+----------------------------------------------+-------+
| 001     | 东方明珠广播电视塔                           | 5A    |
| 002     | 上海野生动物园                               | 5A    |
| 003     | 上海科技馆                                   | 5A    |
| 006     | 上海佘山国家森林公园·东佘山园                | 4A    |
| 007     | 上海佘山国家森林公园·西佘山园                | 4A    |
+---------+----------------------------------------------+-------+
5 rows in set (0.00 sec)

  查询第二页数据时,结果为:

mysql> SELECT * FROM tbl_mgm_tour ORDER BY grade DESC LIMIT 5, 5;
+---------+----------------------------------------------+-------+
| tour_id | name                                         | grade |
+---------+----------------------------------------------+-------+
| 007     | 上海佘山国家森林公园·西佘山园                | 4A    |
| 006     | 上海佘山国家森林公园·东佘山园                | 4A    |
| 005     | 上海博物馆                                   | 4A    |
| 060     | 中国农民画村                                 | 3A    |
| 057     | 大宁郁金香公园                               | 3A    |
+---------+----------------------------------------------+-------+
5 rows in set (0.00 sec)

   tbl_mgm_tour 表共有 13 条数据,有 3 页数据,但是实际查询过程中,第一页与第二页竟然出现了相同的数据。

二、异常现象分析

   这是什么情况?难道上面的分页 SQL 不是先将表数据排好序,再取对应分页的数据吗?

   上面的实际执行结果已经证明,现实与想像往往是有差距的。实际 SQL 执行时并不是按照上述方式执行的。这里其实是 MySQL 会对 Limit 做优化,具体优化方式见官方文档:https://dev.mysql.com/doc/refman/5.7/en/limit-optimization.html(这个是 5.7 版本的说明),提取几个问题直接相关的点做下说明。

  • If you combine LIMIT row_count with ORDER BY, MySQL stops sorting as soon as it has found the first row_count rows of the sorted result, rather than sorting the entire result. If ordering is done by using an index, this is very fast. If a filesort must be done, all rows that match the query without the LIMIT clause are selected, and most or all of them are sorted, before the first row_count are found. After the initial rows have been found, MySQL does not sort any remainder of the result set.

    One manifestation of this behavior is that an ORDER BY query with and without LIMIT may return rows in different order, as described later in this section.

  上面官方文档里面有提到如果将 Limit rowcount 与 order by 混用,MySQL 会找到排序的 rowcount 行后立马返回,而不是排序整个查询结果再返回。如果是通过索引排序,会非常快;如果是文件排序,所有匹配查询的行(不带 Limit 的)都会被选中,被选中的大多数或者全部会被排序,直到 limit 要求的 rowcount 被找到了。如果 limit 要求的 rowcount 行一旦被找到,MySQL 就不会排序结果集中剩余的行了。

  这里我们查看下对应 SQL 的执行计划:

mysql> EXPLAIN SELECT * FROM tbl_mgm_tour ORDER BY grade DESC LIMIT 0, 5;
+----+-------------+--------------+------+---------------+------+---------+------+------+----------------+
| id | select_type | table        | type | possible_keys | key  | key_len | ref  | rows | Extra          |
+----+-------------+--------------+------+---------------+------+---------+------+------+----------------+
|  1 | SIMPLE      | tbl_mgm_tour | ALL  | NULL          | NULL | NULL    | NULL |   13 | Using filesort |
+----+-------------+--------------+------+---------------+------+---------+------+------+----------------+
1 row in set (0.00 sec)

  可以确认是用的文件排序,表确实也没有加额外的索引。所以我们可以确定这个 SQL 执行时是会找到 limit 要求的行后立马返回查询结果的。

  不过就算它立马返回,为什么分页会不准呢?官方文档里面做了如下说明:

If multiple rows have identical values in the ORDER BY columns, the server is free to return those rows in any order, and may do so differently depending on the overall execution plan. In other words, the sort order of those rows is nondeterministic with respect to the nonordered columns.

  如果 order by 的字段有多个行都有相同的值,MySQL 是会以随机的顺序返回查询结果的,具体依赖对应的执行计划。也就是说如果排序的列是无序的,那么排序的结果行的顺序也是不确定的。

  基于这个我们就基本知道为什么分页会不准了,因为我们排序的字段是 grade,正好又有几行数据有相同的 grade 值,在实际执行时,返回结果对应的行的顺序是不确定的。对应上面的情况,第一页返回的 name 为 “上海佘山国家森林公园·东佘山园” 和 “上海佘山国家森林公园·西佘山园” 的数据,可能正好排在前面,而第二页查询时,上述两条数据行正好排在后面,所以第二页又出现了。

三、解决方案

  那这种情况应该怎么解决呢?官方给出了解决方案:

If it is important to ensure the same row order with and without LIMIT, include additional columns in the ORDER BY clause to make the order deterministic. For example, if id values are unique, you can make rows for a given category value appear in id order by sorting like this:

  如果想在 Limit 存在或不存在的情况下,都保证排序结果相同,可以额外加一个排序条件。例如 id 字段是唯一的,可以考虑在排序字段中额外加个 id 排序去确保顺序稳定。

  所以上面的情况下,可以在 SQL 再添加个排序字段,比如 tbl_mgm_tour 表的主键 tour_id 字段,这样分页的问题就解决了。修改后的 SQL 如下:

mysql> SELECT * FROM tbl_mgm_tour ORDER BY grade DESC, tour_id LIMIT 0, 5;

  再次测试问题解决!

四、补充说明

  相同的数据在不同的数据库版本中,排序结果可能正常也可能不正常,上述测试的数据库版本是 5.6.17,在 5.7.29 版本的数据库中测试时,排序结果是正常的。

参考文章:

猜你喜欢

转载自blog.csdn.net/piaoranyuji/article/details/113883210