MySQL 进阶篇:SQL优化全解析

目录

前言

一、插入数据优化

[1.1 普通 INSERT 语句优化](#1.1 普通 INSERT 语句优化)

(1)批量插入数据

(2)手动控制事务

(3)主键顺序插入

[1.2 大批量插入数据优化](#1.2 大批量插入数据优化)

[(1)LOAD 指令优化步骤](#(1)LOAD 指令优化步骤)

(2)核心注意点

二、主键优化

[2.1 InnoDB 的数据组织方式](#2.1 InnoDB 的数据组织方式)

[2.2 页分裂](#2.2 页分裂)

[2.3 页合并](#2.3 页合并)

[2.4 主键设计原则](#2.4 主键设计原则)

[三、ORDER BY 优化](#三、ORDER BY 优化)

[3.1 MySQL 的两种排序方式](#3.1 MySQL 的两种排序方式)

[3.2 ORDER BY 优化核心原则](#3.2 ORDER BY 优化核心原则)

(1)为排序字段建立合适的索引,多字段排序遵循最左前缀法则

(2)尽量使用覆盖索引

(3)多字段升降序排序,需匹配联合索引的ASC/DESC定义

[(4)不可避免的 filesort,增大排序缓冲区大小](#(4)不可避免的 filesort,增大排序缓冲区大小)

[3.3 注意点](#3.3 注意点)

[四、GROUP BY 优化](#四、GROUP BY 优化)

[4.1 分组操作的性能问题](#4.1 分组操作的性能问题)

[4.2 GROUP BY 优化原则](#4.2 GROUP BY 优化原则)

[五、LIMIT 优化](#五、LIMIT 优化)

[5.1 LIMIT 分页的性能问题](#5.1 LIMIT 分页的性能问题)

[5.2 LIMIT 优化方案:覆盖索引 + 子查询](#5.2 LIMIT 优化方案:覆盖索引 + 子查询)

[六、COUNT 优化](#六、COUNT 优化)

[6.1 MyISAM 与 InnoDB 的 COUNT 区别](#6.1 MyISAM 与 InnoDB 的 COUNT 区别)

[6.2 COUNT 的四种用法及效率对比](#6.2 COUNT 的四种用法及效率对比)

效率排序

核心推荐

[6.3 大数据量 COUNT 的终极优化](#6.3 大数据量 COUNT 的终极优化)

[七、UPDATE 优化](#七、UPDATE 优化)

[7.1 InnoDB 的锁机制核心点](#7.1 InnoDB 的锁机制核心点)

[7.2 正面与反面示例](#7.2 正面与反面示例)

高效示例(使用主键索引,行锁)

低效示例(无索引,表锁)

[7.3 UPDATE 优化原则](#7.3 UPDATE 优化原则)

[八、SQL 优化核心总结](#八、SQL 优化核心总结)


前言

在 MySQL 数据库的日常使用与生产环境部署中,SQL 语句的执行效率直接决定了数据库的性能表现,尤其是在大数据量、高并发的业务场景下,一个未优化的 SQL 可能导致整个系统的响应变慢、资源耗尽。本文将基于 MySQL 的 SQL 优化核心知识点,从插入数据、主键设计、排序、分组、分页、统计、更新七个核心维度,详细讲解 SQL 优化的具体方法、底层原理和实操细节,覆盖开发与运维过程中必备的调优技巧,做到极致细节、易学易用。

一、插入数据优化

插入数据是数据库最基础的操作之一,看似简单的INSERT语句,在大数据量场景下如果不做优化,会产生大量的磁盘 IO 和事务开销,导致插入效率极低。插入优化主要分为普通 INSERT 优化大批量插入数据优化 两类,核心思路是减少事务次数、降低磁盘 IO、保证主键顺序

1.1 普通 INSERT 语句优化

针对单表少量多次的插入场景,主要有三种优化方式,可组合使用:

(1)批量插入数据

避免循环执行单条INSERT语句,改为批量插入,减少客户端与数据库的网络交互次数,同时降低事务提交的开销。低效写法

复制代码
insert into tb_test values(1,'tom');
insert into tb_test values(2,'cat');
insert into tb_test values(3,'jerry');

高效写法

复制代码
Insert into tb_test values(1,'Tom'),(2,'Cat'),(3,'Jerry');
(2)手动控制事务

MySQL 默认开启自动提交事务(autocommit=1),每执行一条INSERT都会触发一次事务提交,产生大量的日志刷盘操作。手动开启事务,批量执行插入后再提交,可大幅减少事务提交次数。

复制代码
start transaction;
insert into tb_test values(1,'Tom'),(2,'Cat'),(3,'Jerry');
insert into tb_test values(4,'Lucy'),(5,'Lily'),(6,'Jack');
commit;
(3)主键顺序插入

InnoDB 存储引擎是索引组织表(IOT) ,数据行按照主键顺序存储在聚集索引的叶子节点上。主键顺序插入 会直接在现有页的末尾追加数据,而乱序插入 会导致页分裂(后续主键优化会详细讲解),耗费大量性能。

  • 顺序插入(推荐):1 2 3 4 5 7 8 9 15 21
  • 乱序插入(避免):8 1 9 21 88 2 4 15 89 5

1.2 大批量插入数据优化

当需要一次性插入百万级甚至千万级 数据时,使用INSERT语句会出现性能瓶颈,此时推荐使用 MySQL 的LOAD指令进行数据导入,相比INSERT效率提升数倍。

(1)LOAD 指令优化步骤
  1. 客户端连接数据库时,添加--local-infile参数,开启本地文件导入功能:

    mysql --local-infile -u root -p

  2. 开启全局参数local_infile=1,允许从本地加载文件:

    set global local_infile = 1;

  3. 执行LOAD指令导入数据,指定字段分隔符、行分隔符:

    load data local infile '/root/sql1.log' into table tb_user
    fields terminated by ',' # 字段之间用逗号分隔
    lines terminated by '\n' ; # 行之间用换行符分隔

(2)核心注意点

使用LOAD指令时,主键顺序插入的性能远高于乱序插入 ,原因与普通 INSERT 一致,乱序插入会触发频繁的页分裂,导致磁盘 IO 激增。测试表明,使用LOAD指令顺序插入 100 万条数据仅需 17 秒左右,而乱序插入耗时会翻倍。

二、主键优化

主键是 InnoDB 表的核心,不仅是唯一标识行数据的字段,更是 InnoDB 聚集索引的组成部分,主键的设计和插入方式直接影响数据库的插入、查询、更新 性能。主键优化的核心是理解 InnoDB 的数据组织方式,避免页分裂,减少页合并的性能损耗

2.1 InnoDB 的数据组织方式

InnoDB 表是索引组织表(IOT) ,所有数据行都存储在聚集索引的叶子节点上,而聚集索引默认由主键构建。InnoDB 的磁盘存储以页(Page) 为最小单元,默认页大小为 16K,每个页存储若干行数据,页与页之间通过指针形成链表,数据按照主键顺序在页中排列。

2.2 页分裂

页分裂是主键乱序插入时的核心性能问题,当新插入的主键值需要存放在某个已满的页中时,InnoDB 会开辟一个新页,将原页中部分数据迁移到新页,同时调整页之间的指针,这个过程就是页分裂,会产生大量的磁盘 IO 和数据迁移开销。

  • 主键顺序插入:数据会在当前页的末尾持续追加,当页满时直接开辟新页,无数据迁移,无页分裂;
  • 主键乱序插入:新数据无法在当前页追加,触发页分裂,迁移数据 + 调整指针,性能大幅下降。

2.3 页合并

当删除 InnoDB 表中的数据时,数据不会被物理删除,只是被标记为删除,其占用的空间会被标记为可复用。当一个页中被标记为删除的记录达到MERGE_THRESHOLD(默认页的 50%) 时,InnoDB 会寻找相邻的页,将两个页合并为一个页,释放空闲空间,这个过程就是页合并

页合并本身是对空闲空间的合理利用,但频繁的删除操作会触发大量页合并,产生额外的磁盘 IO,因此在业务中应尽量避免大规模的批量删除,若需删除可采用分批删除的方式。

2.4 主键设计原则

结合 InnoDB 的底层原理,主键设计需遵循以下 4 个核心原则,缺一不可:

  1. 缩短主键长度:InnoDB 的二级索引叶子节点存储的是主键值,主键长度越短,二级索引占用的磁盘空间越小,查询时的磁盘 IO 越少;
  2. 优先顺序插入:使用自增主键实现顺序插入,避免乱序插入触发页分裂;
  3. 使用自增主键(AUTO_INCREMENT):自增主键是实现顺序插入的最佳方式,避免使用 UUID、身份证号等自然主键(UUID 是乱序的,会触发频繁页分裂);
  4. 禁止修改主键:主键是 InnoDB 表数据组织的核心,修改主键相当于删除原数据 + 插入新数据,会触发页分裂 + 二级索引更新,性能损耗极大。

三、ORDER BY 优化

排序操作是业务中高频的 SQL 操作,MySQL 的排序分为Using filesortUsing index 两种方式,二者的性能差距天差地别,优化的核心目标是避免 Using filesort,让排序操作通过索引完成(Using index)

3.1 MySQL 的两种排序方式

排序方式 执行原理 性能
Using filesort 先通过全表扫描 / 索引扫描获取数据,再在排序缓冲区(sort buffer) 中完成排序,属于 "额外排序"
Using index 直接通过有序的索引顺序扫描返回数据,无需额外排序,索引本身就是有序的

3.2 ORDER BY 优化核心原则

(1)为排序字段建立合适的索引,多字段排序遵循最左前缀法则

单字段排序为字段建立单列索引,多字段排序为字段建立联合索引,且联合索引的字段顺序需与排序字段顺序一致,遵循索引的最左前缀法则,否则会触发 Using filesort。

(2)尽量使用覆盖索引

排序时,查询的字段仅包含索引字段,无需回表查询数据,既减少了磁盘 IO,又能让排序直接通过索引完成,是排序优化的最优解。

(3)多字段升降序排序,需匹配联合索引的ASC/DESC定义

若多字段排序时存在一个升序、一个降序,则创建联合索引时需显式指定字段的升降序,否则索引失效,触发 Using filesort。例如:

sql

复制代码
-- 需求:按age升序、phone降序排序,创建匹配的联合索引
create index idx_user_age_phone_ad on tb_user(age asc ,phone desc);
-- 此时执行排序SQL,会使用Using index,无额外排序
explain select id,age,phone from tb_user order by age asc , phone desc ;
(4)不可避免的 filesort,增大排序缓冲区大小

若业务场景无法通过索引优化排序(如多字段无序排序),可适当增大sort_buffer_size(默认 256K),让更多数据在内存中完成排序,减少磁盘临时文件的排序开销。

3.3 注意点

MySQL8.0 支持降序索引 ,而 MySQL5.7 及以下版本的索引默认是升序的,若进行降序排序会出现Backward index scan(反向扫描索引),虽仍为 Using index,但性能略低于正向扫描,因此在 5.7 版本中建议排序方式与索引顺序一致。

四、GROUP BY 优化

分组操作的优化思路与排序操作高度相似,核心是利用索引减少分组时的临时表创建,遵循最左前缀法则

4.1 分组操作的性能问题

若分组字段未建立索引,MySQL 会先通过全表扫描获取数据,再创建临时表 完成分组统计,执行计划中会出现Using temporary,临时表的创建和销毁会产生额外的性能损耗。

4.2 GROUP BY 优化原则

  1. 为分组字段建立索引:索引是有序的,可直接通过索引扫描完成分组,避免创建临时表;
  2. 多字段分组遵循最左前缀法则:多字段分组时,需为分组字段建立联合索引,且联合索引的字段顺序与分组字段顺序一致,否则索引失效。

示例 :为profession,age,status创建联合索引后,执行以下分组 SQL:

复制代码
-- 遵循最左前缀,使用索引,无Using temporary
explain select profession , count(*) from tb_user group by profession ;
-- 遵循最左前缀,使用索引,无Using temporary
explain select profession,age , count(*) from tb_user group by profession,age ;
-- 不遵循最左前缀,索引失效,出现Using temporary
explain select age , count(*) from tb_user group by age ;

五、LIMIT 优化

LIMIT 分页是业务中必备的功能,但在大数据量场景下(如百万级、千万级数据)LIMIT offset, size的分页方式会出现越往后翻页,查询越慢 的问题,优化的核心思路是利用覆盖索引减少数据排序和扫描的范围

5.1 LIMIT 分页的性能问题

当执行LIMIT 2000000, 10时,MySQL 需要扫描前 2000010 条数据,然后丢弃前 2000000 条,仅返回最后 10 条,扫描和排序的开销极大,这是导致分页变慢的核心原因。

5.2 LIMIT 优化方案:覆盖索引 + 子查询

利用主键索引作为覆盖索引,先通过子查询获取分页后的主键 ID,再通过主键 ID 关联表查询完整数据,此时 MySQL 仅需扫描主键索引的少量数据,大幅减少扫描范围。

优化前(低效)

复制代码
-- 扫描大量数据,耗时久
select * from tb_sku limit 2000000,10;

优化后(高效)

复制代码
-- 子查询通过主键索引获取分页ID(覆盖索引,扫描量小),再关联表查询
explain select * from tb_sku t , (select id from tb_sku order by id limit 2000000,10) a where t.id = a.id;

该方案的核心是主键索引是聚集索引,扫描效率极高,子查询仅需扫描主键索引的 2000010 条数据,而原 SQL 需要扫描全表的 2000010 条数据,磁盘 IO 大幅减少。

六、COUNT 优化

COUNT(*)是统计表中行数的高频操作,在大数据量表中,直接执行COUNT(*)会耗时极长,优化的核心是理解不同存储引擎的 COUNT 实现,选择最高效的 COUNT 用法

6.1 MyISAM 与 InnoDB 的 COUNT 区别

MySQL 中不同存储引擎对COUNT(*)的实现方式不同,性能差距显著:

  • MyISAM :将表的总行数存储在磁盘上,执行COUNT(*)时直接返回该数值,效率极高;但带条件的 COUNT仍需全表扫描,效率低下;
  • InnoDB :没有在磁盘上存储表的总行数,执行COUNT(*)时需要逐行扫描表,统计符合条件的行数,大数据量表中耗时极长。

6.2 COUNT 的四种用法及效率对比

COUNT()是聚合函数,其参数为NULL 时不计数,非 NULL 时计数 ,常用的用法有COUNT(主键)COUNT(字段)COUNT(1)COUNT(*),各自的执行原理和效率不同:

COUNT 用法 执行原理 效率
COUNT (主键) 遍历全表,取出每行的主键值(主键非 NULL),逐行计数 较低
COUNT (字段) 若字段无 NOT NULL 约束:遍历全表,取出字段值,判断是否为 NULL,非 NULL 则计数;若字段有 NOT NULL 约束:遍历全表,直接取字段值计数 最低
COUNT(1) 遍历全表,不取出任何字段,仅为每行分配一个数字 1,逐行计数 较高
COUNT(*) InnoDB 专属优化,遍历全表,不取出任何字段,直接逐行计数,是 InnoDB 对 COUNT 的最优实现 最高(与 COUNT (1) 基本持平)
效率排序

COUNT(字段) < COUNT(主键) < COUNT(1) ≈ COUNT(*)

核心推荐

在 InnoDB 表中,无论是否带条件,都优先使用 COUNT (*),这是 MySQL 官方推荐的用法,也是效率最高的用法。

6.3 大数据量 COUNT 的终极优化

若业务中需要频繁统计全表行数,且对实时性要求不是极高,可采用外部存储统计结果的方式:

  1. 使用Redis等内存数据库存储表的总行数,插入 / 删除数据时更新 Redis 中的数值,查询时直接从 Redis 获取,效率极高;
  2. 建立统计明细表 ,通过定时任务(如 crontab)执行COUNT(*)并将结果存入统计表,业务查询时直接读取统计表。

七、UPDATE 优化

UPDATE 操作的优化核心是避免 InnoDB 的行锁升级为表锁,行锁的粒度小,并发性能高,而表锁的粒度大,会导致并发更新时的阻塞,性能大幅下降。

7.1 InnoDB 的锁机制核心点

InnoDB 的行锁是针对索引加的锁,而非针对记录加的锁 ,如果 UPDATE 语句的WHERE 条件字段没有建立索引,或索引失效 ,InnoDB 会无法精准定位到需要更新的行,此时会将行锁升级为表锁,导致整个表被锁定,其他事务无法对该表进行任何更新操作,并发性能极差。

7.2 正面与反面示例

高效示例(使用主键索引,行锁)
复制代码
-- WHERE条件为主键,使用主键索引,加行锁,仅锁定id=1的行,并发性能高
update course set name = 'javaEE' where id = 1 ;
低效示例(无索引,表锁)
复制代码
-- WHERE条件为name,name无索引,索引失效,行锁升级为表锁,锁定整个course表,并发性能低
update course set name = 'SpringBoot' where name = 'PHP';

7.3 UPDATE 优化原则

  1. UPDATE 的 WHERE 条件字段必须建立索引,确保 InnoDB 能通过索引精准定位到需要更新的行,加行锁;
  2. 避免 WHERE 条件中的索引失效,如索引列运算、字符串不加引号、模糊查询前加 % 等,防止行锁升级为表锁;
  3. 尽量批量更新,减少事务次数,但批量更新的行数不宜过多,避免长时间持有行锁导致其他事务阻塞。

八、SQL 优化核心总结

MySQL 的 SQL 优化并非孤立的知识点,而是围绕减少磁盘 IO、利用索引、降低锁粒度、减少额外开销这四大核心思路展开的,本文讲解的七大优化维度可总结为以下关键要点:

  1. 插入优化:批量插入、手动控事务、主键顺序插入,大数据量用 LOAD 指令;
  2. 主键优化:自增主键、缩短长度、顺序插入、禁止修改,避免页分裂;
  3. 排序 / 分组优化:为排序 / 分组字段建立索引,遵循最左前缀,使用覆盖索引;
  4. 分页优化:大数据量分页使用 "覆盖索引 + 子查询",减少数据扫描范围;
  5. 统计优化:InnoDB 优先用 COUNT (*),大数据量用 Redis / 统计表做外部统计;
  6. 更新优化:WHERE 条件字段建索引,避免行锁升级为表锁。
相关推荐
小高不会迪斯科8 小时前
CMU 15445学习心得(二) 内存管理及数据移动--数据库系统如何玩转内存
数据库·oracle
寻寻觅觅☆8 小时前
东华OJ-基础题-106-大整数相加(C++)
开发语言·c++·算法
偷吃的耗子8 小时前
【CNN算法理解】:三、AlexNet 训练模块(附代码)
深度学习·算法·cnn
e***8908 小时前
MySQL 8.0版本JDBC驱动Jar包
数据库·mysql·jar
l1t8 小时前
在wsl的python 3.14.3容器中使用databend包
开发语言·数据库·python·databend
青云计划9 小时前
知光项目知文发布模块
java·后端·spring·mybatis
赶路人儿9 小时前
Jsoniter(java版本)使用介绍
java·开发语言
化学在逃硬闯CS9 小时前
Leetcode1382. 将二叉搜索树变平衡
数据结构·算法
ceclar1239 小时前
C++使用format
开发语言·c++·算法
探路者继续奋斗9 小时前
IDD意图驱动开发之意图规格说明书
java·规格说明书·开发规范·意图驱动开发·idd