数据库慢查询日志是排查性能瓶颈最直接、最有效的手段。打开慢查询日志(slow query log),你会看到每条执行超过阈值时间的SQL语句、执行耗时、扫描行数、返回行数等关键信息。拿到这些数据后,核心工作就是三步:定位慢SQL、分析执行计划、重写SQL语句。下面我会从日志配置、日志解读、执行计划分析到SQL重写技巧,把整套流程讲透。

一、慢查询日志的开启与配置

MySQL默认可能没有开启慢查询日志,需要手动配置。在my.cnf或my.ini中添加以下参数:

slow_query_log = 1
slow_query_log_file = /var/log/mysql/slow.log
long_query_time = 2
log_queries_not_using_indexes = 1
log_throttle_queries_not_using_indexes = 60

long_query_time设为2秒,意味着超过2秒的SQL都会被记录。log_queries_not_using_indexes设为1,会把没有使用索引的查询也记录下来,这对发现隐式全表扫描非常有用。生产环境建议把阈值设为1秒甚至更低,避免漏掉中等程度的慢查询。PostgreSQL的做法类似,在postgresql.conf中设置:

log_min_duration_statement = 1000
log_checkpoints = on
log_lock_waits = on

PostgreSQL用毫秒为单位,1000即1秒。配置好后重启数据库服务,慢查询日志就开始工作了。

二、慢查询日志的关键字段解读

一条典型的MySQL慢查询日志长这样:

# Time: 2024-01-15T10:23:45.123456Z
# User@Host: app_user[app_user] @ localhost []
# Query_time: 3.523456  Lock_time: 0.000123  Rows_sent: 10  Rows_examined: 158234
SET timestamp=1705313025;
SELECT * FROM orders WHERE customer_name LIKE '%张%' AND status = 1 ORDER BY create_time DESC;

重点看四个字段:Query_time是执行耗时,Rows_examined是扫描行数,Rows_sent是返回行数,Lock_time是锁等待时间。如果Rows_examined远大于Rows_sent,说明大量无效扫描,索引大概率没用上。Lock_time高则说明存在锁竞争问题。时间戳帮你定位问题发生的时段,结合业务高峰判断是否是并发导致。

三、用EXPLAIN分析执行计划

找到慢SQL后,第一件事不是急着改,而是用EXPLAIN看执行计划。在SQL前面加EXPLAIN关键字执行:

EXPLAIN SELECT * FROM orders WHERE customer_name LIKE '%张%' AND status = 1 ORDER BY create_time DESC;

输出结果重点关注这几列:type列显示访问类型,从好到差依次是system > const > eq_ref > ref > range > index > ALL,出现ALL就是全表扫描,必须优化。key列显示实际使用的索引,如果是NULL说明没走索引。rows列是预估扫描行数,数字越大越慢。Extra列会出现"Using filesort"或"Using temporary",这两个都是性能杀手,意味着额外的排序或临时表操作。

MySQL 8.0以上还可以用EXPLAIN ANALYZE,它会真正执行SQL并给出实际运行数据,比EXPLAIN的估算更准确:

EXPLAIN ANALYZE SELECT * FROM orders WHERE customer_name LIKE '%张%' AND status = 1 ORDER BY create_time DESC;

四、SQL重写的核心原则与实战技巧

1. 避免SELECT *,只查需要的列

SELECT *会导致无法利用覆盖索引,还会增加网络传输和内存消耗。改成明确的字段列表:

-- 慢
SELECT * FROM orders WHERE status = 1;
-- 快
SELECT order_id, customer_id, amount, create_time FROM orders WHERE status = 1;

2. 优化LIKE模糊查询

前面带通配符的LIKE '%xxx%'会导致索引失效。如果业务允许,改成后缀匹配LIKE 'xxx%',这样可以走索引。或者使用全文索引(FULLTEXT)替代:

-- 慢:全表扫描
SELECT * FROM articles WHERE content LIKE '%数据库优化%';
-- 快:使用全文索引
SELECT * FROM articles WHERE MATCH(content) AGAINST('数据库优化' IN BOOLEAN MODE);

3. 用EXISTS替代IN子查询

当子查询结果集较大时,IN的性能会急剧下降,改成EXISTS通常更高效:

-- 慢
SELECT * FROM orders WHERE customer_id IN (SELECT id FROM customers WHERE level = 'VIP');
-- 快
SELECT * FROM orders o WHERE EXISTS (SELECT 1 FROM customers c WHERE c.id = o.customer_id AND c.level = 'VIP');

4. 避免在WHERE条件中对字段做函数运算

对索引列使用函数会导致索引失效,必须把运算移到等号右边:

-- 慢:索引失效
SELECT * FROM orders WHERE YEAR(create_time) = 2024;
-- 快:走范围索引
SELECT * FROM orders WHERE create_time >= '2024-01-01' AND create_time < '2025-01-01';

5. JOIN优化:小表驱动大表

JOIN时确保驱动表(小表)在前,被驱动表(大表)的关联字段有索引。同时避免过多表JOIN,一般控制在3-4张表以内。如果必须多表关联,考虑先用子查询把大表过滤后再JOIN:

-- 慢:直接大表JOIN
SELECT * FROM orders o JOIN order_items i ON o.id = i.order_id JOIN products p ON i.product_id = p.id WHERE o.status = 1;
-- 快:先过滤再JOIN
SELECT * FROM (SELECT id, customer_id, amount FROM orders WHERE status = 1) o 
JOIN order_items i ON o.id = i.order_id 
JOIN products p ON i.product_id = p.id;

6. 分页查询的深分页问题

LIMIT 100000, 10这种深分页会扫描前10万条再丢弃,非常慢。用游标分页或延迟关联替代:

-- 慢:深分页
SELECT * FROM orders ORDER BY id LIMIT 100000, 10;
-- 快:延迟关联
SELECT * FROM orders o INNER JOIN (SELECT id FROM orders ORDER BY id LIMIT 100000, 10) t ON o.id = t.id;
-- 或者用游标
SELECT * FROM orders WHERE id > 100000 ORDER BY id LIMIT 10;

7. 合理使用索引覆盖和联合索引

建立联合索引时遵循最左前缀原则。比如WHERE a = 1 AND b = 2 AND c = 3,建索引(a, b, c)三个条件都能用到。如果查询只涉及a和c,索引只能用到a。另外,把选择性高的列放在联合索引前面,选择性 = 不同值数量 / 总行数,越接近1越好。

五、日志分析工具推荐

手动看日志效率太低,推荐几个工具。mysqldumpslow是MySQL自带的慢查询日志分析工具,可以按查询时间、锁时间、扫描行数排序:

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

pt-query-digest是Percona Toolkit里的工具,功能更强大,能生成详细的分析报告。开源的还有pganalyze(针对PostgreSQL)、JetProfiler等。企业级可以考虑New Relic、Datadog这类APM工具,能自动抓取慢查询并给出优化建议。

六、建立长效监控机制

慢查询优化不是一次性工作。建议建立定期巡检机制:每天自动分析慢查询日志,周报汇总TOP 10慢SQL,月度做索引使用率评估。同时设置告警阈值,当慢查询数量突增或单条SQL耗时超过5秒时自动通知DBA。数据库版本升级后也要重新审视执行计划,因为优化器策略可能变化,原来走索引的SQL可能变成全表扫描。

七、容易被忽视的细节

很多人只关注SQL本身,忽略了数据量增长带来的问题。一条SQL在10万数据时很快,到1000万就可能变成灾难。所以优化要有前瞻性,提前评估数据增长曲线。另外,字符集不一致也会导致索引失效,比如一个字段是utf8mb4,另一个是utf8,JOIN时索引就用不上。还有,批量插入时不要逐条INSERT,用批量语句或LOAD DATA,能提升几个数量级的写入速度。

总结一下,数据库慢查询优化是一个系统工程:先开日志、再看执行计划、然后针对性重写SQL、最后建立监控闭环。掌握EXPLAIN分析、索引设计原则、SQL重写技巧这三板斧,能解决80%以上的慢查询问题。剩下的20%,往往需要从架构层面考虑分库分表、读写分离、缓存降级等方案。