MySql大表分页(附独门秘技)
问题背景
MySql(InnoDB)中的订单表需要按时间顺序分页查询,且主键不是时间维度递增,订单表在百万以上规模,此时如何高效地实现该需求?
注:本文并非主要讲解如何建立索引,以下的分析均建立在有合适的索引的前提下
初步方案1
众所周知,MySql中,有一个limit offset, pageSize的用法,可以实现分页查询
select * from order where user_id = xxx and 【其它业务条件】 order by created_time, id limit offset, pageSize
因为created_time可能重复,所以order by时应加上id,保证顺序的确定性
点评
该方案在表规模较小的时候,不会暴露出问题,当order表增长到十万级,并且查询后面几页的时候,执行速度明显变慢,可能降到100ms的量级,如果数据量增长到百万级,则耗时达到秒级,如果增长到千万级,那耗时就变得完全不可接受了(曾排查过这样的线上慢SQL)
深入分析
方案1为啥在大表中表现这么差呢?我们可以来揣测一下MySql是怎么执行这个查询的
假设我们在user_id,created_time,以及【其它业务条件】建立了联合索引,当我要查找第100000条到100049条的记录时,因为MySql的索引是b+ tree结构,不像数组可以随机定位到第N条记录,它需要花不小的成本去找到N的位置,N越大,成本越大
抛开b+ tree的细节不讲,我们还可以借助统计表记录总数的SQL来理解
select count(1) from order
如果能非常高效地定位第N条记录,那么上述统计也能非常高效的执行,但实际上,在大表中统计记录总条数,也是非常慢的(本文是在InnoDB的场景下)
方案1低效的根本原因在于:定位到offset的成本过高,未能充分利用索引的有序性
方案2
索引(b+ tree)的特点在于,数据是有序的,虽然找到第N条记录的效率比较低,但找到某一条数据在索引中的位置,其效率是很高的(索引本来就是解决这个问题的)
我们换一种思路,每次取50条记录,第一次取的时候,指定从上次结束的位置继续往后取50条,这样,我们便可以利用上索引的有序性了
我们先看一个以id为序,进行分页查询的例子
select * from order where id > 'pre max id' order by id limit 50
第一次查询不用带条件,后续查询则传入前一次查询的最大id,简单分析可知,MySql在执行时,先定位到pre max id的位置(id是有序的,定位非常快),然后从这往后取50条记录即可,整个过程非常高效
我们回到最开始的问题,“按时间顺序分页查询,且主键不是时间维度递增”,此时我们不能用id作为分页的条件,因为按它去分页,便不是按时间顺序了,但也不能直接把id换成时间,因为时间可能会重复,我们来分析一下
| id | username | created_time |
| xxx | zhangsan | 2019-01-01 |
| ddd | zhangsan | 2019-02-03 |
| yyy | zhangsan | 2019-02-03 |
| abc | zhangsan | 2019-02-05 |
| aaa | zhangsan | 2020-08-01 |
假如前一次分页的最后一条记录为id=ddd的这条(created_time为2019-02-03),下一次查询使用created_time>2019-02-03作为条件时,则会把id=yyy的这条记录漏掉,如果换成created_time>=2019-02-03也不行,id=ddd的这条记录就又被查出来了
对于这个数据遗漏或重复的问题,我看到一种解决方案是这样的:
分三种情况进行查询
- 首次查询,created_time>='xxxx-xx-xx',如果不要求以某时间开始,则无条件
select * from order where user_id = xxx and 【其它业务条件】 and created_time >= 'xxxx-xx-xx' order by created_time, id limit pageSize
- 如果上次查询的记录条数等于pageSize,则用created_time和id的组合条件来查询,为了防止created_time在边界位置发生重复时漏掉数据
select * from order where user_id = xxx and 【其它业务条件】 and created_time = 'created_time of latest recored' and id > 'id of latest recored' order by created_time, id limit pageSize
- 如果上次查询的记录数小于pageSize,并且上次查询是第二种查询,则仅用created_time来查询,
select * from order where user_id = xxx and 【其它业务条件】 and created_time > 'created_time of latest recored' order by created_time, id limit pageSize
点评
上述方法确实可以解决漏掉数据或重复的问题,并且也有着不错的性能,但缺点也比较明显,查询过于复杂,得分情况执行不同的SQL,并且分页不稳定,中间查询出来的记录数可能小于pageSize,实际上后面还有数据
进一步深入分析
我尝试在网上找过资料,只找到了以id为分页顺序,然后用id>'pre max id'这种方式来查,而我们要以可重复的created_time为分页顺序,如何写出简洁高效的SQL呢?
如果要成为一个优秀的程序员,我觉得分析&解决新问题的能力,是必不可少的,即使在网上能找到解决方案,优秀的分析能力也有助于借鉴并结合自己的场景,优化出更好的个性化方案。
我们在(user_id,created_time)建立了索引,并且我们知道InnoDB的辅助索引是包含了主键的,且主键一定不会重复,这意味着在索引上,每条记录的顺序是完全确定的,不存在重复的情况
我们要分页的顺序跟此索引的顺序是吻合的,只需要沿着索引,一批一批地取数据就可以了,这是一个对索引很直接的利用,为什么现在我没办法做到?
如果我是MySql的设计人员,针对这种很常见很直接的需求,我怎么去提供支持?还是说不支持?
我举一个例子,像java中的基于排序的TreeSet,我猜它一定有floor和ceiling这样的方法(返回Set中,大于或小于指定元素的第一个元素),这是基于排序的数据结构该有的东西,如果它没有,那早被人喷了然后加上去了
回到索引的话题,这种直接的需求,它应该支持,否则说不过去,现在的问题变成了:用什么语法来,来实现在组合索引上,基于组合(user_id,created_time,id的组合)顺序的遍历?
此时脑海里便回想起以前用过的(a,b) in ((1,2),(3,4),(7,4))这样的组合写法,然后猜测它也支持大于小于这类比较,跑去MySql中验证一下:
select (3,7)>(3,7), (3,6)>(3,7), (3,8)>(3,7), (4,7)>(3,7), (4,2)>(3,7);
返回:
0 0 1 1 1
如此一来,这问题就变得和id>'pre max id'这种一样简单了。
注:这种写法后来在官方文档中找到了对应的资料,官方称这类运算为“行比较”(row comparisons)
https://dev.mysql.com/doc/refman/5.7/en/comparison-operators.html#operator_greater-than
方案3
由于有(a,b)>(1,2)这种语法,所以可以写出简洁又高效的SQL
select * from order where user_id = xxx and 【其它业务条件】 and (created_time, id) > (created_time and id of latest recode) order by created_time, id limit pageSize
此方式跟以id为序的分页查询是一样的,首次查询去掉组合条件即可,代码简洁,同时又可以利用上组合索引,十分高效,耗时稳定,不会因为遍历到末尾而性能降低
总结
方案1在小表的情况下,简单方便,只用传页码和页大小即可,还可以随机跳到指定页,具有一定优势
方案2和方案3在大表的情况下,有着优异的性能,以及稳定性,缺点是不能随机地跳转页面,需要传入上一页的排序字段。这个弊端在一定程度上可以规避,比如现在很多分页都是一页一页地往下翻,比如微博、朋友圈动态等,或者是分批处理全表数据,不需要随机跳转
细心的同学可能发现,where条件里还有【其它业务条件】,这样还能正常走索引吗?是否会发生全表扫描?这个问题其实是可以规避的,有空再写一篇执行计划并不完全可靠的案例。
题外话
方案3的写法是我自己琢磨出来的,在网上也没找到类似的资料,算独门秘技吧,除此之外,我觉得同样很有价值的是【进一步深入分析】中的思考过程,如果养成这种思考习惯,有利于创新,去解决别人没遇到过的问题,在未知的领域,知道该从哪个方向去寻找答案;或者找到新的方法更好地去解决旧问题。
如果本文有帮助到你,或者觉得有价值,麻烦点个赞,这样我会更有动力去更多地分享自己的经验
MySql大表分页(附独门秘技)的更多相关文章
- 优秀后端架构师必会知识:史上最全MySQL大表优化方案总结
本文原作者“ manong”,原创发表于segmentfault,原文链接:segmentfault.com/a/1190000006158186 1.引言 MySQL作为开源技术的代表作之一,是 ...
- MySQL 大表优化方案(长文)
当MySQL单表记录数过大时,增删改查性能都会急剧下降,可以参考以下步骤来优化: 单表优化 除非单表数据未来会一直不断上涨,否则不要一开始就考虑拆分,拆分会带来逻辑.部署.运维的各种复杂度,一般以整型 ...
- [记录]一则清理MySQL大表以释放磁盘空间的案例
一则清理MySQL大表以释放磁盘空间的案例 一.基本情况: 1.dbtest库554G,先清理st_online_time_away_ds(37G)表的数据,保留半年的数据: 1)删除的数据:sele ...
- 从云数据迁移服务看MySQL大表抽取模式
摘要:MySQL JDBC抽取到底应该采用什么样的方式,且听小编给你娓娓道来. 小编最近在云上的一个迁移项目中被MySQL抽取模式折磨的很惨.一开始爆内存被客户怼,再后来迁移效率低下再被怼.MySQL ...
- MySQL大数据分页的优化思路和索引延迟关联
之前上次在部门的分享会上,听了关于MySQL大数据的分页,即怎样使用limit offset,N来进行大数据的分页,现在做一个记录: 首先我们知道,limit offset,N的时候,MySQL的查询 ...
- 详解MySQL大表优化方案( 转)
当MySQL单表记录数过大时,增删改查性能都会急剧下降,可以参考以下步骤来优化: 单表优化 除非单表数据未来会一直不断上涨,否则不要一开始就考虑拆分,拆分会带来逻辑.部署.运维的各种复杂度,一般以整型 ...
- MySQL 大表优化方案探讨
当MySQL单表记录数过大时,增删改查性能都会急剧下降,可以参考以下步骤来优化: 单表优化 除非单表数据未来会一直不断上涨,否则不要一开始就考虑拆分,拆分会带来逻辑.部署.运维的各种复杂度,一般以整型 ...
- MySQL大表优化方案
转:https://segmentfault.com/a/1190000006158186?hmsr=toutiao.io&utm_medium=toutiao.io&utm_sour ...
- MySQL 大表优化方案
当MySQL单表记录数过大时,增删改查性能都会急剧下降,可以参考以下步骤来优化: 单表优化 除非单表数据未来会一直不断上涨,否则不要一开始就考虑拆分,拆分会带来逻辑.部署.运维的各种复杂度,一般以整型 ...
随机推荐
- Sql Or NoSql,看完这一篇你就懂了(转五月的仓颉)
前言 你是否在为系统的数据库来一波大流量就几乎打满CPU,日常CPU居高不下烦恼?你是否在各种NoSql间纠结不定,到底该选用那种最好?今天的你就是昨天的我,这也是写这篇文章的初衷. 这篇文章是我好几 ...
- scrapy 基础组件专题(九):scrapy-redis 源码分析
下面我们来看看,scrapy-redis的每一个源代码文件都实现了什么功能,最后如何实现分布式的爬虫系统: connection.py 连接得配置文件 defaults.py 默认得配置文件 dupe ...
- 用PyInstaller打包用PyQt5编写的python程序
0.背景 本弱初学PyQt5,写了一个GUI小程序,但在用PyInstaller打包时出现了不少问题,现将几个比较大的问题记录如下,希望以后能记住. 1. 资源打包 首先是资源打包的问题,我写的程序引 ...
- Python Ethical Hacking - BeEF Framework(1)
Browser Exploitation Framework. Allows us to launch a number of attacks on a hooked target. Targets ...
- JVM系列之:详解java object对象在heap中的结构
目录 简介 对象和其隐藏的秘密 Object对象头 数组对象头 整个对象的结构 简介 在之前的文章中,我们介绍了使用JOL这一神器来解析java类或者java实例在内存中占用的空间地址. 今天,我们会 ...
- 一起学Blazor WebAssembly 开发(3)
接着上篇,本篇开始讲下实现登录窗口,先看下大概的效果图: 打开的效果,没有美工美化 点登录校验得到不能为空 我在做blazor时用到了一个ui框架,这个框架名叫Ant Design blazor(ht ...
- 题解 洛谷 P4695 【[PA2017]Banany】
考虑用动态点分治来解决像本题这样带修的树上路径问题. 首先对原树进行点分治,建出点分树,在点分树每个节点上用动态开点线段树来维护以该节点为起点,到其点分树子树中每个节点的利润. 查询时只需在点分树上当 ...
- 题解 洛谷 P3710 【方方方的数据结构】
因为有撤销操作,所以修改操作可能会只会存在一段时间,因此把时间看作一维,被修改的序列看作一维. 可以把操作都离线下来,对于每个修改操作,就是在二维平面上对一个矩形进行修改,询问操作,就是查询单点权值. ...
- python-多任务编程02-进程(processing)
进程与程序 程序:例如xxx.py这是程序,是一个静态的 进程:一个程序运行起来后,代码+用到的资源 称之为进程,它是操作系统分配资源的基本单元. multiprocessing模块 multipro ...
- 下载spring的路径的文章,已经试用没问题
文章:https://blog.csdn.net/ethan__xu/article/details/80273249 spring jar下载路径 http://repo.spring.io/rel ...