李锋镝的博客

  • 首页
  • 时间轴
  • 说说
  • 读书观影
  • 系列
  • AI工具集
  • 论坛
  • 友圈
    • 友链
    • 朋友圈
    • 游客中心
  • 留言
    • 留言
    • 走心评论
  • 关于
    • 关于本站
    • Now
    • 每日心情
    • 网站地图
    • 网站统计
    • 赞助
  • 星球穿梭
  • 🚇开往
静亦定 动亦定
惟坚韧者始能遂其志
  1. 首页
  2. 转载
  3. 技术
  4. 正文

MySQL分页排序时数据重复问题(MySQL优先队列)

2021年5月29日 248 2 0
本文最后更新于 2021年5月29日,距今已 1995 天,其中的信息可能已经发生变化,请注意甄别。

背景

MySQL版本:5.7.18

问题

假设字段category无索引且有重复值,order by category 和limit组合使用的结果会和预期不符。

场景复现

表结构(复现问题,两个字段足够了~)

CREATE TABLE `ratings` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `category` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=11 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;

对所有数据按category字段排序: select * from ratings order by category;

id category
1 1
5 1
10 1
3 2
4 2
6 2
9 2
2 3
7 3
8 3

当我们想分页展示前5条时使用select * from ratings order by category limit 5;

期望得到的ID顺序是1 5 10 3 4。

但实际结果如下:

id category
1 1
10 1
5 1
3 2
4 2

可能有同学遇到过这个问题,百度或谷歌一下解决了,你有没有想过,你查到的办法是最优解吗?别人是怎么得出这个办法的?MySQL 为什么会这样做,跟版本有关吗?

先抛结论:

  • 最优解是后面再加个列值唯一的排序字段,如:order by category,id
  • MySQL 为什么这样做?答案是为了快!(MySQL 5.6及其之后才有此优化)
  • 次优解是对order by后面的category 加索引(为什么是次优解?看完本文你将会有答案)

寻找最优解

MySQL 文档 8.2.1.19 LIMIT Query Optimization 中对此场景有如下描述:

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.

One factor that affects the execution plan is LIMIT, so an ORDER BY query with and without LIMIT may return rows in different orders.

总结来说就是:当 ORDER BY 列的字段值存在重复,那么这条 ORDER BY 语句返回的数据顺序会因为LIMIT的存在而变得不一样。

这是 MySQL 默认对该场景做的优化,如果你需要保证加不加 LIMIT 顺序都要一致,官方也给出了办法:

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.

就是在ORDER BY后面再多加一个排序字段(比如 ID 字段)。

以上描述最早出现在MySQL 5.6文档中,从这个版本开始,引入了这个针对ORDER BY LIMIT的优化。

好了, 针对文中的场景,我们只需要select * from ratings order by category,id;即可解决。

那么问题来了,MySQL 为什么要做这么一个看似是 Bug 的优化?

MySQL 的 ORDER BY 逻辑

ORDER BY 就是排序。

执行一下explain select * from ratings order by category limit 5;

*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: ratings
   partitions: NULL
         type: ALL
possible_keys: NULL
          key: NULL
      key_len: NULL
          ref: NULL
         rows: 10
     filtered: 100.00
        Extra: Using filesort
1 row in set, 1 warning (0.00 sec)

可以看到 Extra: Using filesort 表示需要排序。

正常情况下, MySQL 会有内存排序和外部排序两种:

如果待排序的数据量小于sort buffer size,排序就在内存中完成(快速排序)

如果待排序的数据量大于sort buffer size,就使用临时文件进行外部排序(归并排序)

很明显,这两种排序都是对所有结果全部排序,讲道理,不管有没有LIMIT,都是从排完序的结果中按顺序取需要的条数,有没有LIMIT是不会影响返回的结果顺序的。

但是,MySQL 5.6 版本针对 ORDER BY LIMIT做了个小优化(排序字段无索引,且列值不唯一时):优化器在遇到 ORDER BY LIMIT语句的时候,使用了priority queue。

filesort.cc 中有如下伪代码描述该优化:

while (get_next_sortkey())
   {
     if (using priority queue)
       push sort key into queue
     else
     {
       try to put sort key into buffer;
       if (no free space in sort buffer)
       {
         do {
           allocate new, larger buffer;
           retry putting sort key into buffer;
         } until (record fits or no space for new buffer)
         if (no space for new buffer)
         {
           sort record pointers (all buffers);
           dump sorted sequence to 'tempfile';
           dump Merge_chunk describing sequence location into 'chunk_file';
         }
       }
       if (key was packed)
         tell sort buffer the actual number of bytes used;
     }
   }
   if (buffer has some elements && dumped at least once)
     sort-dump-dump as above;
   else
     don't sort, leave sort buffer to be sorted by caller.

并在 WL#1393: Optimizing filesort with small limit 中阐述了该优化逻辑:

Many web customers have to do
"SELECT ... ORDER BY non_index_column LIMIT X",

When X *  is smaller than sort_buff_size we can use
the following algoritm to speed up the sort:

- Create a queue to hold 'limit' keys.
- Scan through the table and store the first (last if DESC) keys in the queue
- Return values from queue

This is much faster than the current algoritm that works as:

该 WorkLog 中记录了优化后的效果:10 to 20 times faster than a quicksort(感兴趣的同学可以去阅读原文)。

所以,就是为了快!

MySQL 认为这种场景就是求 TOP N 的问题,使用 priority queue 就能解决。

priority queue(优先级队列)

priority queue 其实就是堆,Java 中有java.util.PriorityQueue类,其本质就是 堆 这种数据结构。

简单解释一下什么是堆:

堆是一个完全二叉树;

堆中每一个节点的值都必须大于等于(大顶堆)或小于等于(小顶堆)其子树中每个节点的值。

如果 MySQL 使用归并或快排,需要把所有数据都排好序,再取LIMIT 的前几条,剩余已排序的数据就白白浪费了。

而采用 priority queue 可以根据 LIMIT的条数维护一个堆,只需要把所有数据在这个堆里过一遍就能得到结果。

使用如下语句可以验证 MySQL 使用了 priority queue:

SET optimizer_trace='enabled=on';
select * from ratings order by category limit 5;
SELECT * FROM `information_schema`.`OPTIMIZER_TRACE`\G;
 "filesort_priority_queue_optimization": {
              "limit": 5,
              "chosen": true
            },

可以看到 filesort_priority_queue_optimization.chosen = true

下面用流程图还原一下 priority queue 的执行逻辑(以LIMIT 5为例):

友情提示:图中的小顶堆以 category 值的大小排序

  1. 取前五条数据构成一个小顶堆:

  2. 取下一行数据(6,2),发现 2 小于当前堆中最大的category 3,于是把(2,3)从堆中删掉,把(6,2) 入堆:

  3. 重复步骤 2,直至符合查询条件的数据都经历过比较入堆,最终堆中数据如图:

以上就是通过 priority queue 找到 最小的 5 行 category 数据的执行过程。

最后我们将其出堆即可得到结果,每次出堆最小元素后将最后一个元素放入堆顶,按照小顶堆重新堆化,过程如图:

可以看到,这个结果和select * from ratings order by category limit 5;的输出一致

加索引为什么是次优解

显然,按照ORDER BY的逻辑,直接对排序字段加索引也可以省去内存排序步骤,从而解决这个问题。

但索引也不是银弹,多出来的category索引会增加表的维护成本,如果没有明显的业务需要,单纯为了绕过这个priority queue的优化而加索引,有点得不偿失。

尤其是当表数据量非常大的时候,索引的体量会很可观。而且,针对文中场景,category作为分类字段,重复率会比较高,即使有按分类查询的业务 SQL ,MySQL也不一定会选取这条索引。

综上,针对本场景,个人认为order by category,id才是该问题的最优解。

参考链接

2
  1. 18.2.1.19 LIMIT Query Optimizationdev.mysql.com
  2. 2WL#1393: Optimizing filesort with small limitdev.mysql.com
本作品采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可
分享到

MySQL分页排序时数据重复问题(MySQL优先队列)

也可使用浏览器菜单中的「分享」功能

微信扫一扫分享

标签: MySQL priority queue
最后更新:2021年5月29日

岁月同一天 11 月 14 日

回望过去的今天,你在写什么

  • 后端
    1 年前 2025年11月14日
    JWT、Cookie、Session、Token 区别与实战选型指南

    在 Web 开发中,身份认证与状态管理是核心基础功能——用户登录、权限校验、会话保持等场景都离不开它们。JWT、Cook…

  • AI
    1 年前 2025年11月14日
    Ollama:重新定义本地大模型运行的开源革命

    当大语言模型(LLM)的浪潮从云端席卷而来时,一个矛盾逐渐凸显:一方面,企业和个人对AI的需求从通用对话转向隐私敏感的场…

  • 文学
    7 年前 2019年11月14日
    桃花庵歌

    桃花庵歌 【作者】唐寅 【朝代】明 桃花坞里桃花庵,桃花庵下桃花仙;桃花仙人种桃树,又摘桃花卖酒钱。 酒醒只在花前坐,酒…

相关文章
  • 高性能场景为什么推荐使用PostgreSQL,而非MySQL?2025年10月11日
  • MySQL语句执行顺序2020年7月24日
  • 分布式服务生成唯一不重复ID(24位字符串)2019年7月13日
  • org.apache.ibatis.plugin.Interceptor类详细介绍及使用2026年2月4日
  • MySQL主键索引和普通索引的区别2019年12月22日

李锋镝

既然选择了远方,便只顾风雨兼程。

打赏 点赞
< 上一篇
下一篇 >
1234567891112131415161718192021222324252627282930313233343536373839404142434446474849505152535455575859606162636465666769727476777879808182858687909293949596979899
取消回复
…

文章评论

还没有评论,快来抢沙发吧~

九年没联系的女友突然问我借钱,我开口道:四十万行么?不够了再说话。”前女友很是高兴地回复说:“够用,还是我男朋友有办法啊,你放心,我日后一定还给你,九年多没见了你现在干啥呢,这么有钱?”我呵呵一笑:“没事,不够用您尽管说话,俺现在改做抵押贷款了。”

听点儿音乐吧 朋友~
文章目录
最新 热点 随机
最新 热点 随机
给服务器加了WAF 又一个逆天改命的人:小小徐 AI的生成能力等于生产力吗? NGINX严重漏洞(CVE-2026-15176、CVE-2026-93479、CVE-2026-97205) Kratos-plus v1.1.27更新说明 php2shell远程执行漏洞修复(CVE-2026-109618 和 CVE-2026-109619)
给主题增加了游客中心功能心气没了,干什么都不得劲给服务器加了WAF我觉得是时候公布我的导航网站了游本昌去世关于主题加载速度优化的一点儿小演进
讨论下XXL-JOB GLUE模式的安全问题 又一个逆天改命的人:小小徐 Kafka 为什么要抛弃 Zookeeper? 余秋雨散文集精选 一篇文章帮你彻底搞清楚“I/O多路复用”和“异步I/O”的前世今生 6款核心MCP协议工具让AI深度融入业务,告别“纸上谈兵”
最近评论
李锋镝 发布于 5 天前(11月09日) 我也刷到了,说是恶意引流的
皮皮社长 发布于 5 天前(11月09日) 我刷到的,怎么是说剧本骗流量的?如果真的是用来骗流量,那真恶心。欺骗网友感情。
李锋镝 发布于 5 天前(11月09日) 现在我这个是十个,其实也够用了
老张博客 发布于 5 天前(11月09日) 雷池唯一不好的就是只能免费5个网站。
fairy 发布于 1 周前(11月06日) 机器人快来我这里吧!
标签聚合
架构 WordPress SpringBoot MySQL JVM Redis 日常 Spring AI编程 IDEA SQL Claude Theme AI JAVA 数据库 多线程 分布式 ElasticSearch K8s
友情链接
  • 老张博客
  • 韩情脉脉
  • 瓦匠个人小站
  • 刘永博客
  • 彬红茶日记
  • 韩小韩博客
  • Mr.Sun的博客
  • 九仞之行
  • Serendipity
  • 知向前端
  • 皮皮社
  • 志文工作室
  • Honesty
  • lijie blog
  • 搬砖日记
  • 懋和道人
  • 哥斯拉
  • 林羽凡
  • sssr7844的博客

COPYRIGHT © 2016-2026 lifengdi.com. ALL RIGHTS RESERVED.

lifengdi.com 十年之约虫洞

Domain age badge for lifengdi.com

Theme Kratos-plus By Dylan Li

津ICP备2024022503号-3

京公网安备11011502039375号