SQL优化——深分页&排序
问题背景
在开发 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的基本执行逻辑:
初始化sort_buffer, 放入字段id, item_code, type, sub_type
从type索引中找到符合type = 'normal'的记录,获取id
根据id,从主键索引中获取id, item_code, type, sub_type,放入sort_buffer
重复2~3,直到没有符合type = 'normal'的记录
sort_buffer根据item_code进行排序
排序结果取前20行返回
当排序的数据过大时,会启用外部排序(临时文件归并排序)
- 这里可能会有些疑问,为什么要把不排序的字段也放到sort_buffer中?是因为排完序后,可以直接从排序的结果集中取出完整的Select所需要的字段。
部分字段排序
- 那如果是Select *呢?并且表的字段非常多,是否会过度浪费sort_buffer的资源,导致触发外部排序呢?是的,但是Mysql中存在一个配置,max_length_for_sort_data,当所放字段大于这个值时,就不会把所有select的字段放入sort_buffer,而是选择排完序之后,再次进行回表,得到完整的数据:
初始化sort_buffer, 放入字段id, item_code
从type索引中找到符合type = 'normal'的记录,获取id
根据id,从主键索引中获取id, item_code,放入sort_buffer
重复2~3,直到没有符合type = 'normal'的记录
sort_buffer根据item_code进行排序
排序结果取前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,然后业务代码中自行实现归并排序即可。
参考文献
MySQL实战45讲
从根儿上理解Mysql
SQL优化——深分页&排序的更多相关文章
- MySQL 千万数据库深分页查询优化,拒绝线上故障!
文章首发在公众号(龙台的技术笔记),之后同步到博客园和个人网站:xiaomage.info 优化项目代码过程中发现一个千万级数据深分页问题,缘由是这样的 库里有一张耗材 MCS_PROD 表,通过同步 ...
- 系统优化怎么做-SQL优化
大家好,这里是「聊聊系统优化 」,并在下列地址同步更新 博客园:http://www.cnblogs.com/changsong/ 知乎专栏:https://zhuanlan.zhihu.com/yo ...
- 没内鬼,来点干货!SQL优化和诊断
SQL优化与诊断 Explain诊断 Explain各参数的含义如下: 列名 说明 id 执行编号,标识select所属的行.如果在语句中没有子查询或关联查询,只有唯一的select,每行都将显示1. ...
- SQL优化案例—— RowNumber分页
将业务语句翻译成SQL语句不仅是一门技术,还是一门艺术. 下面拿我们程序开发工程师最常用的ROW_NUMBER()分页作为一个典型案例来说明. 先来看看我们最常见的分页的样子: WITH CTE AS ...
- C#拼接SQL语句,SQL Server 2005+,多行多列大数据量情况下,使用ROW_NUMBER实现的高效分页排序
/// <summary>/// 单表(视图)获取分页SQL语句/// </summary>/// <param name="tableName"&g ...
- C# SQL优化 及 Linq 分页
每次写博客,第一句话都是这样的:程序员很苦逼,除了会写程序,还得会写博客!当然,希望将来的一天,某位老板看到此博客,给你的程序员职工加点薪资吧!因为程序员的世界除了苦逼就是沉默.我眼中的程序员大多都不 ...
- SQL通用优化方案(where优化、索引优化、分页优化、事务优化、临时表优化)
SQL通用优化方案:1. 使用参数化查询:防止SQL注入,预编译SQL命令提高效率2. 去掉不必要的查询和搜索字段:其实在项目的实际应用中,很多查询条件是可有可无的,能从源头上避免的多余功能尽量砍掉, ...
- 正确使用索引(sql优化),limit分页优化,执行计划,慢日志查询
查看表相关命令 - 查看表结构 desc 表名- 查看生成表的SQL show create table 表名- 查看索引 show index from 表名 使用索引和不使用索引 由 ...
- 查询效率提升10倍!3种优化方案,帮你解决MySQL深分页问题
开发经常遇到分页查询的需求,但是当翻页过多的时候,就会产生深分页,导致查询效率急剧下降. 有没有什么办法,能解决深分页的问题呢? 本文总结了三种优化方案,查询效率直接提升10倍,一起学习一下. 1. ...
- 浅谈SQL优化入门:3、利用索引
0.写在前面的话 关于索引的内容本来是想写的,大概收集了下资料,发现并没有想象中的简单,又不想总结了,纠结了一下,决定就大概写点浅显的,好吧,就是懒,先挖个浅坑,以后再挖深一点.最基本的使用很简单,直 ...
随机推荐
- python项目实战——人生重开模拟器
文章目录 1.菜单栏的编写 2.玩家确定颜值.体质.智力.家境 3.生成性别 4.设定角色出生点 5.各个年龄段的变化 5.1 幼年阶段 5.2 青年阶段 5.3中年阶段 5.4 晚年阶段 6.整体代 ...
- cn2 lab 笔记
Ubuntu 18.04 Kafka 先启动kafka自带的zookeeper 在data/kafka_2.13-3.3.1bin目录下执行 ./zookeeper-server-start.sh . ...
- NOIP2024模拟12:孤帆远影
NOIP2024模拟12:孤帆远影 听了机房同学的讨论,于是T1死磕冒泡和逆序对做法.最后只得了40pts. 思想对了,但不是自己的做法. 还是要坚持自己想,坚持自己可以想出来,不要被任何人带偏. T ...
- Go语言Context包源码学习
0前言 context包作为使用go进行server端开发的重要工具,其源码只有791行,不包含注释的话预计在500行左右,非常值得我们去深入探讨学习,于是在本篇笔记中我们一起来观察源码的实现,知其然 ...
- 软件项目技术点(12)——绘制生成的图表到canvas
AxeSlide软件项目梳理 canvas绘图系列知识点整理 插入图表 首先介绍我们的图表功能,点击插入图表弹出如下数据表格窗口,可以填写表格数据,点击确定,默认生成表格图 编辑图表 表格图选中, ...
- 基于nginx的tomcat负载均衡和集群(超简单)
今天看到"基于apache的tomcat负载均衡和集群配置 "这篇文章成为javaEye热点. 略看了一下,感觉太复杂,要配置的东西太多,因此在这里写出一种更简洁的方法. 要集群t ...
- 抓包工具之Charles(windows)
激活码: https://www.zzzmode.com/mytools/charles/ 官方地址:https://www.charlesproxy.com/ PC端如何配置才能抓取到https请 ...
- rsync之实战
简介 rsync是远程(或本地)复制和同步文件最常用的命令. 借助rsync命令,你可以跨目录,跨磁盘和跨网络远程与本地数据进行复制和同步.举例来说:在两台Linux主机之间进行数据备份和镜像.本文介 ...
- 负载均衡-一致性Hash算法
1. Hash算法 哈希(Hash)也称为散列,把任意长度的输入,通过散列算法变换成固定长度的输出,该输出就是散列值.哈希值(hashCode).(来自:百度百科) 在现实中,设计者常常将散列值作为索 ...
- juc 学习
CyclicBarrier 应用场景是比如在做压力测试时,使用多少个用户并发,做集合点测试. 比如设置 100个用户并发,100个用户同时进行压测,只有100个用户压测完毕时,才能再发起下一波的压力测 ...