news 2026/2/10 5:49:55

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
数据库性能优化:SQL 语句的优化(原理+解析+面试)

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

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

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

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

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

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

-- 糟糕写法:查所有字段,即使只需要姓名和手机号SELECT*FROMuserWHEREid=100;-- 优化写法:精准查询需要的字段SELECTname,phoneFROMuserWHEREid=100;
2. 避免在 WHERE 子句中对字段做「函数 / 运算」

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

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

举个通俗例子

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

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

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

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

-- 糟糕写法:对字段create_time做运算,索引失效SELECT*FROMorderWHEREDATE(create_time)='2026-01-19';SELECT*FROMgoodsWHEREprice*0.8<100;-- 优化写法:运算移到常量侧,字段保持原始值SELECT*FROMorderWHEREcreate_time>='2026-01-19'ANDcreate_time<'2026-01-20';SELECT*FROMgoodsWHEREprice<100/0.8;
3. 模糊查询避免「前导通配符」

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

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

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

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

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

-- 糟糕写法:%开头,索引失效SELECT*FROMuserWHEREnameLIKE'%张三';-- 优化写法:%结尾,可走索引SELECT*FROMuserWHEREnameLIKE'张三%';
4. 分页查询优化(OFFSET 过大)

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

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

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

-- 糟糕写法:OFFSET越大,查询越慢SELECT*FROMorderORDERBYidLIMIT10000,10;-- 优化写法:通过主键定位,直接取后续数据SELECT*FROMorderWHEREid>10000ORDERBYidLIMIT10;
6.避免隐式类型转换(索引失效的 “隐形杀手”)

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

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

-- 场景:user表的phone字段是VARCHAR类型(字符串)-- 糟糕写法:用数字查询字符串字段,触发隐式转换,索引失效SELECT*FROMuserWHEREphone=13800001234;-- 优化写法:保持类型一致,走索引SELECT*FROMuserWHEREphone='13800001234';
7. 合理使用 DISTINCT 和 LIMIT

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

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

-- 糟糕写法:先去重再过滤,数据量大时极慢SELECTDISTINCTnameFROMuserWHEREage>18;-- 优化写法:先过滤再去重,减少去重数据量SELECTDISTINCTnameFROM(SELECTnameFROMuserWHEREage>18)t;-- 错误顺序:LIMIT放中间,逻辑错误且性能差SELECT*FROM(SELECTidFROMuserLIMIT10)tWHEREage>18;-- 正确顺序:先过滤再限制结果SELECTidFROMuserWHEREage>18LIMIT10;
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)时,数据库能直接在索引里定位到所有空字符串的位置,快速返回结果,无需全表扫描。

-- 场景:user表的email字段允许NULL,需查询有邮箱的用户-- 糟糕写法:IS NOT NULL可能不走索引SELECT*FROMuserWHEREemailISNOTNULL;-- 优化写法:设置默认值为空字符串,用普通条件查询ALTERTABLEuserMODIFYCOLUMNemailVARCHAR(50)DEFAULT'';SELECT*FROMuserWHEREemail!='';-- 走索引

二、关联查询(JOIN)优化

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

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

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

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

-- 场景:订单表(order,100万条)关联用户表(user,10万条),查用户的订单-- 糟糕写法:大表驱动小表SELECTo.*FROM`order`oLEFTJOIN`user`uONo.user_id=u.idWHEREu.city='北京';-- 优化写法:小表驱动大表(先筛选小表数据,再关联大表)SELECTo.*FROM(SELECTidFROM`user`WHEREcity='北京')uLEFTJOIN`order`oONu.id=o.user_id;
2. 关联字段必须加索引

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

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

-- 给关联字段创建索引(只需执行一次)CREATEINDEXidx_order_user_idON`order`(user_id);-- 关联查询会走索引,效率大幅提升SELECTu.name,o.order_noFROM`user`uINNERJOIN`order`oONu.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 的字段,数据库无需额外排序。

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

-- 场景:按创建时间排序查订单-- 糟糕写法:无索引,会触发Using filesortSELECT*FROM`order`ORDERBYcreate_time;-- 优化写法:创建索引,避免排序CREATEINDEXidx_order_create_timeON`order`(create_time);SELECTid,order_noFROM`order`ORDERBYcreate_time;-- 覆盖索引,无需回表
2. 避免在 GROUP BY 中使用函数

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

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

-- 糟糕写法:按日期聚合,触发全表扫描SELECTDATE(create_time)ASday,COUNT(*)FROM`order`GROUPBYDATE(create_time);-- 优化写法:新增字段order_day,提前存储日期ALTERTABLE`order`ADDCOLUMNorder_dayDATE;UPDATE`order`SETorder_day=DATE(create_time);-- 初始化数据CREATEINDEXidx_order_dayON`order`(order_day);SELECTorder_day,COUNT(*)FROM`order`GROUPBYorder_day;-- 走索引

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

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

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

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

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

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

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

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

-- 糟糕写法:无索引,全表扫描+锁表UPDATE`order`SETstatus=2WHEREcreate_time<'2025-01-01';-- 优化写法:给create_time加索引,精准更新CREATEINDEXidx_order_create_timeON`order`(create_time);UPDATE`order`SETstatus=2WHEREcreate_time<'2025-01-01'LIMIT1000;-- 分批更新,避免锁表过久

五、特殊场景的SQL优化

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

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

-- 场景:删除order表中2025年之前的历史数据(100万条)-- 糟糕写法:一次性删除,锁表+IO暴涨DELETEFROM`order`WHEREcreate_time<'2025-01-01';-- 优化写法:分批删除,每次删少量数据SET@row_count=1;WHILE@row_count>0DODELETEFROM`order`WHEREcreate_time<'2025-01-01'LIMIT1000;SET@row_count=ROW_COUNT();-- 获取本次删除的行数SELECTSLEEP(1);-- 暂停1秒,降低数据库压力ENDWHILE;
2. 子查询优化(避免 “嵌套地狱”)

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

原因:

执行逻辑:

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

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

资源消耗:

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

-- 场景:查询有订单的用户信息-- 糟糕写法:相关子查询,外层每一行都执行一次子查询SELECT*FROMuserWHEREidIN(SELECTuser_idFROM`order`);-- 优化写法:JOIN替代子查询,效率提升数倍SELECTDISTINCTu.*FROMuseruINNERJOIN`order`oONu.id=o.user_id;
3. 统计类查询优化(避免实时计算)

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

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

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

-- 示例:创建统计汇总表CREATETABLEorder_stat(stat_dateDATEPRIMARYKEY,-- 统计日期order_countINTDEFAULT0,-- 当日订单数sale_amountDECIMAL(10,2)DEFAULT0-- 当日销售额);-- 定时任务(如每天凌晨)更新汇总表REPLACEINTOorder_stat(stat_date,order_count,sale_amount)SELECTDATE(create_time)ASstat_date,COUNT(*)ASorder_count,SUM(amount)ASsale_amountFROM`order`WHEREDATE(create_time)=CURDATE()-INTERVAL1DAYGROUPBYDATE(create_time);-- 查询时直接查汇总表,无需实时计算SELECTorder_count,sale_amountFROMorder_statWHEREstat_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执行计划EXPLAINSELECTname,phoneFROMuserWHEREid=100;

重点关注 3 个字段:

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

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

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

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

--1. 查看SQL执行耗时(MySQL) SET profiling=1;-- 开启性能分析 SELECT * FROM user WHERE age>18;-- 执行SQL SHOW PROFILES;-- 查看所有SQL的耗时 SHOW PROFILE FOR QUERY1;-- 查看第1条SQL的详细耗时(如CPU、IO) --2. 查看索引使用情况(判断索引是否有效) SHOW INDEX FROM user;-- 查看索引信息 SELECT * FROM sys.schema_unused_indexes;-- MySQL8.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 一下效率高

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/2/7 11:09:21

Wan2.2开箱即用镜像:0配置部署,1块钱起体验最新模型

Wan2.2开箱即用镜像&#xff1a;0配置部署&#xff0c;1块钱起体验最新模型 你是不是也遇到过这种情况&#xff1a;刚听说Wan2.2发布了新版本&#xff0c;支持更长视频、更高清画质&#xff0c;心里一激动就想马上试试看。结果一打开GitHub仓库&#xff0c;发现依赖复杂、环境…

作者头像 李华
网站建设 2026/2/8 14:56:11

GPEN vs GFPGAN vs CodeFormer实测对比:云端2小时搞定选型

GPEN vs GFPGAN vs CodeFormer实测对比&#xff1a;云端2小时搞定选型 你是不是也遇到过这样的情况&#xff1f;公司要开发一个家谱App&#xff0c;老板让你快速评估三种主流AI人脸修复模型——GPEN、GFPGAN和CodeFormer的效果&#xff0c;好决定最终用哪个技术方案。可问题是…

作者头像 李华
网站建设 2026/2/10 6:32:06

JS vs jQuery:核心差异解析

JavaScript 与 jQuery 的区别JavaScript 是一种脚本编程语言&#xff0c;主要用于网页开发&#xff0c;可以直接在浏览器中运行&#xff0c;实现动态交互效果。jQuery 是一个基于 JavaScript 的库&#xff0c;封装了许多常用的功能&#xff0c;简化了 DOM 操作、事件处理、动画…

作者头像 李华
网站建设 2026/2/10 7:21:00

西门子罗宾康IGBT模块LDZ10503116

西门子罗宾康IGBT模块LDZ10503116技术详解一、产品概述西门子罗宾康LDZ10503116是一款高压绝缘栅双极型晶体管&#xff08;IGBT&#xff09;功率模块&#xff0c;专为工业级中高压变频器设计。该模块采用第三代硅基半导体技术&#xff0c;额定参数为 1700V/105A &#xff0c;封…

作者头像 李华
网站建设 2026/2/9 16:17:13

Gitee:中国企业数字化转型的核心技术引擎

Gitee&#xff1a;中国企业数字化转型的核心技术引擎 在中国企业加速数字化转型的背景下&#xff0c;项目管理软件的市场需求呈现爆发式增长。根据IDC最新报告&#xff0c;2025年中国DevOps市场规模将达到120亿元&#xff0c;年复合增长率高达38%。在这个快速发展的赛道上&…

作者头像 李华