MySQL45讲 第十六讲 “order by”是怎么工作的?

文章目录

MySQL45讲 第十六讲 "order by"是怎么工作的?

一、引言

在应用开发中,经常需要根据指定字段排序显示结果。本文以查询城市为 "杭州" 的市民信息并按姓名排序为例,深入探讨 MySQL 中 "order by" 语句的执行流程、不同算法以及相关优化策略,避免在开发中出现性能问题。

例子:假设你要查询城市是"杭州"的所有人名字,并且按照姓名排序返回 前1000个人的姓名、年龄。

假设这个表的部分定义是这样的:

sql 复制代码
CREATE TABLE `t` (
`id` int(11) NOT NULL,
`city` varchar(16) NOT NULL,
`name` varchar(16) NOT NULL,
`age` int(11) NOT NULL,
`addr` varchar(128) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `city` (`city`)
) ENGINE=InnoDB;

这时,你的SQL语句可以这么写:

sql 复制代码
select city,name,age from t where city='杭州' order by name limit 1000;

二、全字段排序

(一)索引创建与执行情况分析

  1. MySQL会给每个线程分配一块内存用于排序,这块内存称为sort_buffer。为避免全表扫描,需在 city 字段创建索引。

  2. 使用 explain 命令查看执行情况,Extra 字段中的 Using filesort 表示需要排序,MySQL 会为每个线程分配 sort_buffer 内存用于排序。

(二)执行流程

  1. 初始化 sort_buffer,确定放入 name、city、age 三个字段。
  2. 从 city 索引找到满足条件的第一个主键 id。
  3. 到主键 id 索引取出整行,取相关字段值存入 sort_buffer。
  4. 从 city 索引取下一个记录的主键 id,重复 3、4 步直到不满足条件。
  5. 对 sort_buffer 中的数据按 name 字段做快速排序(可能在内存或使用外部排序,取决于 sort_buffer_size 参数和排序数据量)。
  6. 取前 1000 行返回给客户端。

(三)查看是否使用临时文件

  1. 通过设置 optimizer_trace 为 enabled=on,计算执行语句前后 performance_schema.session_status 中 Innodb_rows_read 的差值,并查看 OPTIMIZER_TRACE 结果中的 number_of_tmp_files,可确定是否使用临时文件。若该值大于 0,表示使用了外部排序,MySQL 将数据分成多份排序后合并;若为0,表示可在内存中完成排序。

  2. 下图中的number_of_tmp_files=12代表MySQL将需要排序的数据分成12份,每一份单独排序后存在这些临时文件中。然后把这12个有序文件再合并成一个有序的大文件。


三、rowid 排序

在全字段排序算法 过程里面,只对原表的数据读了一遍,剩下的操作都是在sort_buffer和临时文件中执行的。如果查询要返回的字段很多的话,那么sort_buffer里面要放的字段数太多,即行长度过长,这样内存里能够同时放下的行数很少,要分成很多个临时文件,排序的性能会很差。这时候就需要使用rowid排序。

(一)参数控制与算法改变

SET max_length_for_sort_data = 16;

  1. 当 max_length_for_sort_data 参数设置为较小值(如 16),且单行长度超过该值时,MySQL 采用 rowid排序算法。此算法放入 sort_buffer 的字段只有要排序的列 (如 "name")和主键 id。

(二)执行流程

  1. 初始化 sort_buffer,确定放入 name 和 id 字段。

  2. 从 city 索引找到满足条件的第一个主键 id。

  3. 到主键 id 索引取出整行,取 name 和 id 字段存入 sort_buffer。

  4. 从 city 索引取下一个记录的主键 id,重复 3、4 步直到不满足条件。

  5. 对 sort_buffer中的数据按 name 排序。

  6. 遍历排序结果取前 1000 行,按 id 值回原表取出 city、name 和 age 字段返回给客户端。此算法多了一次回表操作,但在单行数据较大时,可在排序过程中一次排序更多行。

(三)全字段排序和rowid 排序性能对比

  1. 全字段排序 在内存足够时优先选择,可直接从内存返回结果,减少磁盘访问;
  2. rowid 排序在内存较小时使用,虽排序时能处理更多行,但需回表取数据,增加磁盘读操作。

四、利用联合索引避免排序

(一)创建联合索引

  1. 创建 city 和 name 的联合索引(如 city_user (city, name)),可确保从该索引取出行时按 name 递增排序,无需再进行排序操作。

  2. 无需继续创建临时表和排序 ,使用explain指令查看,Extra字段已经没有Using filesort,证明

(二)执行流程简化

  1. 从联合索引找到满足条件的第一个主键 id。

  2. 到主键 id 索引取整行相关字段值直接返回。

  3. 从联合索引取下一个记录主键 id,重复 2 步直到满足条件结束。

(三)覆盖索引优化

  1. 进一步创建 city、name 和 age 的联合索引(如 city_user_age (city, name, age)),可利用覆盖索引,直接从索引获取数据返回,无需回表从主键索引取数据 ,性能更快,但需权衡索引维护代价。覆盖索引是指,索引上的信息足够满足查询请求,不需要回到主键索引上去取数据。

  2. 这样整个查询语句的执行流程就变成了:

    • 从索引(city,name,age)找到第一个满足city='杭州'条件的记录,取出其中的city、name和age 这三个字段的值,作为结果集的一部分直接返回;
    • 从索引(city,name,age)取下一个记录,同样取出这三个字段的值,作为结果集的一部分直接返回;
    • 重复执行步骤2,直到查到第1000条记录,或者是不满足city='杭州'条件时循环结束。

五、总结与思考

MySQL 中 order by 语句有多种执行算法,开发人员应清楚其排序逻辑和系统资源消耗,根据实际情况选择合适方案。

  • 全字段排序可能需要使用临时表进行排序,在字段过多的情况下性能可能会很差。
  • 为了减少字段过长导致的排序性能下降,rowid排序 算法放入 sort_buffer 的字段只有要排序的列 (如 "name")和主键 id。
  • 如果需要进一步提高性能,可以采取联合索引乃至覆盖索引(字段已排序),这样就可以避免排序,但是需要消耗空间存储索引和维护索引为代价。
相关推荐
袋鼠云数栈36 分钟前
让 SQL 解析不再重复造轮子:SqlParserX 正式开源
数据库·人工智能·sql·开源·数据治理
这个DBA有点耶1 小时前
期货 CTP 系统 Oracle RAC 迁移国产库实战:读写分离架构设计与性能对标
数据库·架构·dba
数据库安全1 小时前
AI数据访问安全 |如何控制AI访问权限,防止越权
数据库·安全
小羊没烦恼!2 小时前
异步编程:线程概述及使用
java·开发语言·mysql·算法·c#
这个DBA有点耶2 小时前
Redo Log循环写与checkpoint推进:长事务阻塞下的Checkpoint风暴完整链路
数据库·mysql·代码规范
jnrjian2 小时前
pg_waldump --key-file-name status 查看wal 产生的db
数据库
SelectDB技术团队3 小时前
Snowflake 的开源替代怎么选:能力对照、三条部署路径与迁移执行
大数据·数据库·云原生·开源·snowflake·apache doris
Elastic 中国社区官方博客3 小时前
告别繁琐控制台:Kabanos 如何将 MongoDB Compass 的易用性带入 Elasticsearch
大数据·数据库·mongodb·elasticsearch·搜索引擎·全文检索
京东云开发者3 小时前
当我谈数据库高可用时我谈些什么
数据库
梦影_4 小时前
MySQL进阶教程(一)—— 储存过程与触发器(上)
开发语言·数据库·后端·sql·mysql·中间件