【MySQL | 第六篇】 SQL 优化

一、前言

当我们学会通过慢查询日志、EXPLAIN 或执行耗时去定位慢 SQL 之后,下一步就要分析 SQL 的慢点,并根据执行过程选择合适的优化方向。

二、insert 插入优化

insert 优化主要解决的是大量数据写入时速度慢的问题。如果只是偶尔插入一两条数据,通常不需要特别处理;但如果是批量导入、初始化数据、日志落库,就需要注意写入方式。

1. 批量插入

单条插入的写法通常是这样:

sql 复制代码
insert into tb_user(name, age, phone) values ('张三', 18, '13800000000');
insert into tb_user(name, age, phone) values ('李四', 20, '13900000000');

如果数据量比较大,每一条 SQL 都要经历解析、优化、执行、提交等过程,整体效率会比较低。

可以改成批量插入:

sql 复制代码
insert into tb_user(name, age, phone)
values
    ('张三', 18, '13800000000'),
    ('李四', 20, '13900000000'),
    ('王五', 22, '13700000000');

这样可以减少客户端和 MySQL 服务端之间的交互次数,也能减少 SQL 解析次数。

不过一次插入的数据不建议无限堆叠。一般来说,可以控制在 1000 条以内,具体还要看字段数量、单行大小和数据库配置。

2. 手动提交事务

如果开启了自动提交,每执行一条 insert,MySQL 都可能提交一次事务。大量数据插入时,频繁提交事务会带来额外开销。

可以使用手动事务:

sql 复制代码
start transaction;

insert into tb_user(name, age, phone)
values
    ('张三', 18, '13800000000'),
    ('李四', 20, '13900000000');

commit;

这样可以把多次插入放到一个事务中统一提交,减少提交次数。

需要注意的是,事务也不是越大越好。如果一次事务里塞入过多数据,可能会导致锁持有时间变长、回滚成本变高、日志压力增大。所以实际开发中要分批处理。

3. 主键顺序插入

InnoDB 表的数据是按照主键组织的。如果主键是自增的,插入时通常会追加到后面,页分裂较少,性能比较稳定。

如果主键是乱序的,比如使用完全随机的字符串主键,插入时可能需要在 B+Tree 的中间位置插入数据,这时就可能导致页分裂。

简单理解就是:原来一个数据页里已经按顺序存了一批数据,现在突然要往中间插入一条新数据,如果当前页空间不够,就需要拆分数据页,这个过程会带来额外的移动和维护成本。

所以在没有特殊业务要求时,主键尽量选择顺序增长的方式,例如自增主键,或者使用能保证趋势递增的分布式 ID。

4. 大量数据导入使用 load data

如果是特别大的数据导入,可以考虑使用 load data ,它通常比一条条 insert 更适合导入文件数据。

客户端连接服务端时,可以加上 --local-infile 参数:

bash 复制代码
mysql --local-infile -u root -p

然后查看并开启本地文件导入:

sql 复制代码
select @@local_infile;
set global local_infile = 1;

再把本地文件导入到表中:

sql 复制代码
load data local infile '/root/sql1.log'
into table tb_user
fields terminated by ','
lines terminated by '\n';

这里的意思是:

  • fields terminated by ',':每一列数据使用逗号分隔
  • lines terminated by '\n':每一行数据使用换行分隔
  • into table tb_user:导入到 tb_user 表中

这个方式适合结构比较规整的大批量数据导入。使用前要确认文件格式、字段顺序、字符集以及测试环境导入结果,避免直接在生产环境误导入错误数据。

三、主键优化

主键优化重点在于理解 InnoDB 的数据组织方式。

InnoDB 中,表数据会按照主键构建聚簇索引。也就是说,主键索引的叶子节点保存的是完整行数据。因此主键的设计会直接影响数据插入、查询和索引维护。

1. 尽量使用顺序主键

顺序主键的优势是插入位置比较稳定,一般都是向后追加。这样 B+Tree 的维护成本更低。

比如:

sql 复制代码
id: 1, 2, 3, 4, 5

这种顺序插入比较符合 InnoDB 的存储特点。

如果是乱序插入:

text 复制代码
1, 3, 2

新数据可能插入到已有数据中间。当数据页空间不足时,就可能触发页分裂。

2. 删除数据后的页合并

删除数据时,并不是一定会立刻释放整个数据页。如果页内数据减少到一定比例,比如低于一半,就可能触发页合并。

所以频繁的随机插入和删除,会让索引页不断分裂、合并,增加维护成本。

所以在设计主键时,不能只考虑唯一性,还要考虑它对索引结构的影响。

四、order by 排序优化

order by 是非常常见的性能问题来源。

排序一般有两种情况:

  • 直接利用索引顺序返回结果,通常性能较好
  • 不能利用索引,需要额外排序,也就是常见的 filesort

1. 使用联合索引优化排序

假设经常按照 agephone 排序,可以创建联合索引:

sql 复制代码
create index idx_user_age_phone on tb_user(age, phone);

查询时:

sql 复制代码
select id, age, phone
from tb_user
order by age, phone;

如果执行计划能用上 idx_user_age_phone,MySQL 就可以按照索引本身的顺序读取数据,减少额外排序成本。

2. 注意排序字段顺序

联合索引要遵守最左前缀法则。比如索引是:

sql 复制代码
(age, phone)

那么下面这种排序更容易利用索引:

sql 复制代码
order by age, phone

如果只按照 phone 排序,就不一定能直接使用这个联合索引完成排序。

3. filesort 不一定是坏事

看到 filesort 不代表 SQL 一定有问题,它只是说明 MySQL 使用了额外排序。真正需要关注的是:

  • 数据量的大小
  • 排序结果的耗时
  • 索引对排序成本的降低效果
  • 对核心业务接口的影响

如果排序字段确实无法建立合适索引,或者排序数据量很小,filesort 也可能是可以接受的。

当它成为性能瓶颈时,再考虑优化索引或者调整 sort_buffer_size 等参数。参数优化应该放在后面,不能一上来就改配置。

五、group by 分组优化

group by 的优化思路和 order by 有相似之处,也可以考虑通过索引降低分组成本。

比如经常按照部门、状态统计数据:

sql 复制代码
select dept_id, status, count(*)
from tb_user
where dept_id = 1
group by dept_id, status;

可以考虑建立联合索引:

sql 复制代码
create index idx_user_dept_status on tb_user(dept_id, status);

这样 where 条件group by 字段能够尽量匹配索引顺序,更容易减少扫描和分组成本。

需要注意的是,索引不是越多越好。索引会提升查询性能,但也会增加写入、更新和存储成本。所以创建索引前要结合高频 SQL,而不是看到一个字段就加一个索引。

六、limit 深分页优化

分页查询在后台管理系统中非常常见。普通分页一般这样写:

sql 复制代码
select s.*
from tb_sku s
limit 0, 10;

前几页通常问题不大,但深分页就可能很慢:

sql 复制代码
select s.*
from tb_sku s
limit 9000000, 10;

这条 SQL 的含义是跳过前 9000000 条,再返回 10 条。问题在于 MySQL 仍然需要扫描并丢弃前面大量数据,所以越往后翻页越慢。

1. 先查主键,再回表

可以先查询主键,再根据主键回表查询完整数据:

sql 复制代码
select s.*
from tb_sku s,
     (select id from tb_sku order by id limit 9000000, 10) a
where s.id = a.id;

这个写法的核心思想是:子查询只查 id ,如果 id 是主键或索引字段,扫描成本会比直接查询整行数据低。拿到目标页的主键后,再回表查询完整记录。

2. 使用上一页最大 id

如果业务允许,也可以使用"游标分页"的思路:

sql 复制代码
select *
from tb_sku
where id > 9000000
order by id
limit 10;

这种方式不需要跳过大量数据,性能通常更稳定。

不过它也有边界:它更适合"下一页、上一页"这种连续翻页,不适合直接跳转到第 10000 页的场景。

七、count 统计优化

常见的统计写法有:

  • count(*)
  • count(1)
  • count(主键)
  • count(字段)

它们看起来都能统计数量,但细节上有区别。

1. count(字段) 不统计 null

比如:

sql 复制代码
select count(phone)
from tb_user;

如果 phone 字段null,这部分数据不会被统计进去。

所以如果目标是统计整张表的行数,不建议使用可能为 null 的字段。

2. 推荐使用 count(*)

在 InnoDB 中,通常推荐使用:

sql 复制代码
select count(*)
from tb_user;

count(*) 表示统计结果集行数,并不会真的把所有字段取出来。MySQL 优化器会选择合适的方式执行。

所以不要简单认为 count(*) 就一定比 count(1) 慢。实际开发中,统计总行数一般直接使用 count(*) 即可。

八、update 优化:避免锁范围扩大

update 优化不仅关系到速度,还关系到锁。

在 InnoDB 中,如果更新条件能走索引,通常可以锁定更小范围的数据;如果条件没有走索引,就可能扫描更多记录,锁范围也可能随之扩大。

比如根据主键更新:

sql 复制代码
update tb_user
set phone = '13800000000'
where id = 1;

这种写法通常比较安全,因为 id 是主键,能够快速定位到一行数据。

但如果根据一个没有索引的字段更新:

sql 复制代码
update tb_user
set phone = '13800000000'
where name = '张三';

如果 name 没有索引,MySQL 可能需要扫描更多数据。数据量一大,就容易影响并发性能。

所以更新时要注意:

  • where 条件尽量使用索引字段
  • 避免没有条件的全表更新
  • 更新前先用 select 确认影响范围
  • 大批量更新时分批执行,避免长事务

尤其是在生产环境,更新和删除都要非常谨慎。可以先执行:

sql 复制代码
select count(*)
from tb_user
where name = '张三';

确认影响行数符合预期后,再执行 update

九、SQL 优化的整体思路

前面讲的是具体场景,最后再把 SQL 优化的思路串起来。

1. 先定位,再优化

不要凭感觉优化 SQL。建议先通过这些方式定位问题:

  • 慢查询日志
  • EXPLAIN 执行计划
  • 接口耗时
  • 数据量变化
  • 索引命中情况

只有知道慢在哪里,优化才有方向。

2. 优先优化 SQL 和索引

很多性能问题,其实是 SQL 写法和索引设计不合理造成的。

比如:

  • 排序字段没有合适索引
  • 分组字段没有结合查询条件设计联合索引
  • 深分页跳过了大量数据
  • 更新条件没有走索引

这些问题优先通过 SQL 和索引解决,通常比直接改数据库参数更可靠。

3. 参数优化放在后面

sort_buffer_size 这类参数确实可能影响排序性能,但参数优化应该建立在 SQL 和索引已经分析清楚的基础上。

如果 SQL 本身写得不合理,盲目调参数只是暂时掩盖问题。

十、总结

这篇文章主要整理了几个常见 SQL 优化点。

insert 优化 重点是批量插入、手动提交事务、主键顺序插入以及大批量数据导入时使用 load data

主键优化重点是理解 InnoDB 的聚簇索引结构,尽量减少乱序插入带来的页分裂问题。

order bygroup by 的优化重点是合理设计联合索引,让排序和分组尽量利用索引顺序。

limit 深分页的核心问题是跳过大量数据,可以通过先查主键再回表,或者使用游标分页来优化。

count 统计时,一般推荐使用 count(*),同时要注意 count(字段) 不统计 null

update 优化不仅要考虑查询速度,还要考虑锁范围,更新条件尽量使用索引字段,避免全表扫描和长事务。

SQL 优化最终还是要回到一句话:先看执行计划,再结合业务场景优化 SQL、索引和数据访问方式。

相关推荐
j7~1 小时前
【MYSQL】索引特性--详解
数据库·mysql·索引操作·索引的理解·mysql与磁盘·b+树与mysql
ccddsdsdfsdf9 小时前
DBeaver怎么链接mongoDB
数据库·mongodb
丷丩10 小时前
Postgresql基础实践教程(十一)各种Join
数据库·postgresql·join
星夜夏空9911 小时前
FreeRTOS学习(4)——内存映射
数据库·学习·mongodb
TheRouter11 小时前
AI Agent 记忆体系建设实战:短期、长期与工作记忆的工程实现
数据库·人工智能·oracle
Omics Pro11 小时前
首个!外源天然产物综合性代谢图谱
数据库·人工智能·算法·机器学习·r语言
唐青枫12 小时前
MySQL EXISTS 详解:存在性判断、NOT EXISTS 与实战示例
sql·mysql
JAVA面经实录91712 小时前
Hibernate面试题库
数据库·oracle·hibernate
2301_7736436213 小时前
华为云存储实验
网络·mysql·华为云