数据库性能优化:SQL 语句的优化(原理+解析+面试)

数据库性能优化:SQL 语句的优化(原理+解析+面试)

一、基础查询 SQL 优化(最常用)

这类优化主要针对单表查询,核心是减少数据扫描范围、避免索引失效

1. 杜绝「全字段查询」和「冗余字段」

问题:SELECT * 会查询所有字段,不仅增加网络传输量,还无法利用「覆盖索引」(只查索引字段就能返回结果,无需回表)。

优化:只查询业务需要的字段。

sql 复制代码
-- 糟糕写法:查所有字段,即使只需要姓名和手机号
SELECT * FROM user WHERE id = 100;

-- 优化写法:精准查询需要的字段
SELECT name, phone FROM user WHERE id = 100;
2. 避免在 WHERE 子句中对字段做「函数 / 运算」

问题:对字段做函数 / 运算会导致索引失效,数据库只能全表扫描。

原因:索引是按字段原始值排序的有序结构(如 B + 树),能快速二分查找;而对字段做函数 / 运算后,数据库无法直接匹配索引里的原始值,只能放弃索引,逐行计算后再判断(即全表扫描)。

举个通俗例子

比如索引里存的是原始时间 2026-01-19 10:00:00,你查 DATE(create_time) = '2026-01-19'

索引里没有 "日期格式" 的排序数据,数据库无法直接找;

只能全表扫描,给每一行的 create_time 执行 DATE() 函数,再判断是否等于目标值。

优化:将运算逻辑移到「常量侧」,让字段保持 "原始状态"。

sql 复制代码
-- 糟糕写法:对字段create_time做运算,索引失效
SELECT * FROM order WHERE DATE(create_time) = '2026-01-19';
SELECT * FROM goods WHERE price * 0.8 < 100;

-- 优化写法:运算移到常量侧,字段保持原始值
SELECT * FROM order WHERE create_time >= '2026-01-19' AND create_time < '2026-01-20';
SELECT * FROM goods WHERE price < 100 / 0.8;
3. 模糊查询避免「前导通配符」

问题:LIKE '%xxx' 会让索引失效,LIKE 'xxx%' 则可以利用索引。

原因:这和索引的有序存储特性直接相关:

LIKE 'xxx%' 可以利用索引:索引是按字段值的字符顺序排序的。xxx% 是前缀匹配,意味着所有以 xxx 开头的字符串在索引里是连续排列的。数据库可以在索引里快速定位到开头是 xxx 的范围,再进行精确匹配,所以能走索引。

LIKE '%xxx' 会让索引失效:%xxx 是后缀匹配,所有以 xxx 结尾的字符串在索引里是分散的,没有连续的范围。数据库无法通过有序的索引快速定位,只能逐行扫描全表来判断是否符合条件,所以索引失效。

优化:业务允许的情况下,尽量使用前缀匹配;若必须后缀匹配,可考虑全文索引。

sql 复制代码
-- 糟糕写法:%开头,索引失效
SELECT * FROM user WHERE name LIKE '%张三';

-- 优化写法:%结尾,可走索引
SELECT * FROM user WHERE name LIKE '张三%';
4. 分页查询优化(OFFSET 过大)

问题:LIMIT 10000, 10 会让数据库先扫描前 10010 条数据,再丢弃前 10000 条,效率极低。

原因:LIMIT offset, rows 的执行逻辑是先扫描并获取前 offset+rows 条数据,再舍弃前 offset 条------ 因为数据库无法直接定位到第 offset 行,必须从第一条开始逐行遍历计数,直到找到目标位置。而索引而优化写法 WHERE id > 10000 LIMIT 10 能高效,是因为主键 id 有索引(有序),数据库可直接定位到 id=10000 的位置,再取后续 10 条,无需扫描前面的无效数据。

优化:利用「主键 / 索引字段」做范围查询,替代 OFFSET。

sql 复制代码
-- 糟糕写法:OFFSET越大,查询越慢
SELECT * FROM order ORDER BY id LIMIT 10000, 10;

-- 优化写法:通过主键定位,直接取后续数据
SELECT * FROM order WHERE id > 10000 ORDER BY id LIMIT 10;
6.避免隐式类型转换(索引失效的 "隐形杀手")

这是新手最容易踩的坑,字段类型和查询值类型不匹配,会触发隐式转换,直接导致索引失效。

原因:索引是按字段原始数据类型(如字符串 / 数字)排序存储的,隐式转换会改变查询值的类型,导致数据库无法直接匹配索引里的原始值,只能放弃索引、全表扫描后再做类型转换匹配。

sql 复制代码
-- 场景:user表的phone字段是VARCHAR类型(字符串)
-- 糟糕写法:用数字查询字符串字段,触发隐式转换,索引失效
SELECT * FROM user WHERE phone = 13800001234;

-- 优化写法:保持类型一致,走索引
SELECT * FROM user WHERE phone = '13800001234';
7. 合理使用 DISTINCT 和 LIMIT

DISTINCT 会触发排序去重,尽量用 WHERE 先过滤数据,再去重;(DISTINCT 的去重逻辑是基于查询结果集排序后去重,先通过 WHERE 过滤掉无关数据,能大幅减少排序 / 去重的数据量,降低资源开销;)

LIMIT 要放在最后,避免先全表扫描再限制结果。(LIMIT 是对最终结果集做限制,若放前面会先限制部分数据再过滤,既可能逻辑错误,也会导致数据库先扫描大量无关数据,再过滤 / 关联,浪费资源。)

sql 复制代码
-- 糟糕写法:先去重再过滤,数据量大时极慢
SELECT DISTINCT name FROM user WHERE age > 18;

-- 优化写法:先过滤再去重,减少去重数据量
SELECT DISTINCT name FROM (SELECT name FROM user WHERE age > 18) t;

-- 错误顺序:LIMIT放中间,逻辑错误且性能差
SELECT * FROM (SELECT id FROM user LIMIT 10) t WHERE age > 18;

-- 正确顺序:先过滤再限制结果
SELECT id FROM user WHERE age > 18 LIMIT 10;
8. 慎用 NULL 值判断

NULL 值的判断(IS NULL/IS NOT NULL)可能导致索引失效,建议:

字段尽量设置默认值(如用 0 / 空字符串替代 NULL);

若必须存 NULL,创建索引时包含 NULL 值(MySQL 8.0 + 默认支持,低版本需注意)。

拆解问题:

为什么 IS NULL/IS NOT NULL 会让索引失效?

假设user表的email字段允许 NULL,索引里只存储了有值的email(如xxx@163.com),NULL 值并未按顺序存入索引;

执行WHERE email IS NULL时,索引里找不到 NULL 值的位置,数据库只能逐行扫描全表,检查每条数据的email是否为 NULL。

为什么默认值能解决问题?

email默认值设为空字符串'',索引会将''和其他邮箱地址一起按字符顺序存储;

执行WHERE email = ''(替代IS NULL)时,数据库能直接在索引里定位到所有空字符串的位置,快速返回结果,无需全表扫描。

sql 复制代码
-- 场景:user表的email字段允许NULL,需查询有邮箱的用户
-- 糟糕写法:IS NOT NULL可能不走索引
SELECT * FROM user WHERE email IS NOT NULL;

-- 优化写法:设置默认值为空字符串,用普通条件查询
ALTER TABLE user MODIFY COLUMN email VARCHAR(50) DEFAULT '';
SELECT * FROM user WHERE email != ''; -- 走索引

二、关联查询(JOIN)优化

关联查询是性能问题高发区,核心是减少关联数据量、确保关联字段有索引

1. 小表驱动大表(核心原则)

原理:让小表的数据先循环,再去大表中匹配,减少循环次数和 IO。

优化:MySQL 中INNER JOIN会自动优化表顺序,但LEFT JOIN需手动将小表放左边。

sql 复制代码
-- 场景:订单表(order,100万条)关联用户表(user,10万条),查用户的订单
-- 糟糕写法:大表驱动小表
SELECT o.* FROM `order` o LEFT JOIN `user` u ON o.user_id = u.id WHERE u.city = '北京';

-- 优化写法:小表驱动大表(先筛选小表数据,再关联大表)
SELECT o.* FROM (SELECT id FROM `user` WHERE city = '北京') u 
LEFT JOIN `order` o ON u.id = o.user_id;
2. 关联字段必须加索引

问题:JOIN 的关联字段无索引,会导致大表全表扫描(如 order.user_id 无索引)。

优化:为关联字段创建索引(如 order 表的 user_id 字段)。

sql 复制代码
-- 给关联字段创建索引(只需执行一次)
CREATE INDEX idx_order_user_id ON `order`(user_id);

-- 关联查询会走索引,效率大幅提升
SELECT u.name, o.order_no FROM `user` u 
INNER JOIN `order` o ON u.id = o.user_id;
3. 避免 JOIN 过多表

问题:JOIN 超过 3-4 张表,会导致执行计划复杂,性能急剧下降。

优化:拆分 SQL,分步查询(先查核心表,再根据结果查关联表)。

4.减少使用 IN/NOT IN,用 EXISTS/NOT EXISTS 或关联查询替代

原因:

IN 的问题

当 IN 后是子查询时,数据库会先执行子查询生成临时结果集,再将外层表的每条数据与临时集做匹配;如果临时集过大(如上万条),匹配开销会急剧增加。

NOT IN 更糟:如果子查询结果包含 NULL 值,NOT IN 会直接返回空集(逻辑陷阱),且数据库无法利用索引,只能全表扫描。

EXISTS 的优势

EXISTS 是 "存在性判断",只要找到满足条件的第一条数据就停止扫描(类似 "短路逻辑"),无需生成完整的临时结果集;且能更好地利用索引,尤其是关联字段有索引时,效率远高于 IN/NOT IN。

注意:

关联字段必须加索引 :比如order.user_id要加索引,否则 EXISTS 和关联查询都会退化为全表扫描;

EXISTS 子查询里用 SELECT 1:无需查询具体字段(如 SELECT *),SELECT 1 只是判断 "是否存在",减少数据传输;

DISTINCT 的使用:关联查询(如 INNER JOIN)可能返回重复行,需用 DISTINCT 去重(EXISTS 无需,因为只判断存在性)。

子查询场景:优先用 EXISTS/NOT EXISTS;

常量列表(大量):优先用关联查询 + 临时表;

常量列表(少量):保留 IN 即可(无需过度优化);

5.OR 换 UNION/UNION ALL

原因:

OR 的核心问题是破坏索引的有效性:

如果 OR 连接的多个条件字段都有独立索引(如 id 和 phone 各有索引),数据库优化器通常无法同时使用多个索引,会直接走全表扫描;

如果 OR 连接的条件字段无索引,则必然触发全表扫描,数据量越大越慢。

而 UNION/UNION ALL 是将多个独立查询的结果合并,每个子查询都能单独使用对应索引,最终合并结果,效率远高于 OR。

例外:OR 连接同字段少量值(<10 个)时,可保留 OR(数据库会优化为 IN,走索引),无需替换。

三、聚合 / 排序 SQL 优化(GROUP BY/ORDER BY)

这类 SQL 容易出现「文件排序」「临时表」,核心是利用索引避免排序 / 临时表

1. 索引覆盖 GROUP BY/ORDER BY 字段

原理:如果索引包含 GROUP BY/ORDER BY 的字段,数据库无需额外排序。

优化:创建包含排序 / 聚合字段的联合索引。

sql 复制代码
-- 场景:按创建时间排序查订单
-- 糟糕写法:无索引,会触发Using filesort
SELECT * FROM `order` ORDER BY create_time;

-- 优化写法:创建索引,避免排序
CREATE INDEX idx_order_create_time ON `order`(create_time);
SELECT id, order_no FROM `order` ORDER BY create_time; -- 覆盖索引,无需回表
2. 避免在 GROUP BY 中使用函数

问题:对字段做函数运算会导致索引失效,无法优化聚合操作。

优化:提前预处理数据(如新增字段存储按天 / 月聚合的结果)。

sql 复制代码
-- 糟糕写法:按日期聚合,触发全表扫描
SELECT DATE(create_time) AS day, COUNT(*) FROM `order` GROUP BY DATE(create_time);

-- 优化写法:新增字段order_day,提前存储日期
ALTER TABLE `order` ADD COLUMN order_day DATE;
UPDATE `order` SET order_day = DATE(create_time); -- 初始化数据
CREATE INDEX idx_order_day ON `order`(order_day);
SELECT order_day, COUNT(*) FROM `order` GROUP BY order_day; -- 走索引

四、写入类 SQL 优化(INSERT/UPDATE/DELETE)

写入操作的优化核心是减少锁竞争、降低索引维护开销

1. 批量写入替代单条写入

问题:单条 INSERT 循环执行,会频繁触发日志刷盘和索引更新。

优化:使用批量 INSERT,减少 IO 次数。

sql 复制代码
-- 糟糕写法:单条插入(循环1000次)
INSERT INTO user(name, phone) VALUES ('张三', '1380000');
INSERT INTO user(name, phone) VALUES ('李四', '1390000');

-- 优化写法:批量插入(1次执行)
INSERT INTO user(name, phone) VALUES 
('张三', '1380000'),
('李四', '1390000'),
... -- 最多建议批量1000条左右,避免SQL过长
;
2. UPDATE/DELETE 先筛选再操作

问题:无 WHERE 条件的 UPDATE/DELETE 会锁全表,有 WHERE 但无索引会全表扫描 + 锁表。

优化:WHERE 条件加索引,且先筛选少量数据再更新 / 删除。

sql 复制代码
-- 糟糕写法:无索引,全表扫描+锁表
UPDATE `order` SET status = 2 WHERE create_time < '2025-01-01';

-- 优化写法:给create_time加索引,精准更新
CREATE INDEX idx_order_create_time ON `order`(create_time);
UPDATE `order` SET status = 2 WHERE create_time < '2025-01-01' LIMIT 1000; -- 分批更新,避免锁表过久

五、特殊场景的SQL优化

1. 大批量删除 / 更新(避免锁表)

针对百万级以上数据的删除 / 更新,直接操作会锁表,导致业务阻塞,优化方案:

sql 复制代码
-- 场景:删除order表中2025年之前的历史数据(100万条)
-- 糟糕写法:一次性删除,锁表+IO暴涨
DELETE FROM `order` WHERE create_time < '2025-01-01';

-- 优化写法:分批删除,每次删少量数据
SET @row_count = 1;
WHILE @row_count > 0 DO
    DELETE FROM `order` WHERE create_time < '2025-01-01' LIMIT 1000;
    SET @row_count = ROW_COUNT(); -- 获取本次删除的行数
    SELECT SLEEP(1); -- 暂停1秒,降低数据库压力
END WHILE;
2. 子查询优化(避免 "嵌套地狱")

MySQL 对多层子查询的优化能力弱,尽量用 JOIN 替代子查询,尤其是相关子查询(子查询依赖外层表)。

原因:

执行逻辑:

子查询(尤其是IN + 子查询):数据库通常会先执行子查询得到一个临时结果集,再用这个结果集去外层表匹配;如果是相关子查询(子查询依赖外层表字段),则外层表每一行都会触发一次子查询,相当于 "嵌套循环",时间复杂度会从O(n)变成O(n²)

关联查询(JOIN):数据库优化器会根据表的大小、索引情况,选择最优的连接方式(如嵌套循环连接、哈希连接、合并连接),优先用小表驱动大表,减少扫描次数,整体是 "一次扫描、一次匹配"。

资源消耗:

子查询会生成临时表存储中间结果,增加内存 / 磁盘 IO 开销;而 JOIN 可以直接利用索引做关联,避免临时表的额外消耗。

sql 复制代码
-- 场景:查询有订单的用户信息
-- 糟糕写法:相关子查询,外层每一行都执行一次子查询
SELECT * FROM user WHERE id IN (SELECT user_id FROM `order`);

-- 优化写法:JOIN替代子查询,效率提升数倍
SELECT DISTINCT u.* FROM user u
INNER JOIN `order` o ON u.id = o.user_id;
3. 统计类查询优化(避免实时计算)

高频的统计查询(如 "今日订单数""本月销售额"),实时计算会消耗大量资源,优化方案:

方案 1:预计算 + 定时更新(用定时任务 / 触发器更新统计结果到专用表);

方案 2:使用数据库的汇总表 / 物化视图(MySQL 8.0 + 支持物化视图)。

sql 复制代码
-- 示例:创建统计汇总表
CREATE TABLE order_stat (
    stat_date DATE PRIMARY KEY, -- 统计日期
    order_count INT DEFAULT 0, -- 当日订单数
    sale_amount DECIMAL(10,2) DEFAULT 0 -- 当日销售额
);

-- 定时任务(如每天凌晨)更新汇总表
REPLACE INTO order_stat (stat_date, order_count, sale_amount)
SELECT 
    DATE(create_time) AS stat_date,
    COUNT(*) AS order_count,
    SUM(amount) AS sale_amount
FROM `order`
WHERE DATE(create_time) = CURDATE() - INTERVAL 1 DAY
GROUP BY DATE(create_time);

-- 查询时直接查汇总表,无需实时计算
SELECT order_count, sale_amount FROM order_stat WHERE stat_date = '2026-01-19';

补:

很多新手误以为的 "优化",实际会降低性能,一定要避开:

1.给所有字段加索引:索引会减慢 INSERT/UPDATE/DELETE 速度,且数据库优化器会因索引过多选择错误的执行路径;

2.用 COUNT (*) 代替 COUNT (字段):COUNT (*) 是 MySQL 优化过的,效率比 COUNT (字段) 高(COUNT (字段) 会判断字段是否为 NULL);

3.过度拆分 SQL:比如把一次 JOIN 查询拆成多次单表查询,反而增加网络交互和数据库连接开销;

4.忽略数据倾斜:比如 WHERE 条件中 "status=0" 占 90% 数据,给 status 加索引反而变慢(索引选择性差,数据库会直接全表扫描)。

六、验证优化效果的核心方法

优化后必须验证效果,最常用的是EXPLAIN关键字:

sql 复制代码
-- 查看SQL执行计划
EXPLAIN SELECT name, phone FROM user WHERE id = 100;

重点关注 3 个字段:

type:执行类型,最优为const(常量查询),其次range(范围查询),避免ALL(全表扫描);

key:实际使用的索引,NULL 表示未走索引;

Extra:避免Using filesort(文件排序)、Using temporary(临时表)。

优化后不仅要看执行计划,还要量化耗时和资源消耗:

bash 复制代码
-- 1. 查看SQL执行耗时(MySQL)
SET profiling = 1; -- 开启性能分析
SELECT * FROM user WHERE age > 18; -- 执行SQL
SHOW PROFILES; -- 查看所有SQL的耗时
SHOW PROFILE FOR QUERY 1; -- 查看第1条SQL的详细耗时(如CPU、IO)

-- 2. 查看索引使用情况(判断索引是否有效)
SHOW INDEX FROM user; -- 查看索引信息
SELECT * FROM sys.schema_unused_indexes; -- MySQL 8.0+查看未使用的索引

七、总结

1.SQL 语句优化的核心是让数据库少干活:减少扫描行数、避免无效排序 / 临时表、利用索引减少 IO;

2.优先优化语法层面(如避免字段运算、杜绝 SELECT *),再优化索引,最后验证执行计划;

3.写入类 SQL 重点减少锁竞争和索引维护开销,查询类 SQL 重点利用索引避免全表扫描。

4.容易忽略的坑:隐式类型转换、NULL 值判断、过度索引,这些是索引失效的高频原因;

5.特殊场景优化:大批量操作要分批执行,子查询优先换 JOIN,统计查询优先预计算;

6.避坑关键:不要盲目加索引,不要过度拆分 SQL,优化后要量化验证效果。

八、为了方便记忆做了个简单的面试总结

1.避免全字段查询

不要用 SELECT *,只查需要的字段;优先在 WHEREORDER BY 涉及的字段上建索引,减少数据扫描。

2.子查询换关联查询

少用子查询(尤其是相关子查询),用 JOIN 替代,减少嵌套循环和临时表开销。

3.IN/NOT IN 换 EXISTS / 关联查询

IN/NOT IN 性能差且有逻辑陷阱,用 EXISTS/NOT EXISTS(存在性判断)或 JOIN 替代,提升效率。

4.OR 换 UNION/UNION ALL

UNION(去重)或 UNION ALL(不去重)替代 OR,避免全表扫描,稳定利用索引。

5.避免在 WHERE 子句里做运算或用不等于

别对字段用 !=/<>,也别在字段上做函数 / 计算,否则会导致索引失效,触发全表扫描。

6.避免 NULL 值判断

别用 IS NULL/IS NOT NULL,给字段设置默认值(如空字符串 / 0),用普通条件查询来替代,保证索引可用。

记忆口诀:

少用星号和子查,IN 换 EXISTS 或 JOIN;OR 用 UNION 替代,字段运算 NULL 不查。

分页别用大 OFFSET,小表驱动大表佳;索引不贪多和杂,EXPLAIN 一下效率高

相关推荐
yuhaiqun19892 小时前
SQL+VSCode实战指南:AI赋能高效数据库操作
数据库·人工智能·经验分享·vscode·sql·学习·学习方法
韩立学长2 小时前
基于Springboot琴行学生课程信息管理系统2gt392wb(程序、源码、数据库、调试部署方案及开发环境)系统界面展示及获取方式置于文档末尾,可供参考。
数据库·spring boot·后端
Qhumaing2 小时前
Java学习——第五章 异常处理与输入输出流笔记
java·笔记·学习
Mr.H01272 小时前
Linux常见压缩命令
linux·服务器·数据库
咕噜企业分发小米2 小时前
阿里云与华为云基因测序数据同步的日志内容中,哪些字段对于故障排查最为关键?
数据库·阿里云·华为云
阿杰 AJie2 小时前
MyBatis-Plus 比较运算符
java·数据库·mybatis
码农幻想梦2 小时前
实验六 AOP,JdbcTemplate及声明式事务
java·开发语言·数据库
砚边数影2 小时前
金仓数据库KDMS助力某能源集团完成20TB时序数据智能迁移,效率提升显著、年省运维成本超千万元
运维·数据库·能源·时序数据库·数据库平替用金仓·金仓数据库·kdms
我是一只小青蛙8882 小时前
Python文件组织:路径抽象到安全归档
java·服务器·前端