文章目录
-
- 一、慢查询定位
-
- [1.1 开启慢查询日志](#1.1 开启慢查询日志)
- [1.2 查看慢查询日志](#1.2 查看慢查询日志)
-
- [1.2.1 文本方式查看:快速了解日志内容](#1.2.1 文本方式查看:快速了解日志内容)
- [1.2.2 使用工具分析:高效处理大量日志](#1.2.2 使用工具分析:高效处理大量日志)
-
- [(1)mysqldumpslow:MySQL 自带分析工具](#(1)mysqldumpslow:MySQL 自带分析工具)
- (2)第三方工具:更全面的分析能力
- 二、慢查询优化
-
- [2.1 索引与慢查询的关系](#2.1 索引与慢查询的关系)
-
- [2.1.1 如何判断是否为慢查询?](#2.1.1 如何判断是否为慢查询?)
- [2.1.2 如何判断是否应用了索引?](#2.1.2 如何判断是否应用了索引?)
- [2.1.3 应用了索引是否一定快?](#2.1.3 应用了索引是否一定快?)
- [2.2 提高索引过滤性](#2.2 提高索引过滤性)
-
- [2.2.1 索引过滤性的重要性](#2.2.1 索引过滤性的重要性)
- [2.2.2 影响索引过滤性的因素](#2.2.2 影响索引过滤性的因素)
- [2.2.3 案例:通过联合索引提升过滤性](#2.2.3 案例:通过联合索引提升过滤性)
- [2.3 慢查询原因总结](#2.3 慢查询原因总结)
- [三、分页查询优化:解决 "越往后查越慢" 的问题](#三、分页查询优化:解决 “越往后查越慢” 的问题)
-
- [3.1 `LIMIT` 子句的工作原理](#3.1
LIMIT
子句的工作原理) -
- [3.1.1 `LIMIT` 子句的语法格式](#3.1.1
LIMIT
子句的语法格式) - [3.1.2 一般性分页的性能问题](#3.1.2 一般性分页的性能问题)
- [3.1.3 性能问题根源](#3.1.3 性能问题根源)
- [3.1.1 `LIMIT` 子句的语法格式](#3.1.1
- [3.2 分页优化方案](#3.2 分页优化方案)
-
- [3.2.1 方案一:利用覆盖索引优化](#3.2.1 方案一:利用覆盖索引优化)
- [3.2.2 方案二:利用子查询优化](#3.2.2 方案二:利用子查询优化)
- [3.2.3 注意事项](#3.2.3 注意事项)
- [3.1 `LIMIT` 子句的工作原理](#3.1
在数据库应用中,随着数据量的不断增长,MySQL 查询性能逐渐成为影响系统整体响应速度的关键因素。本文将从慢查询定位、慢查询优化以及分页查询优化三个核心维度,深入剖析 MySQL 查询优化的方法与原理,帮助读者 "知其然更知其所以然",真正学到可落地的干货知识。
一、慢查询定位
慢查询就像数据库性能的 "隐形杀手",若不能及时发现并处理,会持续消耗系统资源。定位慢查询的核心手段是利用 MySQL 的慢查询日志,通过配置相关参数,将执行时间过长或未使用索引的查询语句记录下来,再借助分析工具深入挖掘问题根源。
1.1 开启慢查询日志
慢查询日志是 MySQL 自带的性能监控工具,默认情况下可能处于关闭状态。我们需要通过特定命令开启该日志,并配置关键参数,确保其能精准捕捉慢查询语句。
首先,查看当前 MySQL 数据库是否开启慢查询日志以及慢查询日志文件的存储位置,可执行以下命令:
sql
SHOW VARIABLES LIKE 'slow_query_log%';
该命令会返回slow_query_log
(慢查询日志开关状态,ON 为开启,OFF 为关闭)和slow_query_log_file
(慢查询日志文件路径)两个关键参数的信息。
若慢查询日志未开启,需通过以下命令进行配置:
- 设置慢查询阈值(long_query_time):该参数指定慢查询的判定阈值,单位为秒。当 SQL 语句的执行时间超过该阈值时,就会被判定为慢查询并记录到日志中。默认值为 10 秒,可根据业务需求灵活调整,例如将阈值设为 1 秒,以捕捉更多潜在的性能问题:
sql
SET global long_query_time = 1;
- 开启慢查询日志(slow_query_log):执行以下命令开启慢查询日志,并指定日志文件存储路径(若不指定,将使用默认路径):
sql
SET global slow_query_log = ON;
SET global slow_query_log_file = 'OAK-slow.log';
- 记录未使用索引的查询(log_queries_not_using_indexes) :该参数用于记录未使用索引的查询 SQL,但需注意,只有当
slow_query_log
的值为 ON 时,该参数才会生效。开启此参数可帮助我们发现那些未合理利用索引的查询语句,命令如下:
sql
SET global log_queries_not_using_indexes = ON;
需要注意的是,以上通过SET global
命令进行的配置仅在当前 MySQL 服务运行期间生效,若服务重启,配置会恢复默认值。若需永久生效,需在 MySQL 的配置文件(如 my.cnf 或 my.ini)中添加相应配置项,再重启服务。
1.2 查看慢查询日志
开启慢查询日志后,MySQL 会将符合条件的慢查询语句记录到日志文件中。我们可以通过两种方式查看慢查询日志:文本方式直接查看和使用专用工具分析,前者适合快速浏览,后者适合深入分析大量日志数据。
1.2.1 文本方式查看:快速了解日志内容
慢查询日志以文本形式存储,可直接使用文本编辑器(如 Notepad++、vim 等)打开日志文件(如 OAK-slow.log)。日志每条记录包含以下关键信息,通过这些信息可初步判断慢查询的问题所在:
-
time:日志记录的时间,用于定位慢查询发生的具体时刻。
-
User@Host:执行慢查询的用户账号及客户端主机地址,可用于排查特定用户或主机的查询问题。
-
Query_time :SQL 语句的执行时间,是判断是否为慢查询的核心依据,与
long_query_time
阈值对比可验证配置是否生效。 -
Lock_time:SQL 语句执行过程中表的锁定时间,若该值过大,可能意味着存在表锁竞争问题,需优化表结构或查询语句。
-
Rows_sent:发送给客户端的记录数,反映查询结果的数量,若结果集过大,可能需要分页查询优化。
-
Rows_examined :语句执行过程中扫描的记录条数,是判断查询效率的关键指标。若
Rows_examined
远大于Rows_sent
,说明查询扫描了大量无关数据,可能存在索引未合理使用的问题。 -
SET timestamp :语句执行的时间戳,与
time
字段结合可更精准地定位慢查询发生的时间。 -
select xxx:具体的 SQL 执行语句,是后续优化的核心对象。
例如,一条典型的慢查询日志记录可能如下:
sql
# Time: 2024-05-20T10:30:00.000000Z
# User@Host: root[root] @ localhost [127.0.0.1] Id: 12345
# Query_time: 15.200000 Lock_time: 0.100000 Rows_sent: 10 Rows_examined: 500000
SET timestamp=1716234600;
select * from user where age > 30 order by name;
从这条记录可知,该查询执行时间为 15.2 秒(超过 1 秒的阈值),锁定时间 0.1 秒,返回 10 条记录却扫描了 50 万条数据,明显存在效率问题,需进一步优化。
1.2.2 使用工具分析:高效处理大量日志
当慢查询日志数据量较大时,文本方式查看效率极低,此时可借助 MySQL 提供的专用工具或第三方工具进行分析,快速提取关键信息。
(1)mysqldumpslow:MySQL 自带分析工具
mysqldumpslow 是 MySQL bin 目录下自带的慢查询日志分析工具,支持按执行时间、扫描行数等维度对慢查询进行排序和统计,帮助我们快速定位最耗时、最消耗资源的查询语句。
首先,在 MySQL 的 bin 目录下执行以下命令,查看 mysqldumpslow 的使用格式和参数说明:
perl mysqldumpslow.pl --help
常用参数说明:
-
-t N
:指定显示前 N 条慢查询记录,例如-t 5
表示显示执行时间最长的 5 条记录。 -
-s sort_type
:指定排序方式,常用的排序类型包括:-
at
:按查询执行时间(Query_time)降序排序(默认); -
al
:按锁定时间(Lock_time)降序排序; -
ar
:按返回记录数(Rows_sent)降序排序; -
ae
:按扫描记录数(Rows_examined)降序排序。
-
-
日志文件路径:指定要分析的慢查询日志文件路径。
例如,执行以下命令,查看慢查询日志中执行时间最长的 5 条记录:
perl mysqldumpslow.pl -t 5 -s at C:\ProgramData\MySQL\Data\OAK-slow.log
该命令会输出前 5 条执行时间最长的慢查询语句,并统计每条语句的执行次数、平均执行时间、平均锁定时间等信息,帮助我们快速识别高频、高耗时的查询。
(2)第三方工具:更全面的分析能力
除了 mysqldumpslow,还有许多功能更强大的第三方工具可用于慢查询日志分析,例如 pt-query-digest(Percona Toolkit 中的工具)、mysqlsla 等。这些工具支持更丰富的统计维度(如按 SQL 模板分组、按数据库分组),还能生成可视化报告,更适合企业级的日志分析场景。
以 pt-query-digest 为例,它能将相似的 SQL 语句(如参数不同的查询)归为一个模板,统计每个模板的执行情况,避免因 SQL 参数不同导致的重复统计。执行以下命令即可对慢查询日志进行分析:
pt-query-digest C:\ProgramData\MySQL\Data\OAK-slow.log
分析结果会包含慢查询的总体统计信息(如总执行时间、平均执行时间)、按执行时间排序的 SQL 模板列表,以及每条模板的详细执行信息(如扫描行数、使用的索引等),为后续优化提供更精准的方向。
二、慢查询优化
定位到慢查询后,下一步需要分析慢查询的原因并进行优化。慢查询的产生原因多种多样,可能是未使用索引、索引使用不合理、全表扫描、回表查询频繁等。其中,索引是影响查询性能的核心因素,因此优化慢查询通常从索引入手,同时结合查询语句的执行逻辑进行调整。
2.1 索引与慢查询的关系
在优化慢查询前,我们需要先明确三个关键问题:如何判断一条查询是否为慢查询?如何判断查询是否使用了索引?应用了索引是否一定能提升查询速度?理清这三个问题的逻辑,是后续优化的基础。
2.1.1 如何判断是否为慢查询?
MySQL 判断一条 SQL 语句是否为慢查询的逻辑非常明确:将语句的实际执行时间与long_query_time
参数设定的阈值进行比较。若执行时间大于 long_query_time
,则判定为慢查询,并记录到慢查询日志中;若执行时间小于或等于阈值,则不属于慢查询。
需要注意的是,long_query_time
的默认值为 10 秒,但在实际业务中,这个阈值可能过高。例如,对于实时性要求较高的业务(如电商订单查询),若查询执行时间超过 1 秒,用户就会明显感觉到卡顿,此时应将long_query_time
调整为 1 秒甚至更低,以捕捉更多潜在的慢查询。
2.1.2 如何判断是否应用了索引?
判断 SQL 语句是否使用了索引,最常用且有效的方法是使用EXPLAIN
命令。EXPLAIN
命令能模拟 MySQL 执行查询语句的过程,输出查询计划的详细信息,其中key
字段是判断是否使用索引的核心依据。
EXPLAIN
命令的使用格式如下:
EXPLAIN SELECT 字段列表 FROM 表名 WHERE 条件;
例如,要判断select id from user order by abs(age);
(假设表中已创建age
索引)是否使用了索引,可执行:
EXPLAIN select id from user order by abs(age);
执行结果中,key
字段的取值含义如下:
-
若
key
字段的值为具体的索引名 (如age
),说明查询使用了该索引; -
若
key
字段的值为NULL,说明查询未使用任何索引,可能存在全表扫描的问题。
例如,执行EXPLAIN select * from user where id = 2;
(假设id
为主键,默认创建主键索引),key
字段的值会显示为PRIMARY
(主键索引的默认名称),说明查询使用了主键索引;而执行EXPLAIN select * from user where age > 30;
(若未创建age
索引),key
字段的值为 NULL,说明查询未使用索引,会进行全表扫描。
2.1.3 应用了索引是否一定快?
很多人存在一个误区:认为只要查询使用了索引,执行速度就一定快。但实际上,应用了索引并不意味着查询效率一定高,关键在于索引是否真正减少了查询扫描的数据行数。若索引使用不当,即使应用了索引,也可能出现 "索引失效" 的情况,导致查询效率低下。
例如,执行select * from user where id > 0;
(id
为主键,存在主键索引),通过EXPLAIN
分析会发现,key
字段的值为PRIMARY
(说明使用了主键索引),但type
字段的值为index
(表示全索引扫描),Rows_examined
字段的值等于表中的总记录数。这是因为id > 0
的条件会匹配表中的所有记录,MySQL 会从主键索引的最左边叶节点开始,向右扫描整个索引树,本质上与全表扫描没有区别,此时索引失去了 "快速定位" 的意义,查询效率依然很低。
而执行select * from user where id = 2;
时,EXPLAIN
结果中type
字段的值为const
(表示通过索引一次就能找到数据),Rows_examined
字段的值为 1(仅扫描一条记录),这才是索引的正确使用方式,能显著提升查询效率。
由此可见,"是否使用索引" 与 "是否为慢查询" 之间没有必然联系:使用了索引的查询可能因全索引扫描而成为慢查询;未使用索引的查询(如小表查询)可能因数据量小而执行速度很快,不属于慢查询。因此,在优化慢查询时,不能只关注是否使用了索引,更应关注索引是否真正减少了扫描的数据行数 ------ 只有当Rows_examined
显著降低时,查询效率才会得到本质提升。
2.2 提高索引过滤性
索引的过滤性是指通过索引筛选后,剩余数据量占总数据量的比例。过滤性越高,说明索引能筛选掉更多无关数据,Rows_examined
越小,查询效率越高;反之,过滤性越低,索引的作用越有限,查询效率越低。
2.2.1 索引过滤性的重要性
假设存在一个包含 5000 万条记录的用户表(user
),若我们创建了sex
字段的索引,执行查询select * from user where sex = '男';
。由于 "性别" 字段的取值只有 "男""女" 两种(部分场景可能有其他取值,但总体取值范围极小),通过sex = '男'
筛选后,可能仍有 3000 万条记录需要处理。此时,即使使用了索引,Rows_examined
依然高达 3000 万,查询执行时间依然会很长,索引的过滤性极差,无法有效提升查询效率。
反之,若我们创建了phone
字段的索引(手机号唯一),执行查询select * from user where phone = '13800138000';
,通过索引筛选后,Rows_examined
仅为 1(仅扫描一条记录),索引过滤性接近 100%,查询效率极高。
由此可见,索引过滤性的高低直接决定了索引的有效性。在创建索引时,需优先选择过滤性高的字段,避免为取值范围小、重复度高的字段(如性别、状态等)创建单独索引(除非与其他字段组合创建联合索引)。
2.2.2 影响索引过滤性的因素
索引的过滤性主要受以下三个因素影响:
-
索引字段的取值范围:取值范围越广、重复度越低的字段,过滤性越高(如手机号、身份证号、邮箱等唯一字段);取值范围越窄、重复度越高的字段,过滤性越低(如性别、年龄分组、状态等)。
-
表的数据量 :对于相同的索引字段,表的数据量越大,索引过滤性的影响越明显。例如,在 100 条记录的表中,
sex = '男'
可能筛选出 50 条记录,Rows_examined
为 50,查询效率影响不大;但在 5000 万条记录的表中,同样的筛选条件会导致Rows_examined
高达 2500 万,查询效率急剧下降。 -
表设计结构 :表的字段设计是否合理也会影响索引过滤性。例如,若将 "用户地址" 字段设计为一个整体(如
address = '北京市海淀区XX街道'
),则无法通过 "城市" 或 "区" 进行精准筛选,索引过滤性较低;若将地址拆分为province
(省份)、city
(城市)、district
(区)三个字段,则可针对不同层级创建联合索引,提升索引过滤性。
2.2.3 案例:通过联合索引提升过滤性
下面通过一个具体案例,说明如何通过优化索引(创建联合索引)提升索引过滤性,从而优化慢查询。
案例背景:
存在一个学生表(student
),包含字段id
(主键)、name
(姓名)、sex
(性别)、age
(年龄),表中包含 100 万条记录。执行查询select * from student where age = 18 and name like '张%';
,通过EXPLAIN
分析发现,该查询未使用任何索引(key
为 NULL),type
为ALL
(全表扫描),Rows_examined
为 100 万,执行时间较长,属于慢查询。
优化过程:
-
第一次优化:创建单一字段索引
首先,尝试为
name
字段创建索引,执行命令:
SQL
alter table student add index idx_name (name);
再次执行 EXPLAIN select * from student where age = 18 and name like '张%';
分析,发现 key
字段的值变为 idx_name
(说明使用了 name
索引),但 Rows_examined
仍高达 3 万条(假设表中有 3 万条姓名以 "张" 开头的记录)。这是因为 name
索引仅能筛选出姓名以 "张" 开头的记录,却无法进一步筛选年龄为 18 的记录,需要在索引筛选后,对这 3 万条记录进行 "回表查询"(通过索引中的主键 ID 到主键索引中查询完整数据),再过滤出年龄为 18 的记录,索引过滤性依然较低,查询效率提升有限。
-
第二次优化:创建联合索引
为了同时利用
age
和name
两个字段的筛选能力,提升索引过滤性,我们创建(age, name)
联合索引,执行命令:
sql
alter table student add index idx_name (name);
再次执行 EXPLAIN
分析,结果显示:
-
key
字段的值为idx_age_name
(使用了联合索引); -
type
字段的值为range
(表示通过索引范围查询筛选数据); -
Rows_examined
降至 500 条(假设表中年龄为 18 且姓名以 "张" 开头的记录仅 500 条)。
这是因为联合索引 (age, name)
遵循 "最左前缀原则",MySQL 会先通过 age = 18
筛选出所有年龄为 18 的记录,再在这些记录中通过 name like '张%'
进一步筛选,大幅减少了扫描的数据行数,索引过滤性显著提升。此时,查询执行时间从原来的几秒缩短至几十毫秒,优化效果明显。
-
第三次优化:利用虚拟列创建更精准的联合索引
虽然
(age, name)
联合索引已大幅提升效率,但name like '张%'
仍属于范围查询,若想进一步优化筛选精度,可利用 MySQL 5.7 及以上版本支持的 "虚拟列" 功能,提取姓名的第一个字作为独立字段,再创建联合索引。
首先,为 student
表添加虚拟列 first_name
(存储姓名的第一个字),并创建 (first_name, age)
联合索引,执行命令:
sql
alter table student add first_name varchar(2) generated always as (left(name, 1)),
add index idx_firstname_age (first_name, age);
此时,将查询语句调整为:
sql
select * from student where first_name = '张' and age = 18;
执行 EXPLAIN
分析,结果显示:
-
key
字段的值为idx_firstname_age
(使用了虚拟列联合索引); -
type
字段的值为ref
(表示通过非唯一索引精确匹配查询); -
Rows_examined
仅为 500 条(与第二次优化结果一致,但查询语句更简洁,索引匹配更精准)。
虚拟列 first_name
直接存储了姓名的第一个字,避免了 like
范围查询的性能损耗,同时联合索引 (first_name, age)
能通过两个字段的精确匹配快速定位数据,进一步提升了查询效率。
2.3 慢查询原因总结
通过对慢查询案例的分析,我们可以总结出慢查询的常见原因,为后续优化提供明确方向:
-
全表扫描(type = ALL)
当
EXPLAIN
分析结果中type
字段的值为ALL
时,表示查询进行了全表扫描,需要遍历表中的所有记录才能找到符合条件的数据。全表扫描在数据量较小时影响不明显,但在百万级、千万级数据量的表中,会导致Rows_examined
极大,查询执行时间急剧增加。常见场景 :未创建合适的索引、索引失效(如使用
or
连接非索引字段、在索引字段上使用函数运算等)。 -
全索引扫描(type = index)
当
type
字段的值为index
时,表示查询进行了全索引扫描,需要遍历整个索引树才能找到符合条件的数据。虽然全索引扫描比全表扫描减少了数据读取量(仅读取索引数据,不读取表数据),但本质上仍是 "遍历所有数据",在索引数据量较大时,性能依然很差。常见场景 :查询条件匹配了索引中的所有记录(如
select id from user where id > 0
,id
为主键索引)、使用了覆盖索引但筛选条件过于宽泛。 -
索引过滤性不好
索引过滤性不好会导致
Rows_examined
远大于Rows_sent
,即使使用了索引,仍需扫描大量数据。索引过滤性主要与索引字段选型、表的数据量、表设计结构相关:
-
索引字段选型不当 :选择取值范围窄、重复度高的字段(如
sex
、status
)创建单独索引; -
表数据量过大 :在千万级数据量的表中,即使索引过滤性中等,
Rows_examined
依然会很高; -
表设计不合理:字段设计过于冗余(如将多个信息存储在一个字段中),无法通过索引精准筛选数据。
-
频繁的回表查询开销
回表查询是指查询使用了非主键索引(二级索引),但需要获取表中的完整数据,此时需要先通过二级索引找到主键 ID,再到主键索引(聚簇索引)中查询完整数据,两次索引查询会增加性能开销。
常见场景 :使用
select *
查询所有字段(若二级索引不包含所有查询字段,必须回表)、二级索引设计不合理(未包含常用查询字段)。优化方案 :尽量避免使用
select *
,只查询需要的字段;创建覆盖索引(索引中包含查询所需的所有字段),避免回表查询。
三、分页查询优化:解决 "越往后查越慢" 的问题
分页查询是业务开发中最常见的场景之一(如商品列表分页、订单列表分页),通常使用 LIMIT
子句实现。但在数据量较大的表中,传统的分页查询会出现 "越往后查越慢" 的问题,需要通过针对性的优化提升性能。
3.1 LIMIT
子句的工作原理
3.1.1 LIMIT
子句的语法格式
LIMIT
子句用于限制查询结果的返回数量,语法格式如下:
sql
SELECT * FROM 表名 LIMIT [offset, ] rows;
-
offset
:指定第一个返回记录行的偏移量(从 0 开始),可选参数; -
rows
:指定返回记录行的最大数目,必选参数。
示例:
-
select * from user limit 10;
:返回表中前 10 条记录(offset 默认为 0); -
select * from user limit 100, 20;
:返回表中从第 101 条记录开始的 20 条记录(offset = 100,rows = 20)。
3.1.2 一般性分页的性能问题
为了探究分页查询的性能瓶颈,我们通过两个实验分析 offset
和 rows
对执行时间的影响(实验基于 100 万条数据的 user
表,id
为主键):
实验 1:固定 offset,调整 rows
执行以下查询,观察执行时间变化:
sql
select * from user limit 10000, 1; -- 偏移量 10000,返回 1 条记录
select * from user limit 10000, 10; -- 偏移量 10000,返回 10 条记录
select * from user limit 10000, 100; -- 偏移量 10000,返回 100 条记录
select * from user limit 10000, 1000;-- 偏移量 10000,返回 1000 条记录
实验结果 :当 offset
固定为 10000 时,返回记录数(rows
)低于 100 条时,查询时间基本稳定在 50ms 左右;当 rows
超过 100 条后,查询时间随 rows
增大而线性增加(如 rows = 1000
时,查询时间增至 200ms)。
实验 2:固定 rows,调整 offset
执行以下查询,观察执行时间变化:
sql
select * from user limit 100, 10; -- 偏移量 100,返回 10 条记录
select * from user limit 1000, 10; -- 偏移量 1000,返回 10 条记录
select * from user limit 10000, 10; -- 偏移量 10000,返回 10 条记录
select * from user limit 100000, 10; -- 偏移量 100000,返回 10 条记录
实验结果 :当 rows
固定为 10 时,offset
低于 100 时,查询时间约为 10ms;当 offset
超过 100 后,查询时间随 offset
增大而急剧增加(如 offset = 100000
时,查询时间增至 500ms)。
3.1.3 性能问题根源
传统分页查询 "越往后查越慢" 的核心原因是 LIMIT
子句的工作机制:当执行 LIMIT offset, rows
时,MySQL 会从表的第一条记录开始扫描,跳过前 offset
条记录,然后返回后续的 rows
条记录。也就是说,即使只需要返回 10 条记录,若 offset = 100000
,MySQL 仍需扫描前 100010 条记录,扫描行数(Rows_examined
)随 offset
增大而线性增加,导致查询时间急剧增加。
3.2 分页优化方案
针对传统分页查询的性能问题,我们可以通过以下两种方案进行优化,核心思路是 "利用索引定位数据,减少扫描行数":
3.2.1 方案一:利用覆盖索引优化
覆盖索引是指索引中包含查询所需的所有字段,此时 MySQL 无需回表查询,直接从索引中获取数据即可。在分页查询中,若使用覆盖索引定位到 offset
对应的主键 ID,再通过主键 ID 查询完整数据,可大幅减少扫描行数。
优化步骤:
-
通过覆盖索引获取
offset
对应的主键 ID :利用主键索引(或包含主键的联合索引),快速定位到第offset + 1
条记录的主键 ID,此时Rows_examined = offset + 1
; -
通过主键 ID 查询完整数据 :利用主键索引的快速定位能力,查询
id >= 目标ID
的rows
条记录,此时Rows_examined = rows
。
示例:
传统分页查询(慢查询):
sql
select * from user limit 100000, 10; -- Rows_examined = 100010,执行时间约 500ms
优化后查询:
sql
-- 步骤 1:获取第 100001 条记录的主键 ID(假设为 100001)
select id from user limit 100000, 1; -- 覆盖索引查询,Rows_examined = 100001,执行时间约 50ms
-- 步骤 2:通过主键 ID 查询完整数据
select * from user where id >= 100001 limit 10; -- 主键索引查询,Rows_examined = 10,执行时间约 10ms
优化效果 :总执行时间从 500ms 降至 60ms,Rows_examined
从 100010 降至 100011(看似增加,但覆盖索引查询的 "扫描成本" 远低于全表扫描,且主键查询几乎无性能损耗)。
3.2.2 方案二:利用子查询优化
利用子查询可以将 "获取目标主键 ID" 和 "查询完整数据" 两个步骤合并为一条 SQL 语句,简化操作的同时,保持与方案一相同的优化效果。
优化示例:
sql
select * from user
where id >= (select id from user limit 100000, 1)
limit 10;
执行逻辑:
-
子查询
select id from user limit 100000, 1
利用覆盖索引获取第 100001 条记录的主键 ID; -
主查询
where id >= 目标ID limit 10
利用主键索引查询完整数据。
优化效果:与方案一一致,总执行时间约 60ms,且无需手动拆分 SQL 语句,使用更便捷。
3.2.3 注意事项
- 主键连续性 :以上两种优化方案依赖于主键 ID 的连续性。若主键 ID 存在断层(如删除过记录),可能导致查询结果缺失。此时可通过 "基于上一页最后一条记录的 ID 进行分页" 替代
offset
分页,例如:
sql
-- 上一页最后一条记录的 ID 为 100000
select * from user where id > 100000 limit 10; -- 无需 offset,直接通过 ID 定位,Rows_examined = 10
- 非主键索引场景 :若分页查询基于非主键字段排序(如
order by create_time
),可创建包含排序字段和主键的联合索引(如idx_create_time_id
),再通过类似方案优化:
sql
select * from user
where id >= (select id from user order by create_time limit 100000, 1)
order by create_time
limit 10;
联合索引 idx_create_time_id
确保子查询可通过覆盖索引获取目标 ID,避免全表扫描。