【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,避免全表扫描。

相关推荐
oracle04062 小时前
sql练习题单-知识点总结
数据库·sql
lypzcgf3 小时前
Coze源码分析-资源库-编辑数据库-前端源码-核心组件
前端·数据库·源码分析·coze·coze源码分析·ai应用平台·agent平台
wei_shuo3 小时前
KingbaseES聚焦产品上线
数据库·kingbasees
难以触及的高度3 小时前
Linux-CentOS 7 上安装 MySQL 8.0.43(保姆级教程)
linux·mysql·centos
AI浩3 小时前
Redis中的RPOP、BRPOP、LPOP 和 BLPOP
数据库·chrome·redis
数据和云4 小时前
从Databricks和Supabase看AI时代的中国数据库启示
数据库·人工智能
我科绝伦(Huanhuan Zhou)4 小时前
Oracle ADRCI工具全面使用指南:从基础到故障诊断实战
数据库·oracle
数据库生产实战4 小时前
Oracle LOB使用入门和简单使用,提供学习用的测试用例!
数据库·学习·oracle
武子康4 小时前
Java-144 深入浅出 MongoDB BSON详解:MongoDB核心存储格式与JSON的区别与应用场景
java·开发语言·数据库·mongodb·性能优化·json·bjson