3. SQL查询优化:EXPLAIN执行计划解读、慢查询日志分析、避免SELECT * 与索引失效场景

查询优化,说白了就是让数据库少干活。你写一条SQL,数据库怎么执行它?是走索引还是全表扫?这些信息都藏在执行计划里。我刚开始做优化时,也是两眼一抹黑,后来慢慢摸清了门道。今天咱们就聊聊这几个核心点。

3.1 EXPLAIN执行计划解读

EXPLAIN是MySQL里最常用的调优工具。你只要在SQL前面加上EXPLAIN,数据库就会告诉你它打算怎么干这活。我个人习惯,但凡线上要跑的SQL,都会先EXPLAIN一把看看。

核心字段解读:

  • type:访问类型,从好到差依次是 system > const > eq_ref > ref > range > index > ALL。ALL就是全表扫,能避免就避免。
  • key:实际使用的索引。如果为NULL,说明没走索引。
  • rows:预估扫描的行数。这个数字越小越好。
  • Extra:额外信息,比如Using filesort、Using temporary都是性能杀手。

举个例子,你写个查询:

EXPLAIN SELECT * FROM orders WHERE order_date = '2024-01-01';

如果type是ALL,rows是几十万,那基本可以断定这条SQL要出问题。我在项目中遇到过,一个报表查询跑了30秒,一看就是全表扫。加了个索引后,直接降到0.1秒。

小技巧:EXPLAIN的结果只是预估,不是实际执行。想看到真实执行情况,可以用EXPLAIN ANALYZE(MySQL 8.0.18+)。

3.2 慢查询日志分析

慢查询日志,就是数据库帮你记下来的那些跑得慢的SQL。你想想看,如果没人告诉你哪条SQL慢,你怎么优化?

开启慢查询日志很简单:

-- 查看是否开启
SHOW VARIABLES LIKE 'slow_query_log';

-- 开启慢查询日志
SET GLOBAL slow_query_log = 'ON';

-- 设置阈值(超过2秒就算慢)
SET GLOBAL long_query_time = 2;

日志文件默认在数据目录下,名字一般是主机名-slow.log。打开后你会看到类似这样的内容:

# Time: 2024-01-15T10:30:00
# Query_time: 5.234  Lock_time: 0.001
# Rows_sent: 100  Rows_examined: 500000
SELECT * FROM orders WHERE status = 'pending';

这里Query_time是5.234秒,Rows_examined是50万行,但只返回了100行。这说明什么?扫描了大量数据,但只返回了少量结果。典型的索引缺失。

注意:慢查询日志在生产环境要谨慎开启,尤其是磁盘IO紧张的时候。我曾经在高峰期开了慢查询日志,结果日志写入把磁盘打满了...嗯,从那以后我都是低峰期才开。

分析慢查询,我一般用mysqldumpslow工具:

mysqldumpslow -s t -t 10 /var/lib/mysql/slow.log

这个命令按查询时间排序,显示最慢的10条SQL。省得你一条条翻日志。

3.3 避免SELECT *

SELECT * 是新手最爱,也是性能杀手。为什么?

  • 它会把所有字段都查出来,哪怕你只需要两个字段。
  • 它没法利用覆盖索引。覆盖索引的意思是,索引里已经包含了你要的数据,不用回表查。但SELECT * 通常需要回表。
  • 它增加了网络传输量。字段越多,数据包越大。

举个例子:

-- 不推荐
SELECT * FROM users WHERE age > 18;

-- 推荐
SELECT id, name, age FROM users WHERE age > 18;

如果users表有20个字段,但你只需要3个,SELECT * 就多查了17个字段。数据量大了以后,这个差距非常明显。我记得有个项目,接口响应慢,查了半天发现是SELECT * 惹的祸。改成指定字段后,响应时间从2秒降到了0.3秒。

习惯养成:写SQL时,先想清楚要哪些字段,再写SELECT。别偷懒用星号。

3.4 索引失效场景

索引建了,但SQL没走索引,这种情况我见得太多了。下面列几个常见的坑:

场景 错误写法 正确写法
对索引列使用函数 WHERE DATE(create_time) = '2024-01-01' WHERE create_time >= '2024-01-01' AND create_time < '2024-01-02'
隐式类型转换 WHERE phone = 13800138000(phone是varchar) WHERE phone = '13800138000'
LIKE以通配符开头 WHERE name LIKE '%张三%' WHERE name LIKE '张三%'
OR条件中有非索引列 WHERE age = 18 OR status = 1(status无索引) 拆成两个查询用UNION,或者给status加索引
联合索引未用最左前缀 索引(a,b,c),但WHERE b = 1 按最左前缀使用,比如WHERE a = 1 AND b = 1

这里重点说一下联合索引。你建了(a,b,c)的联合索引,但查询条件是b = 1,那这个索引就用不上。因为MySQL是从左到右匹配的。我有个同事就踩过这个坑,建了联合索引但查询条件没带最左列,结果全表扫,业务直接卡死。

避坑指南:我曾经在线上环境遇到一个慢查询,排查了半天才发现是隐式类型转换。字段是varchar,但传参传了数字,MySQL做了类型转换,索引就失效了。所以写SQL时,类型一定要匹配。

还有一个容易被忽略的点:数据分布不均匀。比如性别字段,只有男和女两种值,就算建了索引,MySQL也可能不走。因为它觉得全表扫更快。这种情况,索引其实没什么用。

3.5 总结

查询优化,说白了就是三件事:看懂执行计划、用好慢查询日志、避免踩索引的坑。我个人习惯,每次上线前都会过一遍慢查询日志,看看有没有新增的慢SQL。发现问题及时处理,别等用户投诉了再救火。

嗯,今天就聊到这儿。下一章咱们聊聊分库分表,那个坑更多,到时候再细说。