“报表又卡了!”“用户说订单查询要等半天!” 相信不少开发和运维同学都被这类SQL性能问题折磨过。明明功能没问题,数据量一上来就秒变“龟速”,其实大多是SQL写法或索引设计出了岔子。今天就用最接地气的方式,带你吃透SQL优化的核心逻辑,避开那些踩过无数次的坑。

一、先搞懂:SQL为什么会变慢?

在讲优化前,得先明白“慢”的根源。我们可以把数据库比作图书馆,SQL查询就是“找书”的过程,而慢查询的本质,就是“找书方式太笨”。

1. 最常见的坑:全表扫描(相当于翻遍整个图书馆)

当SQL没有用到索引时,数据库会像没目录的图书馆一样,从第一行数据开始逐行查找,这就是“全表扫描”。数据量小的时候还好,一旦表有100万行,就相当于翻100万页书,不慢才怪。

举个例子:有一张order表(100万行),要查“2024年1月的所有订单”,没加索引的SQL是这样的:

-- 慢查询:无索引,触发全表扫描
SELECT * FROM `order` WHERE create_time >= '2024-01-01' AND create_time < '2024-02-01';

执行效果就像这样——逐行排查,效率极低:

2. 次常见坑:索引失效(有目录却不会用)

很多同学知道要建索引,但建完还是慢,原因是SQL写法把索引“弄失效”了。比如在索引字段上做函数运算、用不等于(!=)、模糊查询开头带%,都会让数据库放弃使用索引,回头走全表扫描。

反面例子:对索引字段create_time用了函数,索引直接失效:

-- 索引失效:在索引字段上用函数
SELECT * FROM `order` WHERE DATE(create_time) = '2024-01-01';

二、SQL优化核心原则:让数据库“少干活”

优化SQL的本质,就是减少数据库的“工作量”——要么减少扫描的数据量,要么减少计算的复杂度。核心围绕三个方向:合理建索引、优化SQL写法、利用执行计划

三、实战优化:从慢查询到秒级响应

我们用一个真实场景贯穿始终:某电商平台的order订单表(100万行),字段包括id(主键)、user_id(用户ID)、create_time(创建时间)、amount(金额)、status(订单状态),现在要优化两个常见慢查询。

场景1:查询“用户10086在2024年1月的订单”

优化前:慢到超时的SQL
-- 执行耗时:12.8秒,全表扫描
SELECT id, amount, status 
FROM `order` 
WHERE user_id = 10086 
  AND create_time >= '2024-01-01' 
  AND create_time < '2024-02-01';

EXPLAIN分析(重点看type和key字段),发现type是ALL(全表扫描),key是NULL(未用索引):

优化步骤1:建“合适的索引”——联合索引比单字段索引更高效

这里查询条件有两个:user_id和create_time,单独建其中一个索引效果有限,建联合索引才是最优解。联合索引的顺序有讲究:过滤性强的字段放前面(这里user_id能精准定位到某用户,过滤性比时间强)。

-- 建立联合索引:user_id在前,create_time在后
CREATE INDEX idx_order_user_create ON `order`(user_id, create_time);
优化步骤2:验证优化效果——从12秒到0.01秒

再执行原SQL,耗时直接降到0.01秒。用EXPLAIN查看,type变成ref(索引查找),key显示用到了刚建的联合索引:

场景2:关联查询“用户订单及对应商品信息”

需求:查询“2024年1月已支付的订单,包含用户昵称和商品名称”,涉及三张表:order(100万行)、user(10万行)、goods(5万行)。

优化前:大表驱动小表,关联效率低
-- 执行耗时:8.5秒,大表order驱动小表user
SELECT o.id, u.nickname, g.goods_name, o.amount
FROM `order` o
JOIN user u ON o.user_id = u.id
JOIN goods g ON o.goods_id = g.id
WHERE o.status = 2  -- 已支付
  AND o.create_time >= '2024-01-01';

问题出在驱动表选择:order是大表,用它驱动小表,相当于用100万行数据去匹配10万行,循环次数太多。

优化原则:小表驱动大表(用“目录”定位后再找细节)

联查的核心逻辑:用小表的结果集作为“筛选条件”,去匹配大表的数据,就像先查目录找到书的位置,再去书架拿书。这里可以先从order表筛选出“2024年1月已支付”的小结果集(假设只有5000行),再关联其他表。

优化后:先筛选小结果集,再关联
-- 执行耗时:0.05秒,先筛选小结果集
SELECT o.id, u.nickname, g.goods_name, o.amount
FROM (
  -- 子查询先筛选出小结果集(5000行),作为驱动表
  SELECT id, user_id, goods_id, amount 
  FROM `order` 
  WHERE status = 2 
    AND create_time >= '2024-01-01'
) o
JOIN user u ON o.user_id = u.id
JOIN goods g ON o.goods_id = g.id;

同时给goods表的id和user表的id建主键索引(通常默认会建),给order表的status和create_time建联合索引,进一步提升效率。

四、避坑指南:这些SQL写法千万别用

很多时候,不是索引没建对,而是SQL写法“坑”了自己。整理了6个高频错误写法,避坑就是提效:

错误写法 问题原因 正确写法
SELECT * FROM order 查询无关字段,增加IO和传输开销 SELECT id, amount FROM order
WHERE DATE(create_time) = ‘2024-01-01’ 索引字段用函数,导致索引失效 WHERE create_time BETWEEN ‘2024-01-01 00:00:00’ AND ‘2024-01-01 23:59:59’
WHERE user_id != 10086 不等于(!=)会触发全表扫描 WHERE user_id < 10086 OR user_id > 10086(若必须用)
WHERE nickname LIKE ‘%小明’ 模糊查询开头带%,索引失效 WHERE nickname LIKE ‘小明%’(若业务允许)
GROUP BY user_id(无索引) 无索引时,GROUP BY会触发临时表 建索引idx_order_user(user_id),或先筛选再分组
SELECT COUNT(*) FROM order InnoDB中COUNT(*)需扫描数据,若仅需粗略计数 用EXPLAIN估算,或记录到单独统计表

五、工具加持:用EXPLAIN看透执行计划

优化SQL的核心工具是EXPLAIN,在SQL前加EXPLAIN,就能看到数据库“怎么执行这个查询”,从而定位问题。重点关注这3个字段:

1. type:查询类型,从好到差依次是:system > const > eq_ref > ref > range > ALL(全表扫描,要避免)

2. key:实际使用的索引,NULL表示未用索引

3. rows:预估扫描的行数,数值越小越好

只要这三个字段达标,SQL性能基本不会差。

六、总结:SQL优化的“三板斧”

  1. 建对索引:单字段索引适合单一条件查询,联合索引适合多条件查询(过滤性强的放前面),避免重复索引;

  2. 优化写法:小表驱动大表,避免索引失效写法,只查需要的字段;

  3. 用对工具:用EXPLAIN分析执行计划,定位全表扫描、索引失效等问题。

最后提醒:SQL优化不是“一次到位”,需要结合数据量变化、业务场景调整。比如初期数据量小时,索引可能看不出效果;数据量暴涨后,原来的索引可能又不够用。定期用EXPLAIN检查慢查询日志,才是长期保持高效的关键。

你在工作中遇到过哪些奇葩的SQL性能问题?欢迎在评论区分享你的踩坑经历和优化技巧~

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐