问题背景

在开发 Web 应用或处理数据库查询时,分页是一项常见需求。然而,当面对深度分页(即页码较大,偏移量较高的分页情况)时,性能问题往往接踵而至。比如对一些需要拉特定的页面查询、范围导出、范围计算等业务需求,都会涉及大量的深分页查询的SQL,不当的SQL会导致执行超时,页面响应显著上升等问题。本文重点讨论Mysql下以Innodb为存储引擎时,深分页造成性能恶化的根因以及一般解决方案。

为什么Limit在offset很大时性能会变差?

假设当前存在一张item表,单表有千万级数据,其核心字段有id, item_code, item_name, type, sub_type, biz_type, status等,并且在type, sub_type, biz_type三个字段上存在索引, id上存在默认的主键索引

当执行SQL

-- SQL1  -> 3ms; 如果把候选列设置为* -> 4ms
Select id, item_code, type, sub_type
From item
where type = 'normal'
order by id ASC
limit 0, 20; -- SQL2 2300ms; 如果把候选列设置为* -> 3700ms
Select id, item_code, type, sub_type
From item
where type = 'normal'
order by id ASC
limit 1000000, 20; -- SQL3 500ms; 只查找主键和覆盖索引上的东西
Select id, type
From item
where type = 'normal'
order by id ASC
limit 1000000, 20;


不必要的回表

我们都知道innodb采用B+树作为索引(如上图),在普通索引的条件下,非叶子阶段上存储了索引列值的比较锚点,叶子节点上存储索引列值到主键id的pair。在普通索引上查询到叶子节点并获取对应的主键后,若SQL中需要获取索引列以外的值,则需要到主键索引上,利用先前定位到的主键进行回表操作,获取完整的数据。

SQL2和SQL3的对比正好是是否需要触发回表的对比,可以发现执行时间差了4倍以上。通过执行两者的执行计划,也可以发现,在SQL3的extra信息中,会有Using Index的信息,这代表命中了覆盖索引,不需要额外进行回表。

同时对比SQL1和SQL2,两者只有在offset的部分存在区别,所以当offset变大时,会导致回表后需要扫描记录数显著增加(这也说明limit的执行逻辑是在回表之后,但是where条件执行是会在索引或者回表时就应用的,那么有办法把limit的操作前置到where中呢?)

延迟关联

-- 500ms
Select id, item_code, type, sub_type
From item
INNER JOIN (
Select id
From item
where type = 'normal'
order by id ASC
limit 1000000, 20
) t1 using (id);

子查询

-- 500ms
Select id, item_code, type, sub_type
From item
where id >= (
Select id
From item
where type = 'normal'
order by id ASC
limit 1000000, 1
) limit 20;

两种方式都非常类似,都是通过避免回表,拿到数据的主键id,再通过join 或者id直接比较的方式,跳过了无意义的回表扫描。(相当于通过人为的方式将limit操作前置到回表之前了)

禁止跳页

还有一种简单的方式就是,需要客户端传入上一次调用的last_id,然后在where条件里加上last_id的条件即可。但是这种做法使得客户端无法进行跳页访问了,只能连续的进行上一页或者下一页操作。(批处理或者导出的场景还是非常适用的)

关于OrderBy的影响

上述的SQL当order by 条件发生变化时,SQL的执行效率也会发生巨大的变化,甚至比limit本身影响更大。因为order By会决定mysql优化器的索引选择,以及会触发FileSort(即在内存中开辟专属空间进行排序)。

-- SQL5 9000 ms
Select id, item_code, type, sub_type
From item
where type = 'normal'
order by item_code ASC
-- 此处基于item_code排序
limit 0, 20;

全字段排序

可以看到SQL5仅仅是换了一个排序条件,并且查询计划显示命中的索引仍与先前一致,但是执行时间却来到了夸张的9000ms,相比SQL1,多了近20倍,为什么会产生这样的结果呢?可以先了解一下OrderBy的基本执行逻辑:

  1. 初始化sort_buffer, 放入字段id, item_code, type, sub_type

  2. 从type索引中找到符合type = 'normal'的记录,获取id

  3. 根据id,从主键索引中获取id, item_code, type, sub_type,放入sort_buffer

  4. 重复2~3,直到没有符合type = 'normal'的记录

  5. sort_buffer根据item_code进行排序

  6. 排序结果取前20行返回

当排序的数据过大时,会启用外部排序(临时文件归并排序)

  • 这里可能会有些疑问,为什么要把不排序的字段也放到sort_buffer中?是因为排完序后,可以直接从排序的结果集中取出完整的Select所需要的字段。

部分字段排序

  • 那如果是Select *呢?并且表的字段非常多,是否会过度浪费sort_buffer的资源,导致触发外部排序呢?是的,但是Mysql中存在一个配置,max_length_for_sort_data,当所放字段大于这个值时,就不会把所有select的字段放入sort_buffer,而是选择排完序之后,再次进行回表,得到完整的数据:
  1. 初始化sort_buffer, 放入字段id, item_code

  2. 从type索引中找到符合type = 'normal'的记录,获取id

  3. 根据id,从主键索引中获取id, item_code,放入sort_buffer

  4. 重复2~3,直到没有符合type = 'normal'的记录

  5. sort_buffer根据item_code进行排序

  6. 排序结果取前20行, 得到对应的id,从聚簇索引中回表,得到完整的数据

通常来说,Mysql规格够大时,不建议使用这种排序方式,因为会额外回表。

覆盖索引跳过排序

如果此时存在type, item_code的覆盖索引,则无需额外排序,即可返回结果集,执行效率是最高的。

但是如果type的条件变为in呢?

-- SQL5 9000 ms
Select id, item_code, type, sub_type
From item
where type in ('normal', 'abnormal')
order by item_code ASC
-- 此处基于item_code排序
limit 0, 20;

答案是,需要排序。因此这种情况下推荐在业务代码中,将其拆为两句 type = 'normal' 和 type = 'abnormal'的SQL,然后业务代码中自行实现归并排序即可。

参考文献

  1. MySQL实战45讲

  2. 从根儿上理解Mysql

  3. Mysql官方文档/排序优化

SQL优化——深分页&排序的更多相关文章

  1. MySQL 千万数据库深分页查询优化,拒绝线上故障!

    文章首发在公众号(龙台的技术笔记),之后同步到博客园和个人网站:xiaomage.info 优化项目代码过程中发现一个千万级数据深分页问题,缘由是这样的 库里有一张耗材 MCS_PROD 表,通过同步 ...

  2. 系统优化怎么做-SQL优化

    大家好,这里是「聊聊系统优化 」,并在下列地址同步更新 博客园:http://www.cnblogs.com/changsong/ 知乎专栏:https://zhuanlan.zhihu.com/yo ...

  3. 没内鬼,来点干货!SQL优化和诊断

    SQL优化与诊断 Explain诊断 Explain各参数的含义如下: 列名 说明 id 执行编号,标识select所属的行.如果在语句中没有子查询或关联查询,只有唯一的select,每行都将显示1. ...

  4. SQL优化案例—— RowNumber分页

    将业务语句翻译成SQL语句不仅是一门技术,还是一门艺术. 下面拿我们程序开发工程师最常用的ROW_NUMBER()分页作为一个典型案例来说明. 先来看看我们最常见的分页的样子: WITH CTE AS ...

  5. C#拼接SQL语句,SQL Server 2005+,多行多列大数据量情况下,使用ROW_NUMBER实现的高效分页排序

    /// <summary>/// 单表(视图)获取分页SQL语句/// </summary>/// <param name="tableName"&g ...

  6. C# SQL优化 及 Linq 分页

    每次写博客,第一句话都是这样的:程序员很苦逼,除了会写程序,还得会写博客!当然,希望将来的一天,某位老板看到此博客,给你的程序员职工加点薪资吧!因为程序员的世界除了苦逼就是沉默.我眼中的程序员大多都不 ...

  7. SQL通用优化方案(where优化、索引优化、分页优化、事务优化、临时表优化)

    SQL通用优化方案:1. 使用参数化查询:防止SQL注入,预编译SQL命令提高效率2. 去掉不必要的查询和搜索字段:其实在项目的实际应用中,很多查询条件是可有可无的,能从源头上避免的多余功能尽量砍掉, ...

  8. 正确使用索引(sql优化),limit分页优化,执行计划,慢日志查询

    查看表相关命令 - 查看表结构   desc 表名- 查看生成表的SQL   show create table 表名- 查看索引   show index from  表名 使用索引和不使用索引 由 ...

  9. 查询效率提升10倍!3种优化方案,帮你解决MySQL深分页问题

    开发经常遇到分页查询的需求,但是当翻页过多的时候,就会产生深分页,导致查询效率急剧下降. 有没有什么办法,能解决深分页的问题呢? 本文总结了三种优化方案,查询效率直接提升10倍,一起学习一下. 1. ...

  10. 浅谈SQL优化入门:3、利用索引

    0.写在前面的话 关于索引的内容本来是想写的,大概收集了下资料,发现并没有想象中的简单,又不想总结了,纠结了一下,决定就大概写点浅显的,好吧,就是懒,先挖个浅坑,以后再挖深一点.最基本的使用很简单,直 ...

随机推荐

  1. Ubuntu 22.04 安装 Nvidia 驱动最方便安全的方式

    刚安装好的 Ubuntu 22.04 没有 N 卡驱动,输入 nvidia-smi,提示没有此程序并推荐到 apt 安装. 但是,使用 apt 安装 nvidia 驱动会有极大概率出现启动黑屏和闪屏问 ...

  2. C++之OpenCV入门到提高002:加载、修改、保存图像

    一.介绍 今天是这个系列<C++之 Opencv 入门到提高>得第二篇文章.今天这个篇文章很简单,只是简单介绍如何使用 Opencv 加载图像.显示图像.修改图像和保存图像,先给大家一个最 ...

  3. 2024年网鼎杯青龙组 pwn

    pwn2 开局泄露栈地址,又是栈溢出,直接栈转移拿下 from pwn import * from LibcSearcher import LibcSearcher #from Crypto.Util ...

  4. 2.14 Linux文件目录结构一览表

    学习 Linux,不仅限于学习各种命令,了解整个 Linux 文件系统的目录结构以及各个目录的功能同样至关重要. 使用 Linux 时,通过命令行输入 ls -l / 可以看到,在 Linux 根目录 ...

  5. 如何在原生鸿蒙APP中使用RN的bundle包

    一.创作背景 上一篇博客中,我给大家分享了如何创建一个RN的项目,并且解决了其中的问题点,成功打出了Bundle包.接下来就是我给大家分享一下,如何在原生鸿蒙项目中使用那个Bundle包,这一篇分享完 ...

  6. 使用SwingWorker异步加载JTree

    SwingWorker是Java SE 6.0新加入的一个工具包,利用它可以使长时间运行并更新用户界面的任务大大简化.本文以一个异步加载JTree的demo演示了SwingWorker的基本功能. 环 ...

  7. golang之异步队列Asynq

    Asynq[1]是一个Go实现的分布式任务队列和异步处理库,基于redis,类似Ruby的sidekiq[2]和Python的celery[3].Go生态类似的还有machinery[4]和gowor ...

  8. go mod使用小结

    转载请注明出处: go mod 命令是用于管理 Go 语言项目的模块依赖关系的工具.Go 语言从 1.11 版本开始引入了模块支持,并在后续版本中逐渐完善.模块是 Go 语言代码的一个集合,每个模块都 ...

  9. C#生成Guid的几种方式-迷恋自留地

    var uuid = Guid.NewGuid().ToString(); // 9af7f46a-ea52-4aa3-b8c3-9fd484c2af12 var uuidN = Guid.NewGu ...

  10. 对于web性能优化我有话说!

    web性能是什么呢? 简单来说就是咱们的项目打开的快不快,举以下几个例子 表单提交 列表切换 动画的流程性 打开速度 MDN上对web性能的定义则是Web性能是网站或应用程序的客观度量和可感知的用户体 ...