半夜被慢查询告警吵醒,limit深度分页的坑

limit · 浏览次数 : 0

小编点评

## 分页查询优化 ### 一、问题引入 在使用分页查询时,我们可能会遇到查询速度慢的问题。本文将通过一个实际案例,分析慢查询的原因,并提出相应的优化方案。 ### 二、案例重现 #### 1. 建表语句 ```sql CREATE TABLE `Product` ( `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, `type` tinyint(3) unsigned NOT NULL DEFAULT '1', `spuCode` varchar(50) NOT NULL DEFAULT '', `spuName` varchar(100) NOT NULL DEFAULT '', `spuTitle` varchar(300) NOT NULL DEFAULT '', `channelId` bigint(20) unsigned NOT NULL DEFAULT '0', `sellerId` bigint(20) unsigned NOT NULL DEFAULT '0', `mallSpuCode` varchar(32) NOT NULL DEFAULT '', `originCategoryId` bigint(20) unsigned NOT NULL DEFAULT '0', `originCategoryName` varchar(50) NOT NULL DEFAULT '', `marketPrice` decimal(10,2) unsigned NOT NULL DEFAULT '0.00', `status` tinyint(3) unsigned NOT NULL DEFAULT '1', `isDeleted` tinyint(3) unsigned NOT NULL DEFAULT '0', `timeCreated` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), `timeModified` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), PRIMARY KEY (`id`) USING BTREE, UNIQUE KEY `uk_spuCode` (`spuCode`,`channelId`,`sellerId`), KEY `idx_timeCreated` (`timeCreated`), KEY `idx_spuName` (`spuName`), KEY `idx_channelId_originCategory` (`channelId`,`originCategoryId`,`originCategoryName`) USING BTREE, KEY `idx_sellerId` (`sellerId`) ) ENGINE=InnoDB AUTO_INCREMENT=12553120 DEFAULT CHARSET=utf8mb4 COMMENT='商品表'; ``` #### 2. 深度分页查询 ```sql SELECT * FROM Product WHERE timeCreated > "2020-09-12 13:34:20" LIMIT 10000000, 10; ``` ### 三、慢查询原因分析 #### 1. 普通索引与聚簇索引 普通索引的非叶子节点不包含所有行的数据记录,而是存储键值和主键值。在深度分页查询中,需要多次回表以获取完整数据,导致查询效率低下。 #### 2. 查询计划 深度分页查询的执行计划显示,需要进行大量的回表操作,这是导致查询速度慢的主要原因。 ### 四、优化方案 #### 1. 子查询法 通过将查询条件转移到主键索引树,减少回表次数。例如: ```sql SELECT * FROM Product WHERE id >= (SELECT p.id FROM Product p WHERE p.timeCreated > "2020-09-12 13:34:20" LIMIT 10000000, 1); ``` #### 2. 使用INNER JOIN 与子查询法类似,通过INNER JOIN方式优化查询。例如: ```sql SELECT * FROM Product p1 INNER JOIN (SELECT id FROM Product p WHERE p.timeCreated > "2020-09-12 13:34:20" LIMIT 10000000, 10) AS p2 ON p1.id = p2.id; ``` #### 3. 标签记录法(锚点记录法) 通过标记上次查询到的位置,下次查询时从该位置开始扫描。例如: ```sql SELECT * FROM Product WHERE timeCreated > "2020-09-12 13:34:20" AND id > 10000000 LIMIT 10; ``` ### 五、总结 通过以上分析和优化方案,我们可以有效地解决深度分页查询慢的问题。在实际应用中,可以根据具体情况选择合适的优化方法。

正文

分享是最有效的学习方式。

博客:https://blog.ktdaddy.com/

故事

梅雨季,闷热的夜,令人窒息,窗外一道道闪电划破漆黑的夜幕,小猫塞着耳机听着恐怖小说,辗转反侧,终于睡意来了,然而挨千刀的手机早不振晚不振,偏偏这个时候振动了一下,一个激灵,没有按捺住对内容的好奇,点开了短信,卧槽?告警信息,原来是负责的服务出现慢查询了。小猫想起来,今天在下班之前上线了一个版本,由于新增了一个业务字段,所以小猫写了相关的刷数据的接口,在下班之前调用开始刷历史数据。

考虑到表的数据量比较大,一次性把数据全部读取出来然后在内存里面去刷新数据肯定是不现实的,所以小猫采用了分页查询的方式依次根据条件查询出结果,然后进行表数据的重置。没想到的是,数据量太大,分页的深度越来越深,渐渐地,慢查询也就暴露出来了。

慢查询告警

强迫症小猫瞬间睡意全无,翻起来打开电脑开始解决问题。

那么为什么用使用limit之后会出现慢查询呢?接下来老猫和大家一起来剖析一下吧。

剖析流程

limit分页为什么会变慢?

在解释为什么慢之前,咱们来重现一下小猫的慢查询场景。咱们从实际的例子推进。

做个小实验

假设我们有一张这样的业务表,商品Product表。具体的建表语句如下:

CREATE TABLE `Product` (
  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
  `type` tinyint(3) unsigned NOT NULL DEFAULT '1' ,
  `spuCode` varchar(50) NOT NULL DEFAULT '' ,
  `spuName` varchar(100) NOT NULL DEFAULT '' ,
  `spuTitle` varchar(300) NOT NULL DEFAULT '' ,
  `channelId` bigint(20) unsigned NOT NULL DEFAULT '0',
  `sellerId` bigint(20) unsigned NOT NULL DEFAULT '0'
  `mallSpuCode` varchar(32) NOT NULL DEFAULT '',
  `originCategoryId` bigint(20) unsigned NOT NULL DEFAULT '0' ,
  `originCategoryName` varchar(50) NOT NULL DEFAULT '' ,
  `marketPrice` decimal(10,2) unsigned NOT NULL DEFAULT '0.00',
  `status` tinyint(3) unsigned NOT NULL DEFAULT '1' ,
  `isDeleted` tinyint(3) unsigned NOT NULL DEFAULT '0',
  `timeCreated` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
  `timeModified` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) ,
  PRIMARY KEY (`id`) USING BTREE,
  UNIQUE KEY `uk_spuCode` (`spuCode`,`channelId`,`sellerId`),
  KEY `idx_timeCreated` (`timeCreated`),
  KEY `idx_spuName` (`spuName`),
  KEY `idx_channelId_originCategory` (`channelId`,`originCategoryId`,`originCategoryName`) USING BTREE,
  KEY `idx_sellerId` (`sellerId`)
) ENGINE=InnoDB AUTO_INCREMENT=12553120 DEFAULT CHARSET=utf8mb4 COMMENT='商品表'

从上述建表语句中我们发现timeCreated走普通索引。
接下来我们根据创建时间来执行一下分页查询:

当为浅分页的时候,如下:

select * from Product where timeCreated > "2020-09-12 13:34:20" limit 0,10

此时执行的时间为:
"executeTimeMillis":1

当调整分页查询为深度分页之后,如下:

select * from Product where timeCreated > "2020-09-12 13:34:20" limit 10000000,10

此时深度分页的查询时间为:
"executeTimeMillis":27499

此时看到这里,小猫的场景已经重现了,此时深度分页的查询已经非常耗时。

剖析一下原因

简单回顾一下普通索引和聚簇索引

我们来回顾一下普通索引和聚簇索引(也有人叫做聚集索引)的关系。

大家可能都知道Mysql底层用的数据结构是B+tree(如果有不知道的伙伴可以自己了解一下为什么mysql底层是B+tree),B+tree索引其实可以分为两大类,一类是聚簇索引,另外一类是非聚集索引(即普通索引)。

(1)聚簇索引:InnoDB存储表是索引组织表,聚簇索引就是一种索引组织形式,聚簇索引叶子节点存放表中所有行数据记录的信息,所以经常会说索引即数据,数据即索引。当然这个是针对聚簇索引。

聚簇索引

由图可知在执行查询的时候,从根节点开始共经历了3次查询即可找到真实数据。倘若没有聚簇索引的话,就需要在磁盘上进行逐个扫描,直至找到数据为止。显然,索引会加快查询速度,但是在写入数据的时候,由于需要维护这颗B+树,因此在写入过程中性能也会下降。

(2)普通索引:普通索引在叶子节点并不包含所有行的数据记录,只是会在叶子节点存本身的键值和主键的值,在检索数据的时候,通过普通索引子节点上的主键来获取想要找到的行数据记录。

普通索引

由图可知流程,首先从非聚簇索引开始寻找聚簇索引,找到非聚簇索引上的聚簇索引后,就会到聚簇索引的B+树上进行查询,通过聚簇索引B+树找到完整的数据。该过程比较专业的叫法也被称为“回表”。

看一下实际深度分页执行过程

有了以上的知识基础我们再来回过头看一下上述深度分页SQL的执行过程。
上述的查询语句中idx_timeCreated显然是普通索引,咱们结合上述的知识储备点,其深度分页的执行就可以拆分为如下步骤:

1、通过普通索引idx_timeCreated,过滤timeCreated,找到满足条件的记录ID;

2、通过ID,回到主键索引树,找到满足记录的行,然后取出展示的列(回表);

3、扫描满足条件的10000010行,然后扔掉前10000000行,返回。

结合看一下执行计划:

执行计划

原因其实很清晰了:
显然,导致这句SQL速度慢的问题出现在第2步。其中发生了10000010次回表,这前面的10000000条数据完全对本次查询没有意义,但是却占据了绝大部分的查询时间。

再深入一点从底层存储来看,数据库表中行数据、索引都是以文件的形式存储到磁盘(硬盘)上的,而硬盘的速度相对来说要慢很多,存储引擎运行sql语句时,需要访问硬盘查询文件,然后返回数据给服务层。当返回的数据越多时,访问磁盘的次数就越多,就会越耗时。

替换limit分页的一些方案。

上述我们其实已经搞清楚深度分页慢的原因了,总结为“无用回表次数过多”。

那怎么优化呢?相信大家应该都已经知道了,其核心当然是减少无用回表次数了。

有哪些方式可以帮助我们减少无用回表次数呢?

子查询法

思路:如果把查询条件,转移回到主键索引树,那就不就可以减少回表次数了。
所以,咱们将实际的SQL改成下面这种形式:

select * FROM Product where id >= (select p.id from Product p where p.timeCreated > "2020-09-12 13:34:20" limit 10000000, 1) LIMIT 10;

测试一下执行时间:
"executeTimeMillis":2534

我们可以明显地看到相比之前的27499,时间整整缩短了十倍,在结合执行计划观察一下。

执行计划2

我们综合上述的执行计划可以看出,子查询 table p查询是用到了idx_timeCreated索引。首先在索引上拿到了聚集索引的主键ID,省去了回表操作,然后第二查询直接根据第一个查询的 ID往后再去查10个就可以了!

显然这种优化方式是有效的。

使用inner join方式进行优化

这种优化的方式其实和子查询优化方法如出一辙,其本质优化思路和子查询法一样。
我们直接来看一下优化之后的SQL:

select * from Product p1 inner join (select p.id from Product p where p.timeCreated > "2020-09-12 13:34:20" limit 10000000,10) as p2 on p1.id = p2.id

测试一下执行的时间:
"executeTimeMillis":2495

执行计划3

咱们发现和子查询的耗时其实差不多,该思路是先通过idx_timeCreated二级索引树查询到满足条件的主键ID,再与原表通过主键ID内连接,这样后面直接走了主键索引了,同时也减少了回表。

上面两种方式其核心优化思想都是减少回表次数进行优化处理。

标签记录法(锚点记录法)

我们再来看下一种优化思路,上述深度分页慢原因我们也清楚了,一次性查询的数据太多也是问题,所以我们从这个点出发去优化,每次查询少量的数据。那么我们可以采用下面那种锚点记录的方式。类似船开到一个地方短暂停泊之后继续行驶,那么那个停泊的地方就是抛锚的地方,老猫喜欢用锚点标记来做比方,当然看到网上有其他的小伙伴称这种方式为标签记录法。其实意思也都差不多。

这种方式就是标记一下上次查询到哪一条了,下次再来查的时候,从该条开始往下扫描。我们直接看一下SQL:

select * from Product p where p.timeCreated > "2020-09-12 13:34:20" and id>10000000 limit 10

显然,这种方式非常快,耗时如下:
"executeTimeMillis":1

但是这种方式显然是有缺陷的,大家想想如果我们的id不是连续的,或者说不是自增形式的,那么我们得到的数据就一定是不准确的。与此同时咱们也不能跳页查看,只能前后翻页。

当然存在相同的缺陷,我们还可以换一种写法。

select * from Product p where p.timeCreated > "2020-09-12 13:34:20" and id between 10000000 and 10000010  

这种方式也是一样存在上述缺陷,另外的话更要注意的是between ...and语法是两头都是闭区域间。上述语句如果ID连续不断地情况下,咱们最终得到的其实是11条数据,并不是10条数据,所以这个地方还是需要注意的。

存入到es中

上述罗列的几种分页优化的方法其实已经够用了,那么如果数据量再大点的话咋整,那么我们可能就要选择其他中间件进行查询了,当然我们可以选择es。那么es真的就是万能药吗?显然不是。ES中同样存在深度分页的问题,那么针对es的深度分页,那么又是另外一个故事了,这里咱们就不展开了。

写到最后

那么半夜三更爬起来优化慢查询的小猫究竟有没有解决问题呢?电脑前,小猫长吁了一口气,解决了!
我们看下小猫的优化方式:

select * from InventorySku isk inner join (select id from InventorySku where inventoryId = 6058 limit 109500,500 ) as d on isk.id = d.id

显然小猫采用了inner join的优化方法解决了当前的问题。

相信小伙伴们后面遇到这类问题也能搞定了。

我是老猫,资深研发老鸟,让我们一起聊聊技术,聊聊职场,聊聊人生。

与半夜被慢查询告警吵醒,limit深度分页的坑相似的内容:

半夜被慢查询告警吵醒,limit深度分页的坑

分享是最有效的学习方式。 博客:https://blog.ktdaddy.com/ 故事 梅雨季,闷热的夜,令人窒息,窗外一道道闪电划破漆黑的夜幕,小猫塞着耳机听着恐怖小说,辗转反侧,终于睡意来了,然而挨千刀的手机早不振晚不振,偏偏这个时候振动了一下,一个激灵,没有按捺住对内容的好奇,点开了短信,卧

程序员失业日记1:工作五年,交接半天

最近发现越来越多的小伙伴被公司裁员,有的是因为公司业绩不景气被裁员,有的是因为压力太大离职。很多公司都在裁人、减员。找工作也比之前难。刚好去年我也被上家裁员了,正好做一个系列的日志,希望能帮到在找工作的你。 本文为第一篇失业日记:工作五年,交接只需要半天。 上午敲代码,下午HR谈话 离职那天天,一切

坚持与确定性:毒药还是良药?

前段时间跟几个大龄程序员一起吃饭,聊了大家的现状,后来写了篇博客总结了一下《从大龄程序员现状聊聊出路》,本想着给朋友们提供些观点和思路,结果被有些网友批评了。 1. 我的认知达不到赚快钱 有的网友认为我在瞎扯,有的觉得我在灌鸡汤,还有的认为我在指错路。 文中虽然总结了一些自认为有价值的观点,本想着让

蜘蛛的依旧疯狂与园子的新畅想:尝试放出被屏蔽的百度蜘蛛网段

因为看到博文,百度搜索部门的人昨天对园子进行了线上回访,让我们看到了一丝希望。今天早上,带着这丝希望,我们试着放出今年3月因为过于疯狂、喜欢在别人地盘上飙车而被我们屏蔽的百度蜘蛛网段,看看半年之后它是否“疯”子回头,结果依旧是意料之中的疯狂依旧,“疯”性难移

[转帖]集群监控之 —— ipmi操作指南

https://www.cnblogs.com/gaoyuechen/p/8506930.html 这两天,配置了一堆500来个节点的大型集群,被ipmi的问题困扰了一天半,到下午16:40,终于解决了。这里来总结一下: 智能平台管理界面(IPMI,Intelligent Platform Mana

[转帖]美国把龙芯列入黑名单没有什么用

https://baijiahao.baidu.com/s?id=1759406814932648369 美国商务部周四在黑名单中又增加了28家中国实体,龙芯中科名列其中,理由仍是“威胁美国国家安全”之类的陈词滥调。谁都知道龙芯迟早会被美国制裁,只是笔者以为应该是在半年之后…… 笔者难以理解,龙芯是

经典游戏:吃豆人

该游戏的背景以黑色为主。画面中,“Google”6个字母组成回廊似的迷宫画面,四个颜色分别为红、黄、蓝、绿的鬼面符号在迷宫中穿梭,似乎在寻找一个半开半合的黄色圆圈符号。当记者按动键盘上的方位键时,发现该黄色圆圈符号可以行走,并且可以吞吃迷宫路径上的小黄豆,但遇到鬼面符号时就要被吃掉。

XTTS测试遇到问题:ORA-20001、ORA-06512

现场测试工程师在半夜电话反馈:在新建的小测试库做XTTS流程验证,遇到错误: ```shell ERROR at line 1: ORA-20001: TABLESPACE(S) IS READONLY OR, OFFLINE JUST CONVERT, COPY ORA-06512: at lin

[转帖]全连接队列和半连接队列

半连接队列 syn-cookie打开的情况下 服务器接收到第一次握手的消息后,不会立刻将相关信息放进半连接队列,而是根据对面发过来的报文计算自己的SYN初始序列号。 利用下面几个部分: 客户端IP、客户端端口号、服务端IP、服务端端口号,这4个部分计算一个哈希值一个缓慢增长的时间戳t客户端发来的SY

半同态计算芯片

半同态计算芯片 学习该文章:华控清交推出业界首款半同态计算芯片 赋能隐匿查询实用化 摘要 隐匿查询是指在不向数据提供方暴露查询方的查询意图,同时又能在保护数据提供方数据库中其他数据的情况下让查询方获得相关查询结果。实际使用的场景大多是跨广域网环境下基于关键字的查询。当前的常用方法要么需要传输大量的数