# 背景
之前碰到了MySQL大表查询时的优化问题。因此写了这个文章,整理记录一下目前网上的一些大表分页查询的思路。文中首先创建了一个测试数据集,并讨论了两类情况以及常见的几种优化方案的原理及效率
- 针对主键的id排序的分页查询
- 针对随机分布的create_time排序的分页查询
常见的优化方案有:
- 针对id优化查询
- 使用子查询
- 使用临时表
- 使用游标查询
# 参考链接
https://blog.csdn.net/zhaojw_420/article/details/135910080 https://www.cnblogs.com/trytocatch/p/mysql-page-query.html https://www.cnblogs.com/yanggb/p/11058707.html
# 环境准备
创建一个order表作为测试数据。注意:这里使用id作为主键,并对create_time加上了索引。
配置:
- 采用本地搭建MySQL数据库,配置为2核CPU,1G内存。使用配置脚本插入1000w行数据。其他配置的数据集请自行测试
- 文中的查询尽可能使用第一次查询得到的数据,减少数据库缓存对查询性能的干扰
# SQL脚本
|
|
# 数据创建
使用python脚本插入数据
|
|
# 大数据量分页查询及其问题
一些常见的分页查询sql
|
|
可以看到,随着查询分页时,起始位置的增加,查询性能成倍变慢。原因如下:
当我们使用 LIMIT offset, count 查询语句进行深度分页查询时,例如 LIMIT 10000000,100 ,会发生以下过程:
- MySQL首先会根据给定条件从相应的索引树中查找m+n条记录。对于聚集索引来说,它直接找到需要的结果即丢弃前offset条数据,返回count条数据并返回;
- 如果使用的是二级索引,在查到m+n条记录后还需要通过这些记录所关联的主键ID去聚集索引里再次搜索出完整的行数据,然后再丢弃掉前offset条数据,返回count条数据。因此在这个过程中可能会产生大量的“回表”操作,这将导致性能下降。
- 所以当offset+count量很大时,Mysql的执行器认为全表扫描的性能更由于使用索引,所以也导致索引失效。所以我们要做的尽可能的减少回表的记录数量。
- 定位到offset的成本过高,获取了很多无效数据并丢弃,未能充分利用索引的有序性
# 针对ID排序的分页查询
# 使用id作为限定条件优化
可以使用between and语句或大于小于符号直接限定id位置。适用于id连续分布,且已知id范围
|
|
注:
- 适用于id连续分布,且已知id范围
- 如表中id不连续或被删除过部分,可考虑适当放宽id范围,确保查询数量大于pagesize。并通过LIMIT限制最终数量
# 使用子查询优化
|
|
主要优化思路:
- 在查询时,使用select id替代 select *,不需要获取全部数据再丢弃,加速了大约3倍
- 先使用子查询直接获取起始id位置,再直接从id位置向后搜索100条取出
- 相对于原本分页查询,整体速度快了几倍
# 使用 INNER JOIN 优化
|
|
思路类似子查询,将子查询的结果构造为临时表。子查询中仅查询id相关,不需要获取全部数据并使用了主键索引。再在主查询中使用对应id查询所有数据
注:在基于id的分页查询中效率较低,不如直接使用基于主键id的子查询
# 游标查询(SEARCH AFTER)方案
思路:每次都记录查询开始的位置,每次取X条记录,指定从上次结束的位置继续往后取X条,利用索引的有效性加快查询
|
|
优点:
- 简单易理解
- 使用主键,查询效率高
缺点:
- 如果id不连续或不断变化,可能漏掉数据
- 只能支持连续分页,不能支持获取随意页的数据。
# 针对create_time排序的分页查询
# 普通查询及问题
|
|
可以看到,对于create_time排序的查询速度比相同数量的对于id排序的查询速度慢了更多。这种情况下,更需要优化SQL查询的效率。
分析一下,慢的主要原因有:
- create_time不是主键,在索引上查找二级索引后,需要再进行回表查找主键,再获取对应全部数据,再全部丢弃,效率更差
- 本次生成的数据集中create_time随机排布,对排序查找更不友好。
# 使用子查询
例子:
|
|
优化思路:
- 子查询中,create_time直接从二级索引上可以获取,不会进行回表
- 主查询中,从指定的create_time查询,取100调数据。需要回表查询的数据仅需要100条。
- 减少了大量需要回表查询再丢弃的数据
# 使用 INNER JOIN
例子:
|
|
优化思路:
- 临时表查询中create_time存储在二级索引中,且二级索引中能直接获取对应id,不会进行回表查找
- 临时表通过id与主查询关联,仅需查询count条数据
- 减少了回表的数据,并使用了主键索引,效率很高
- 在基于create_time排序时,相对于子查询优化的方案效率更高。因为直接传递了id
# 游标查询(SEARCH AFTER)方案
在对create_time进行游标式分页查询时,实际上由于create_time可能重复,需要用到create_time和id两个条件来确保排序。使用普通分页查询的想法去查询,会比较复杂。这里提供两种思路供参考
# 普通游标查询思路
但如果按照create_time排序,写法较麻烦。可分三种情况执行:
- 首次查询,create_time>=‘xxxx-xx-xx’,如果不要求以某时间开始,则无条件
|
|
- 如果上次查询的记录条数等于pageSize,则用create_time和id的组合条件来查询,防止create_time在边界位置发生重复时漏掉数据
|
|
- 如果上次查询的记录数小于pageSize,并且上次查询是第二种查询,则仅用create_time来查询
|
|
可以看到,这样去通过游标查询,逻辑非常繁琐复杂,一般不建议这样使用
# 多条件行比较思路
mysql支持多条件 行比较,如需要要求(a,b,c)>(x,y,z),可以使用写法:
|
|
因此,游标式查询sql可以改为
|
|
缺点:
- 不能随机跳转数据
- 对于同时使用create_time和id排序时,这种查询效率较低,更建议使用其他方式
# 总结
本文整理了一些网上对于大表分页搜索的思考以及方案。
总的来说,大表分页查询慢的主要原因是:使用LIMIT进行分页时,会将前半无用的数据都取出,抛弃后再取出真正需要的数据。当数据量大时,取出前半无用数据的过程就消耗了大量查询时间
主要解决的思路有:
- 基于主键id直接查询
- 在子查询中使用LIMIT分页时仅查询需要的可索引字段(如id,create_time等),再在主查询中获取对应的全部数据
- 使用inner join。在子查询中使用LIMIT分页,但仅查询主键id字段,作为临时表输出。再在主查询中直接根据主键id获取对应全部数据
- 游标式查询,每次都基于上次查询的最大值累进分页
对于按照不同方式排序的分页查询,不同的思路效率不同。还需要根据实际情况选择适当的方案