MySQL排序算法

news/2025/11/3 21:19:56/文章来源:https://www.cnblogs.com/ciel717/p/19187252

一、概述

ORDER BY的核心功能,是按照指定的单个或多个字段,对SELECT查询返回的结果集进行升序(ASC,默认)或降序(DESC)排列,以满足业务对数据有序性的需求。但要判断ORDER BY的实际执行效率,最直接的工具是EXPLAIN分析——通过输出结果中“Extra”列的两个关键标识,可快速识别MySQL采用的排序算法,这也是优化排序性能的核心判断依据。

1.1 Using filesort

EXPLAIN的“Extra”列显示Using filesort时,意味着MySQL无法利用索引完成排序,需通过“内存+磁盘”的混合方式对数据进行排序。这里的“文件”并非特指磁盘文件,排序过程分为两种情况:

  • 内存排序:若待排序数据量较小(≤sort_buffer_size参数值),MySQL会直接在排序缓冲区(内存区域)中通过快速排序(QuickSort)算法完成排序,性能损耗较小;
  • 磁盘排序:若数据量超过排序缓冲区容量,MySQL会先将数据分批次排序并写入磁盘临时文件,最后通过归并排序(MergeSort)算法合并所有临时文件,得到有序结果集。此时会产生大量磁盘IO操作,性能损耗显著增加。

简言之,Using filesort通常意味着排序性能较低,尤其是在大数据量场景下,需优先优化以避免性能瓶颈。

1.2 Using Index

当“Extra”列显示Using Index时,代表MySQL通过有序索引直接获取数据,无需额外执行排序操作,这种方式被称为“索引排序”,是效率最高的排序方式。

核心原理是:MySQL的B+树索引本身按“最左前缀原则”有序存储(如主键索引按ID递增排列、联合索引按字段顺序有序存储)。查询时,MySQL只需沿着索引结构遍历,即可得到天然有序的结果集,完全避免了排序计算的开销。

例如:对主键为idusers表执行SELECT id, username FROM users WHERE id < 100,由于主键索引是有序的,返回结果会自动按id升序排列,且EXPLAIN的“Extra”列会显示Using Index

二、常用排序操作

MySQL的排序操作并非仅由ORDER BY子句直接触发,根据排序行为是否由开发者明确指定,可分为“显式排序”和“隐式排序”两类。其中,隐式排序因隐蔽性强,常被开发者忽视,进而导致不必要的性能损耗。

2.1 显式排序

显式排序是指开发者在SQL语句中通过ORDER BY子句明确指定排序字段和排序方向的排序方式,语法直观、场景明确,是最常用的排序形式,主要包括以下三种类型:

2.1.1 单列排序

适用于仅需按一个字段排序的简单场景,例如查询“注册时间最晚的10个用户”:

SELECT id, username, register_time
FROM users
ORDER BY register_time DESC  -- 按注册时间降序
LIMIT 10;  -- 取前10条结果

2.1.2 多列排序

当单一字段无法满足排序需求时,可按多个字段依次排序——先满足第一个字段的排序规则,对第一个字段值相同的记录,再按第二个字段排序,以此类推。

例如:查询商品信息,要求“先按分类ID升序排列,同分类下按价格降序排列”:

SELECT id, category_id, product_name, price
FROM products
ORDER BY category_id ASC,  -- 第一排序字段:分类ID升序price DESC;       -- 第二排序字段:价格降序

关键规则:排序优先级由ORDER BY后的字段顺序决定,前一个字段是“主排序字段”,后一个字段是“次排序字段”,仅当主排序字段值相同时,次排序字段才会生效。

2.1.3 自定义排序

对于“状态”“类型”等非数值、非日期类型的字段,若需按业务自定义的顺序排序(如“待支付→已支付→已发货→已完成”),可通过FIELD()函数或CASE WHEN语句实现。

方式1:使用FIELD()函数

FIELD()函数按参数顺序定义字段值的排序优先级,未匹配的字段值会排在最后:

SELECT id, order_no, status
FROM orders
-- 按“待支付→已支付→已发货→已完成”的顺序排序
ORDER BY FIELD(status, 'pending_payment', 'paid', 'shipped', 'completed');

方式2:使用CASE WHEN语句

当排序逻辑更复杂时(如包含条件判断),CASE WHENFIELD()更灵活:

SELECT id, order_no, status
FROM orders
ORDER BY CASEWHEN status = 'pending_payment' THEN 1  -- 待支付:排序权重1WHEN status = 'paid' THEN 2              -- 已支付:排序权重2WHEN status = 'shipped' THEN 3           -- 已发货:排序权重3WHEN status = 'completed' THEN 4         -- 已完成:排序权重4ELSE 5 END;                              -- 其他状态:排序权重5(最后)

2.2 隐式排序

隐式排序是指开发者未在SQL中编写ORDER BY子句,但MySQL为满足查询语义或语法规则,自动触发的排序操作。需重点关注以下5种场景,避免无意义的性能损耗:

2.2.1 索引扫描

当查询使用覆盖索引(索引包含查询所需的所有字段)或主键索引时,MySQL会直接通过索引的有序结构返回数据,此时虽无ORDER BY,但结果集天然有序。

例如:users表有联合索引idx_ur(username, register_time),执行SELECT username, register_time FROM users WHERE username LIKE 'Zhang%'时,由于索引按username+register_time有序存储,返回结果会自动按register_time升序排列。

2.2.2 旧版GROUP BY的默认排序

MySQL 5.7及之前的版本中,GROUP BY子句会默认对分组后的结果集按“分组字段”升序排序(MySQL8.0+已移除这一默认行为)。

问题:若仅需分组统计(无需排序),默认排序会额外消耗CPU和内存资源。
解决方案:显式添加ORDER BY NULL禁用默认排序,例如:

-- MySQL 5.6/5.7场景:禁用GROUP BY的默认排序
SELECT category_id, COUNT(*) AS total
FROM products
GROUP BY category_id
ORDER BY NULL;  -- 关键:取消默认的升序排序

2.2.3 DISTINCT去重

DISTINCT的核心作用是“去除结果集中的重复记录”,但MySQL实现DISTINCT时,若无合适索引,会先对数据进行排序(以便快速识别重复值),再执行去重操作,此时会触发Using filesort

例如:

SELECT DISTINCT category_id 
FROM products

category_id无索引,EXPLAIN会显示Using filesort(先排序再去重)。

优化方案:为category_id建立单独索引,利用索引的有序性直接去重,避免排序操作。

2.2.4 窗口函数

窗口函数(如ROW_NUMBER()RANK()DENSE_RANK())需基于“窗口框架”内的数据进行计算,若未在OVER()中显式指定ORDER BY,部分窗口函数会默认按“主键”或“窗口分区字段”排序,以保证结果的稳定性。

例如:

SELECT product_name, ROW_NUMBER() OVER (PARTITION BY category_id) AS rn 
FROM products

MySQL会默认按“category_id(分区字段)+主键”排序,为每条记录分配行号。

优化方案:若无需排序,显式指定ORDER BY 1(按第一个字段)或ORDER BY NULL(部分版本支持),减少排序开销。

2.2.5 MAX()/MIN()

MAX()MIN()用于获取字段的最大值和最小值,但在无合适索引的情况下,MySQL会通过排序的方式快速定位极值。

例如:

SELECT MAX(price) 
FROM products 
WHERE category_id = 10

products表未在(category_id, price)上建立索引,MySQL会先筛选出category_id=10的记录,再对price排序,最后取最大值,此时会触发隐式排序。

优化方案:建立(category_id, price)联合索引,直接通过索引定位极值(无需排序)。

三、工作原理

MySQLORDER BY的底层排序逻辑,核心可分为两大分支:利用有序索引获取有序数据(索引排序)和文件排序Using filesort)。其中,文件排序又根据“是否需要回表查询”,细分为“全字段排序”和“Rowid排序”,两种方式的选择由max_length_for_sort_data参数(默认1024字节)与“查询字段总长度”的对比决定。

3.1 索引排序

3.1.1 核心逻辑

利用B+树索引的有序结构,直接遍历索引获取数据,无需额外执行排序计算。

例如:products表有联合索引idx_cp(category_id, price DESC),执行下面SQL语句:

SELECT category_id, price FROM products WHERE category_id = 10 ORDER BY price DESC

MySQL会直接通过索引筛选category_id=10的记录,并按price DESC的顺序返回,无需排序操作,EXPLAIN显示Using Index

3.1.2 适用场景

  • 查询字段需包含在索引中(覆盖索引),或查询条件+排序字段与索引完全匹配(遵循最左前缀原则);
  • 数据量较大(百万级以上),需避免文件排序的磁盘IO开销。

3.2 文件排序

当无法利用索引排序时,MySQL会通过“内存+磁盘”的方式完成排序,根据“是否回表”分为两种方式:

  • 全字段排序
  • Rowid排序
SELECT id, username, register_time, avatar 
FROM users 
WHERE age > 20 ORDER BY register_time DESC

avatar为长字符串,总字段长度超1024字节

3.2.1 全字段排序

核心逻辑:将“查询所需的所有字段”存入排序缓冲区,排序完成后直接返回结果,无需回表查询。
触发条件:查询字段总长度 ≤ max_length_for_sort_data(默认1024字节)。
执行流程: 全字段排序的步骤如下:

  1. 初始化排序缓冲区:MySQL分配sort_buffer_size大小的内存(默认256KB),用于存储idusernameregister_time三个字段的数据;
  2. 读取并筛选数据:根据WHERE age > 20的条件,筛选出符合要求的记录,将三个字段的数据存入排序缓冲区;
  3. 内存排序:若筛选出的记录总数≤缓冲区容量,直接在内存中通过快速排序(QuickSort)按register_time DESC排序;
  4. 磁盘合并(若溢出):若数据量超过缓冲区容量,MySQL会将缓冲区中的数据先排序并写入磁盘临时文件,再继续读取剩余数据填充缓冲区,重复排序-写入过程;最后通过归并排序(MergeSort)合并所有临时文件,得到有序数据集;
  5. 返回结果:直接返回排序后的完整字段数据(无需回表)。

3.2.2 Rowid排序

核心逻辑:仅将“排序字段+主键(Rowid)”存入排序缓冲区,排序完成后,通过主键回表查询所需的其他字段数据。
触发条件:查询字段总长度 > max_length_for_sort_data(例如包含avatar(长字符串)、description(文本字段)等)。
执行流程: Rowid排序的步骤如下:

  1. 初始化排序缓冲区:仅存储“排序字段register_time+主键id”(减少缓冲区占用);
  2. 读取关键数据:根据WHERE age > 20筛选记录,将register_timeid存入缓冲区;
  3. 排序关键数据:按register_time DESC排序(内存/磁盘方式同全字段排序),得到“register_time+id”的有序集合;
  4. 回表查询完整数据:根据排序后的id,到主键索引(InnoDB聚簇索引)中查询usernameavatar字段;
  5. 返回结果:组合所有字段,返回完整的有序结果集。

3.3 对比

排序方式 适用场景 优势 劣势
全字段排序 查询字段总长度≤1024字节 无需回表,减少IO开销 排序缓冲区占用大,易溢出
Rowid 排序 查询字段总长度>1024字节 缓冲区占用小,防溢出 需回表查询,增加IO开销

四、优化策略

ORDER BY优化的核心思路是:优先利用索引排序避免文件排序,若无法避免文件排序,则通过参数调整和SQL优化减少排序开销。以下是针对不同场景的可落地优化方案:

4.1 索引优化

索引是优化排序性能的“最优解”,需根据“查询条件+排序字段”设计联合索引,严格遵循最左前缀原则,让MySQL可直接通过索引获取有序数据。

4.1.1 单条件+单排序字段

若查询有一个WHERE筛选条件,且需按单个字段排序,需建立“筛选字段+排序字段”的联合索引。

例如:查询“年龄>20的用户,按注册时间降序”,需建立索引idx_ar(age, register_time DESC)

-- 优化前:无索引,EXPLAIN显示Using filesort
SELECT id, username FROM users WHERE age > 20 ORDER BY register_time DESC;-- 优化后:有联合索引,EXPLAIN显示Using Index
SELECT id, username FROM users WHERE age > 20 ORDER BY register_time DESC;

原理:索引先按age筛选符合条件的记录,再按register_time DESC有序存储,查询时直接遍历索引即可得到有序结果。

4.1.2 多条件+多排序字段

若查询有多个WHERE筛选条件,且需按多个字段排序,需确保联合索引的“最左前缀”包含所有筛选字段,后续紧跟排序字段(排序方向需与索引一致)。

例如:查询“分类ID=10且库存>0的商品,按价格降序、销量升序”,需建立索引idx_csos(category_id, stock, price DESC, sales ASC)

SELECT id, product_name FROM products
WHERE category_id = 10 AND stock > 0
ORDER BY price DESC, sales ASC;

关键注意点:排序字段的“方向”(ASC/DESC)需与索引完全一致,否则MySQL无法利用索引排序。例如,若索引中priceDESC,而ORDER BYpriceASC,则无法触发Using Index

4.1.3 无筛选条件+排序字段

若查询无WHERE条件,仅按字段排序,需为排序字段建立单独索引(或联合索引的首字段为排序字段)。例如,执行SELECT id, username FROM users ORDER BY register_time DESC,需建立索引idx_rt (register_time DESC)

4.2 参数调整

若无法避免Using filesort(如自定义排序、复杂筛选条件无法建立合适索引),可通过调整以下参数优化文件排序的性能:

  1. sort_buffer_size:排序缓冲区的大小,默认值通常为262144字节(256KB)。若数据量较小,适当增大该参数(如调整为1MB),可让更多数据在内存中完成排序,避免磁盘临时文件的IO开销;但需注意,该参数是会话级别的,若设置过大,会导致并发连接时内存占用过高,反而影响系统性能。建议根据实际数据量调整,避免盲目增大。
  2. max_length_for_sort_data:触发Rowid排序的阈值(默认值通常为1024字节)。若查询字段总长度较小,可适当增大该参数(如调整为2048字节),让MySQL优先选择全字段排序,避免回表IO;若查询字段总长度较大(如包含长字符串),则保持默认值或适当减小,让MySQL选择Rowid排序,减少排序缓冲区的占用。
  3. tmp_table_sizemax_heap_table_size:控制内存临时表的大小,若文件排序过程中需使用临时表(如GROUP BY+ORDER BY),当临时表大小超过这两个参数的最小值时,会转为磁盘临时表(性能下降)。适当增大这两个参数(如均调整为64MB),可减少磁盘临时表的使用。

4.3 特殊场景优化

4.3.1 GROUP BY优化

  • 禁用默认排序:在MySQL 5.7及之前版本,GROUP BY默认排序,若无需排序,需显式添加ORDER BY NULL,避免不必要的排序开销。
  • 利用索引优化:建立“分组字段+排序字段”的联合索引,让MySQL通过索引完成分组和排序。
SELECT category_id, COUNT(*) 
FROM products 
GROUP BY category_id 
ORDER BY COUNT(*) DESC

需建立索引idx_cc (category_id, sales)(若COUNT(*)可通过sales字段间接优化,或直接建立idx_c (category_id)+ORDER BY NULL)。

4.3.2 DISTINCT优化

  • 优先使用索引:为DISTINCT字段建立索引,让MySQL通过索引的有序性快速去重,避免排序。
  • 避免DISTINCT与多字段查询:若DISTINCT需配合多个字段查询(如SELECT DISTINCT category_id, price FROM products),需建立“category_id+price”的联合索引,否则会触发文件排序。
SELECT DISTINCT category_id 
FROM products

建立索引idx_c (category_id)后,可直接通过索引去重,无需Using filesort

4.3.3 窗口函数优化

  • 为窗口框架建立索引:窗口函数的PARTITION BY(分区字段)和ORDER BY(排序字段)需建立联合索引,减少分区和排序的开销。
  • 避免不必要的窗口排序:若窗口函数无需排序(如AVG()按分区计算平均值,无需顺序),可在OVER()中不指定ORDER BY,减少排序操作。
SELECT product_name, price, ROW_NUMBER() OVER (PARTITION BY category_id ORDER BY price DESC) AS rn 
FROM products

需建立索引idx_cp (category_id, price DESC)

五、总结

MySQLORDER BY子句看似简单,但其底层涉及索引排序、全字段排序、Rowid排序等多种复杂逻辑,且隐式排序场景容易被忽视,进而引发性能问题。优化的核心原则可归纳为三点:

  • 优先索引排序:这是最高效的排序方式,需根据“查询条件+排序字段”设计符合最左前缀原则的联合索引,确保查询能直接利用索引的有序性返回数据,避免Using filesort
  • 规避隐式排序:针对旧版GROUP BYDISTINCT、窗口函数等场景,通过显式禁用排序(如ORDER BY NULL)或建立索引,减少无意义的排序开销;
  • 优化文件排序:若无法避免Using filesort,可通过调整sort_buffer_sizemax_length_for_sort_data等参数,平衡内存占用与IO开销,同时精简查询字段(减少排序缓冲区占用)。

在实际开发中,需结合EXPLAIN工具分析排序方式,针对不同场景选择合适的优化方案——大数据量场景优先依赖索引,小数据量场景可灵活调整参数,最终实现排序性能的最优平衡。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/954909.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

CSP-S 2025 饭堂寄

省流:\(100+48+0+0=148\),爆炸。 Day -2 考试前几天竟然发现有些感冒了。 Day -1 考试前一天晚上睡得比较晚,因为回到家都接近 11 点钟了。 Day 1 早上起来已经 9:30 了,起来开始打板子,其实这个时候已经感觉状态…

如何在github上使用github免费域名下预览自己的项目

一、新建自己的工程然后上传自己的工程文件,有首页的话记下首页的路径。 二、点击Settings 点击Pages,填写自己要访问的index.html文件路径

在ROS中安装PX4依赖实现Gazebo仿真

在ROS中安装PX4依赖实现Gazebo仿真最近这几天在做一个无人机项目,在配置gazebo仿真时出现了找不到px4的问题,但是又无法直接安装,需要自行编译 简单做一下记录 sudo apt install ninja-build exiftool ninja-build …

20232314 2024-2025-1 《网络与系统攻防技术》实验四实验报告

一、实验内容 1、恶意代码文件类型标识、脱壳与字符串提取 对提供的rada恶意代码样本,进行文件类型识别,脱壳与字符串提取,以获得rada恶意代码的编写作者,具体操作如下: (1)使用文件格式和类型识别工具,给出ra…

二、驱动基础(基于北京迅为电子)

一、基础Linux驱动的分类:字符设备(顺序访问)、块设备(随机访问)、网络设备(数据包收发) Linux内核源码的目录架构:arch(架构相关)、block(块设备)、crypto(加密算法)、Documentation(官方文档)、driv…

Linux驱动开发学习日记(一)

Linux驱动开发学习日记(一)整完无人机项目之后进行更新,具体怎么写还没想好,现在学的也比较迷糊

Windows 路由表详解

Windows 路由表详解windows 路由表详解 查看ip信息 字段说明IPv4 Address: ipv4地址,用于标识网络中的主机Subnet Mask: 子网掩码,分为 连续的1 和 连续的0 两部分, 可以简写为 /n, 例如 /24,表示高24位为1,剩下为…

微软 Foundry Local - 本地 AI 推理解决方案

微软在其 2025 Build 大会上发布了 Foundry Local,能够在本地设备上执行 AI 推理,意味着可以利用本地的 AI 算力,如:CPU/GPU/NPU;也让用户在隐私方面得到了充足的保障,还能有改善成本效益!Foundry Local 默认除…

如何启用cycloneDDS的iceoryx

共享内存交换 — Eclipse Cyclone DDS,0.11.0 首先我们需要先下载安装iceoryx,因为cycloneDDS如果要使用共享内存传输是依赖于这个插件的。顺带一提,只有同一节点的不同进程间会使用到共享内存,cycloneDDS是根据如…

老化车

老化车非常好 👍,这个问题在电子制造和测试领域里很关键。 “老化车”(又叫 Burn-in Cart 或 Aging Rack)是用于电子产品在出厂前做 老化测试(Burn-in Test) 的一种设备或平台。 下面我给你系统讲清楚 👇🧭…

Android Studio 2025.2.1 汉化中文包临时解决方案

打开 JetBrains 官网 Chinese ​(Simplified)​ Language Pack / 中文语言包 下载最新版 242.152 版本插件将 zh.242.152.jar 文件解压出来用压缩包工具打开 zh.242.152.jar 找到目录 META-INF 并打开用文本编辑工具打…

Markdown 学习训练

Markdown 学习训练 引用(使用>)这是我的第一篇博客,本篇博客是参照狂神说课程进行学习,目的是为了练习markdown使用语法。主要包含各级标题、字体、图片、代码块、超链接、表格使用语法练习。具体可前往typora官网…

jmeter设置中文页面的两种方法

JMeter设置中文界面有两种方法:临时设置(仅当前会话有效)和永久设置(修改配 置文件后永久生效)。 设置方法 1.临时设置(关闭后恢复英文): 打开JMeter,点击菜单栏“Options”→“Choose Language”→选择“Chi…

win10 下运行aoe2,报错,应用程序无法正常启动 0xc000022

控制面板 →搜索“启用或关闭 Windows 功能”找到并勾选 “旧版组件” → “DirectPlay” → 确定 → 重启电脑。

Python生成器表达式详解(含与列表推导式核心对比、别名探讨)

从“囤货”到“现做”:Python生成器表达式详解(含与列表推导式核心对比) 在Python中,处理数据时经常会遇到一个矛盾:既要简洁的语法,又要避免大量数据占用内存。列表推导式虽能简化代码,却会“一次性生成所有元…

在Fiddler中模拟网络中断,返回500错误的过程

开启断点在 Fiddler 菜单栏点击 Rules → Automatic Breakpoints,选择以下任一断点模式: Before Requests(请求发送前断点):可修改请求后再发送,适合模拟服务器因异常请求返回 500。 After Responses(响应返回后…

P4198 楼房重建 分析

题目概述 题目链接:https://www.luogu.com.cn/problem/P4198。 给出一个 \(x\) 轴长度为 \(n\),\(y\) 轴长度为 \(10^9\) 的二维平面。 一共有 \(n\) 天,第 \(i\) 天令坐标为 \(x_i\) 的线段变长为 \(y_i\)(屋顶就…

构建企业级AI提示词攻击防御体系的实战指南-2025年

构建企业级AI提示词攻击防御体系的实战指南-2025年在人工智能技术深度赋能的2025年,大型语言模型已全面渗透金融、政务、医疗等企业核心业务场景。与此同时,提示词攻击正以惊人的速度演进为企业AI安全的头号威胁。研…

矩阵的秩

设运输问题的约束矩阵为: \[A = \begin{bmatrix} 1 & 1 & 1 & 0 & 0 & 0 \\[6pt] 0 & 0 & 0 & 1 & 1 & 1 \\[6pt] 1 & 0 & 0 & 1 & 0 & 0 \\[6pt] 0 &a…

Python列表推导式完全指南

从循环到一行代码:Python列表推导式完全指南 在Python中,列表推导式(List Comprehension)是一种简洁、高效的创建列表的语法。它能将原本需要多行循环+条件判断的代码,浓缩成一行可读性强的表达式。但对新手来说,…