【MySQL体系】第4篇:MySQL 查询优化实用技巧

文章目录

    • 一、慢查询定位
      • [1.1 开启慢查询日志](#1.1 开启慢查询日志)
      • [1.2 查看慢查询日志](#1.2 查看慢查询日志)
        • [1.2.1 文本方式查看:快速了解日志内容](#1.2.1 文本方式查看:快速了解日志内容)
        • [1.2.2 使用工具分析:高效处理大量日志](#1.2.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.2 分页优化方案](#3.2 分页优化方案)
        • [3.2.1 方案一:利用覆盖索引优化](#3.2.1 方案一:利用覆盖索引优化)
        • [3.2.2 方案二:利用子查询优化](#3.2.2 方案二:利用子查询优化)
        • [3.2.3 注意事项](#3.2.3 注意事项)

在数据库应用中,随着数据量的不断增长,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 影响索引过滤性的因素

索引的过滤性主要受以下三个因素影响:

  1. 索引字段的取值范围:取值范围越广、重复度越低的字段,过滤性越高(如手机号、身份证号、邮箱等唯一字段);取值范围越窄、重复度越高的字段,过滤性越低(如性别、年龄分组、状态等)。

  2. 表的数据量 :对于相同的索引字段,表的数据量越大,索引过滤性的影响越明显。例如,在 100 条记录的表中,sex = '男'可能筛选出 50 条记录,Rows_examined为 50,查询效率影响不大;但在 5000 万条记录的表中,同样的筛选条件会导致Rows_examined高达 2500 万,查询效率急剧下降。

  3. 表设计结构 :表的字段设计是否合理也会影响索引过滤性。例如,若将 "用户地址" 字段设计为一个整体(如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),typeALL(全表扫描),Rows_examined为 100 万,执行时间较长,属于慢查询。

优化过程

  1. 第一次优化:创建单一字段索引

    首先,尝试为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 的记录,索引过滤性依然较低,查询效率提升有限。

  1. 第二次优化:创建联合索引

    为了同时利用 agename 两个字段的筛选能力,提升索引过滤性,我们创建 (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 '张%' 进一步筛选,大幅减少了扫描的数据行数,索引过滤性显著提升。此时,查询执行时间从原来的几秒缩短至几十毫秒,优化效果明显。

  1. 第三次优化:利用虚拟列创建更精准的联合索引

    虽然 (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 慢查询原因总结

通过对慢查询案例的分析,我们可以总结出慢查询的常见原因,为后续优化提供明确方向:

  1. 全表扫描(type = ALL)

    EXPLAIN 分析结果中 type 字段的值为 ALL 时,表示查询进行了全表扫描,需要遍历表中的所有记录才能找到符合条件的数据。全表扫描在数据量较小时影响不明显,但在百万级、千万级数据量的表中,会导致 Rows_examined 极大,查询执行时间急剧增加。

    常见场景 :未创建合适的索引、索引失效(如使用 or 连接非索引字段、在索引字段上使用函数运算等)。

  2. 全索引扫描(type = index)

    type 字段的值为 index 时,表示查询进行了全索引扫描,需要遍历整个索引树才能找到符合条件的数据。虽然全索引扫描比全表扫描减少了数据读取量(仅读取索引数据,不读取表数据),但本质上仍是 "遍历所有数据",在索引数据量较大时,性能依然很差。

    常见场景 :查询条件匹配了索引中的所有记录(如 select id from user where id > 0id 为主键索引)、使用了覆盖索引但筛选条件过于宽泛。

  3. 索引过滤性不好

    索引过滤性不好会导致 Rows_examined 远大于 Rows_sent,即使使用了索引,仍需扫描大量数据。索引过滤性主要与索引字段选型、表的数据量、表设计结构相关:

  • 索引字段选型不当 :选择取值范围窄、重复度高的字段(如 sexstatus)创建单独索引;

  • 表数据量过大 :在千万级数据量的表中,即使索引过滤性中等,Rows_examined 依然会很高;

  • 表设计不合理:字段设计过于冗余(如将多个信息存储在一个字段中),无法通过索引精准筛选数据。

  1. 频繁的回表查询开销

    回表查询是指查询使用了非主键索引(二级索引),但需要获取表中的完整数据,此时需要先通过二级索引找到主键 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 一般性分页的性能问题

为了探究分页查询的性能瓶颈,我们通过两个实验分析 offsetrows 对执行时间的影响(实验基于 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 查询完整数据,可大幅减少扫描行数。

优化步骤

  1. 通过覆盖索引获取 offset对应的主键 ID :利用主键索引(或包含主键的联合索引),快速定位到第 offset + 1 条记录的主键 ID,此时 Rows_examined = offset + 1

  2. 通过主键 ID 查询完整数据 :利用主键索引的快速定位能力,查询 id >= 目标IDrows 条记录,此时 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;

执行逻辑

  1. 子查询 select id from user limit 100000, 1 利用覆盖索引获取第 100001 条记录的主键 ID;

  2. 主查询 where id >= 目标ID limit 10 利用主键索引查询完整数据。

优化效果:与方案一一致,总执行时间约 60ms,且无需手动拆分 SQL 语句,使用更便捷。

3.2.3 注意事项
  1. 主键连续性 :以上两种优化方案依赖于主键 ID 的连续性。若主键 ID 存在断层(如删除过记录),可能导致查询结果缺失。此时可通过 "基于上一页最后一条记录的 ID 进行分页" 替代 offset 分页,例如:
sql 复制代码
-- 上一页最后一条记录的 ID 为 100000
select * from user where id > 100000 limit 10; -- 无需 offset,直接通过 ID 定位,Rows_examined = 10
  1. 非主键索引场景 :若分页查询基于非主键字段排序(如 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,避免全表扫描。

相关推荐
0xDevNull2 小时前
MySQL数据冷热分离详解
后端·mysql
科技小花3 小时前
数据治理平台架构演进观察:AI原生设计如何重构企业数据管理范式
数据库·重构·架构·数据治理·ai-native·ai原生
一江寒逸3 小时前
零基础从入门到精通MySQL(中篇):进阶篇——吃透多表查询、事务核心与高级特性,搞定复杂业务SQL
数据库·sql·mysql
D4c-lovetrain3 小时前
linux个人心得22 (mysql)
数据库·mysql
阿里小阿希3 小时前
CentOS7 PostgreSQL 9.2 升级到 15 完整教程
数据库·postgresql
荒川之神3 小时前
Oracle 数据仓库雪花模型设计(完整实战方案)
数据库·数据仓库·oracle
做个文艺程序员3 小时前
MySQL安全加固十大硬核操作
数据库·mysql·安全
不吃香菜学java4 小时前
Redis简单应用
数据库·spring boot·tomcat·maven
一个天蝎座 白勺 程序猿4 小时前
Apache IoTDB(15):IoTDB查询写回(INTO子句)深度解析——从语法到实战的ETL全链路指南
数据库·apache·etl·iotdb
不知名的老吴4 小时前
Redis的延迟瓶颈:TCP栈开销无法避免
数据库·redis·缓存