前言
在日常开发、课程作业乃至面试中,SQL 的编写能力和优化能力都是核心考察点。很多同学写 SQL 时只追求 “能跑出结果”,却忽略了性能隐患和逻辑陷阱,最终导致接口超时、统计结果错误、数据库服务器负载飙升等问题。
本文整理了 5 个带完整业务场景、前因后果、可直接落地的 SQL 优化实战案例,每个案例都遵循「问题现象→根因分析→优化方案→效果验证→避坑指南」的完整逻辑,既可以直接用于课堂分享,也能作为日常开发的避坑手册,新手也能轻松看懂并复用。
目录
- 技巧 1:杜绝 SELECT * ,精准字段查询是 SQL 优化的第一步
- 技巧 2:NOT IN 子查询避坑,否定查询优先使用 NOT EXISTS
- 技巧 3:大表深度分页优化,告别 limit 大偏移量性能灾难
- 技巧 4:避免隐式类型转换,杜绝索引失效的隐形杀手
- 技巧 5:巧用覆盖索引,避免回表操作实现查询性能翻倍
- 核心总结:SQL 优化通用黄金原则
技巧 1:杜绝 SELECT * ,精准字段查询是 SQL 优化的第一步
业务场景与问题复现
学生管理系统,需要实现班级学生列表分页查询功能。student表包含 30 个字段(含 text 类型的个人简介、blob 类型的头像二进制数据),总数据量 50 万条。为了图省事,直接编写了如下 SQL:
sql
SELECT * FROM student WHERE class_id = 101 LIMIT 0,20;
出现的核心问题:
- 接口响应时间长达 800ms,远超 200ms 的预期性能要求
- 数据库服务器磁盘 IO 占用飙升,并发量稍高就出现卡顿
- 后续表结构新增字段时,多次引发接口字段映射异常
问题根因深度拆解
- IO 与网络开销爆炸:
SELECT *会读取表中所有字段,包括 text、blob 等大字段,大幅增加磁盘 IO 读取开销和网络数据传输量; - 索引完全无法高效利用:无法使用覆盖索引,必须通过二级索引回表查询整行数据,查询效率极低;
- 线上稳定性风险:表结构新增 / 修改字段时,查询结果不可控,极易引发线上接口的字段映射故障。
优化方案与代码实现
- 只选取业务真正需要的字段,杜绝冗余字段查询;
- 基于查询字段建立覆盖索引,完全避免回表操作。
sql
-- 优化后SQL:只查询业务所需字段
SELECT id,name,student_no,phone FROM student WHERE class_id = 101 LIMIT 0,20;
-- 配套覆盖索引,让查询完全在索引中完成
CREATE INDEX idx_class_id_base ON student(class_id,name,student_no,phone);
优化效果实测
- 接口响应时间降至 30ms 以内,性能提升 25 倍以上
- 数据库 IO 占用下降 80%,服务器负载显著降低
- 后续表结构变更不影响该查询,接口稳定性大幅提升
避坑与使用注意事项
哪怕业务需要表中绝大多数字段,也必须显式写出字段名,避免表结构变更带来的未知故障;** 严禁在包含大字段(text/blob)的表中使用 SELECT ***。
技巧 2:NOT IN 子查询避坑,否定查询优先使用 NOT EXISTS
业务场景与问题复现
电商订单系统,需要统计「2025 年以来从未下过订单的用户数量」。user用户表 100 万条数据,order订单表 800 万条数据。编写的统计 SQL 如下:
sql
SELECT COUNT(*) FROM user
WHERE user_id NOT IN (SELECT user_id FROM `order` WHERE create_time >= '2025-01-01');
出现两个致命问题:
- SQL 执行时间长达 12s,完全无法满足业务实时查询的需求
- 一次订单表的
user_id出现 NULL 值后,查询结果直接返回 0,造成严重的统计错误,影响运营决策
问题根因深度拆解
- 性能灾难:
NOT IN子查询在 MySQL 中,会对外表做全表扫描,对内表做嵌套循环,数据量越大性能越差,千万级数据下直接超时; - 致命逻辑陷阱:在 SQL 中,
NOT IN (a,b,NULL)等价于user_id != a AND user_id != b AND user_id != NULL,而任何值与 NULL 做不等于比较,结果都是 UNKNOWN,最终整个 WHERE 条件恒为假,返回空结果。
优化方案与代码实现
改用NOT EXISTS实现等价逻辑,彻底规避 NULL 值坑,同时大幅提升查询性能。
sql
-- 优化后SQL:用NOT EXISTS替代NOT IN
SELECT COUNT(*) FROM user u
WHERE NOT EXISTS (
SELECT 1 FROM `order` o
WHERE o.user_id = u.user_id
AND o.create_time >= '2025-01-01'
);
优化效果实测
- SQL 执行时间降至 400ms 以内,性能提升 30 倍
- 彻底解决 NULL 值导致的结果错误问题,哪怕子查询出现 NULL 值,也能返回正确统计结果
NOT EXISTS会走 join 关联优化,能有效利用user_id上的索引,执行效率远高于NOT IN
避坑与使用注意事项
IN 与 EXISTS 的通用选型原则:外表小、内表大,用 IN;外表大、内表小,用 EXISTS;只要涉及否定查询,一律优先使用 NOT EXISTS,完全规避 NOT IN 的 NULL 值陷阱。
补充说明:SQL 中
order是保留关键字,作为表名时必须用反引号`包裹,避免语法报错。
技巧 3:大表深度分页优化,告别 limit 大偏移量性能灾难
业务场景与问题复现
电商订单系统,order订单表总数据量 1200 万条,运营后台需要按订单创建时间倒序分页查询,每页 10 条数据。当用户翻到第 10 万页(偏移量 100 万)时,编写的 SQL 如下:
sql
SELECT id,order_no,amount,create_time FROM `order`
WHERE is_deleted = 0
ORDER BY create_time DESC
LIMIT 1000000,10;
出现的核心问题:SQL 执行时间长达 18s,页面直接超时,完全无法使用。
问题根因深度拆解
MySQL 的limit offset,size执行机制:先扫描offset+size条记录,再丢弃前面的offset条无效数据,只返回后面的size条数据。当offset极大时,数据库需要扫描百万级的无效数据,同时如果无法使用覆盖索引,还会产生大量回表 IO 操作,最终导致性能雪崩。
优化方案与代码实现
针对两种主流业务场景,分别采用最优优化方案:
方案 1:连续分页场景(仅支持上一页 / 下一页):主键锚定法
适用于绝大多数 APP、后台的列表分页,不支持跳转到任意页码,只支持上下页切换,性能最优。
sql
-- 优化后SQL,每次传入上一页最后一条记录的id
SELECT id,order_no,amount,create_time FROM `order`
WHERE is_deleted = 0 AND id < #{last_page_max_id}
ORDER BY create_time DESC
LIMIT 10;
方案 2:跳页场景(支持任意页码跳转):延迟关联法
适用于需要支持跳转到任意页码的场景,先在索引中定位主键,再回表查询数据,大幅减少无效扫描。
sql
-- 优化后SQL:子查询先定位id,再关联回表
SELECT o.id,o.order_no,o.amount,o.create_time
FROM `order` o
INNER JOIN (
SELECT id FROM `order`
WHERE is_deleted = 0
ORDER BY create_time DESC
LIMIT 1000000,10
) AS t ON o.id = t.id;
优化效果实测
- 连续分页场景:执行时间稳定在 10ms 以内,彻底解决偏移量越大、查询越慢的问题;
- 跳页场景:100 万偏移量的查询时间降至 300ms 以内,性能提升 60 倍;
- 数据库 CPU 和 IO 开销大幅下降,支持高并发分页查询。
避坑与使用注意事项
- 主键锚定法要求排序字段和主键是有序正相关的(比如 create_time 和 id 都是递增的);
- 跳页场景的子查询必须使用覆盖索引,避免子查询内回表,否则优化效果会大打折扣。
技巧 4:避免隐式类型转换,杜绝索引失效的隐形杀手
业务场景与问题复现
用户登录接口,需要根据手机号查询用户信息。user表的phone字段为 varchar 类型,且建立了唯一索引。编写的查询 SQL 如下:
sql
SELECT id,name,phone,password FROM user WHERE phone = 13800138000;
出现的核心问题:单条查询执行时间达 800ms,并发量上来后数据库 CPU 直接打满;用 explain 分析发现,SQL 完全没有命中 phone 的索引,触发了全表扫描。
问题根因深度拆解
这是开发中最常见、也最容易被忽略的索引失效陷阱:隐式类型转换。当查询条件左右两边的数据类型不一致时,MySQL 会自动做类型转换。这里phone是 varchar 类型,右边的手机号是数字类型,MySQL 会把左边索引字段的所有值转为数字再比较,相当于对索引字段做了函数操作,直接导致索引失效,触发全表扫描。
优化方案与代码实现
严格匹配字段数据类型,给手机号加上引号,转为字符串类型,和字段类型完全一致。
sql
-- 优化后SQL:严格匹配字段数据类型
SELECT id,name,phone,password FROM user WHERE phone = '13800138000';
优化效果实测
- 查询执行时间降至 1ms 以内,性能提升 800 倍
- 索引正常命中,数据库 CPU 占用下降 90%,接口并发能力大幅提升
避坑与使用注意事项
- 反向场景补充:int 类型字段用字符串查询(比如
user_id = '123'),MySQL 会把字符串转为数字,不会导致索引失效;只有索引字段是字符串、查询值是数字时,才会触发致命的索引失效,这是最高频的踩坑场景; - 表关联查询时,关联字段的字符集、排序规则、数据类型必须完全一致,否则也会触发隐式类型转换;
- 上线前必须用 explain 分析 SQL 的执行计划,提前发现索引失效问题。
技巧 5:巧用覆盖索引,避免回表操作实现查询性能翻倍
业务场景与问题复现
电商商品列表查询,需要根据分类 ID 查询商品,按销量倒序排序。goods商品表总数据量 500 万条。已经给category_id建立了普通索引,编写的 SQL 如下:
sql
SELECT id,goods_name,price,sales FROM goods
WHERE category_id = 201
ORDER BY sales DESC
LIMIT 0,20;
出现的核心问题:SQL 执行时间仍有 1.2s;explain 分析发现,type 是 ref,但 Extra 里出现了Using filesort文件排序,同时存在大量回表操作。
问题根因深度拆解
MySQL 的普通二级索引,只存储了索引字段和主键值。数据库通过二级索引找到主键后,需要回到聚簇索引(主键索引)中查询 select 所需的其他字段,这个过程叫回表。每一条数据都需要一次回表 IO 操作,数据量大时性能极差;同时排序字段sales不在索引中,无法利用索引完成排序,触发了文件排序Using filesort,进一步拉低性能。
优化方案与代码实现
建立覆盖索引,把 where、order by、select 里的所有字段,按规则建立联合索引,让查询在索引中就能全部完成,完全避免回表。
sql
-- 建立覆盖索引:等值查询字段在前,排序字段在后,最后是select所需字段
CREATE INDEX idx_category_sales_cover ON goods(category_id,sales,goods_name,price);
优化效果实测
- SQL 执行时间降至 25ms 以内,性能提升 48 倍
- explain 的 Extra 中出现
Using index,说明成功触发覆盖索引,完全不需要回表 - 同时消除了
Using filesort文件排序,排序直接在索引内完成 - 数据库 IO 开销大幅降低,接口并发能力显著提升
避坑与使用注意事项
- 覆盖索引不是字段越多越好,只包含业务需要的字段,避免索引过大,影响表的写入性能;
- 联合索引的顺序必须遵循最左前缀原则,等值查询字段在前,排序字段在后,最后是查询字段;
- 对于高频查询的核心接口,优先用覆盖索引做优化,投入产出比最高。
核心总结:SQL 优化通用黄金原则
- 最小化原则:只查询业务需要的字段和数据,杜绝冗余查询,这是 SQL 优化的基础;
- 索引优先原则:所有查询必须能命中合理的索引,避免全表扫描,同时杜绝索引失效的各种陷阱;
- 规避陷阱原则:提前规避隐式类型转换、NOT IN NULL 值、大偏移量 limit 等高频踩坑点;
- 覆盖索引优先:高频查询优先使用覆盖索引,彻底避免回表操作,这是性价比最高的优化手段;
- 验证原则:上线前必须用 explain 分析 SQL 执行计划,提前发现并解决问题。
结尾
以上 5 个 SQL 优化技巧,都是日常开发和课程作业中最高频的场景,每一个都有完整的业务闭环,既可以直接用于课堂分享,也可以直接落地到自己的项目中。
如果觉得本文对你有帮助,欢迎点赞、收藏、评论,有其他好用的 SQL 优化技巧,也欢迎在评论区一起交流~

1535

被折叠的 条评论
为什么被折叠?



