慢查询日志不是打开就完事
MySQL 的慢查询日志大概是所有站长最早听说、最少认真用的功能之一。很多人打开它,等了几天,发现文件里要么空空如也,要么塞满几千条一模一样的语句,最后干脆关掉了事。问题不在日志本身,而在于阈值设置和读取方式:long_query_time 默认是 10 秒,可一个正常的网页查询超过 200 毫秒就已经值得关注;而如果不做汇总直接用 tail 看,你看到的永远是最新那几十条,根本没有宏观视角。
这篇文章讲一套在个人站上真正跑得通的做法:怎么设阈值、怎么用 mysqldumpslow 和 performance_schema 做汇总、怎么读懂 EXPLAIN 的关键列、以及最常遇到的几种索引失效场景怎么修。所有示例基于 MySQL 8.0,运行环境假设是一台 2 核 4G 的云服务器,这也是个人站最常见的配置。
第一步:把慢查询日志参数配对
在 /etc/mysql/mysql.conf.d/mysqld.cnf 的 [mysqld] 段里加:
slow_query_log = 1
slow_query_log_file = /var/log/mysql/slow.log
long_query_time = 0.5
log_queries_not_using_indexes = 0
min_examined_row_limit = 1000
log_slow_admin_statements = 1
log_slow_replica_statements = 1几个参数的选择理由:
long_query_time = 0.5:半秒是个实用起点。10 秒的默认值在网站场景里意味着用户已经关掉页面了。如果日志量太大,可以再放宽到 1 秒,但不要一上来就设成 5 秒。log_queries_not_using_indexes = 0:新手常把它打开,结果小表全表扫描也记进日志,一天几万条噪音,真正的问题被淹没。只有在专门排查索引缺失时才临时打开。min_examined_row_limit = 1000:扫描行数少于 1000 的查询不记录。这一条能过滤掉绝大多数无意义的小表全扫。
改完配置要重启而不是 reload,因为这个参数不支持动态修改:
systemctl restart mysql
mysql -e "SHOW VARIABLES LIKE 'slow_query%'; SHOW VARIABLES LIKE 'long_query_time';"如果是线上不方便重启,也可以用运行时变量打开,但重启后会失效,只适合临时排查:
SET GLOBAL slow_query_log = ON;
SET GLOBAL long_query_time = 0.5;
SET GLOBAL log_output = 'FILE';第二步:不要 tail,要汇总
慢查询日志的格式是每条记录带 Query_time、Lock_time、Rows_sent、Rows_examined,语句本身可能跨多行。直接看几乎无法分析,官方自带的 mysqldumpslow 是最好的起点:
# 按总耗时排序,看最烧时间的前 20 条
mysqldumpslow -s t -t 20 /var/log/mysql/slow.log
# 按平均耗时排序
mysqldumpslow -s at -t 20 /var/log/mysql/slow.log
# 按扫描行数排序,找扫描量最大的
mysqldumpslow -s r -t 20 /var/log/mysql/slow.logmysqldumpslow 会自动把参数替换成 N 和 'S',所以上面这些「同一个 SQL 不同参数」的记录会被聚合成一条,这才是你需要的视角。输出里重点看三列:
- Count:出现次数。次数高且单次不慢的,往往比次数少但很慢的更有优化价值。
- Time:总耗时。这是优先级排序最直接的指标。
- Rows:平均扫描行数 / 平均返回行数。这两个数的比值是最重要的信号——如果扫描 10 万行只返回 10 行,说明索引非常低效。
示例输出:
Count: 1843 Time=2.31s (4257s) Lock=0.00s Rows=12.4 (22853), 网站用户[www]@localhost
SELECT * FROM wp_postmeta WHERE meta_key='N' AND meta_value='S'这条记录一眼就能定性:1843 次,平均扫描 22853 行只返回 12 行。wp_postmeta 上 meta_value 没有索引,这是 WordPress 最经典的问题。
第三步:用 performance_schema 看实时汇总
mysqldumpslow 是离线的,需要等日志积累。如果想看「现在正在发生什么」,用 performance_schema 的语句摘要表:
SELECT DIGEST_TEXT,
COUNT_STAR AS calls,
ROUND(SUM_TIMER_WAIT/1e12, 2) AS total_sec,
ROUND(AVG_TIMER_WAIT/1e9, 2) AS avg_ms,
SUM_ROWS_EXAMINED AS rows_examined,
SUM_ROWS_SENT AS rows_sent,
SUM_NO_INDEX_USED AS no_idx
FROM performance_schema.events_statements_summary_by_digest
WHERE SCHEMA_NAME = '你的数据库名'
ORDER BY SUM_TIMER_WAIT DESC
LIMIT 15;这张表的好处是已经按 SQL 指纹聚合过,而且带 SUM_NO_INDEX_USED——直接告诉你哪些语句没走索引。把 ORDER BY 换成 SUM_ROWS_EXAMINED DESC 就是另一份视角,专门找「读了很多但没返回」的语句。
还有一个非常实用的实时视图,能抓到「此刻正在执行且已经跑了很久」的查询:
SELECT ID, USER, TIME, STATE, LEFT(INFO,120) AS sql_text
FROM information_schema.PROCESSLIST
WHERE COMMAND != 'Sleep' AND TIME > 3
ORDER BY TIME DESC;发现长查询之后,先别急着 kill。kill 一个正在做大量写入的事务可能触发回滚,回滚时间往往比原查询还长。正确顺序是先 EXPLAIN 看它为什么慢,判断能不能等它自己跑完。
第四步:读懂 EXPLAIN 的几个关键列
找到目标语句后,EXPLAIN 是下一步。MySQL 8 的 EXPLAIN ANALYZE 会实际执行并给出真实耗时,比传统 EXPLAIN 更准,但只在能安全执行的查询上用:
EXPLAIN ANALYZE SELECT * FROM wp_postmeta
WHERE meta_key = '_thumbnail_id' AND post_id = 12345;传统 EXPLAIN 输出里,只需要盯死三列:
type 列:访问类型
从好到坏大致是:system > const > eq_ref > ref > range > index > ALL。
const:主键或唯一索引等值查询,最多一行,最理想。ref:普通索引等值查询,正常业务查询的常见形态。range:索引范围扫描,BETWEEN、>、IN之类。可接受。index:扫描整棵索引树,比全表扫稍好但通常也要优化。ALL:全表扫描。小表无所谓,十万行以上的表出现在这里就要处理。
rows 列:预估扫描行数
这是优化器估计要读多少行。如果 rows 很大而实际返回很少,说明索引选择性差或者统计信息过期。统计信息过期的典型症状是:明明建了索引,优化器却选全表扫描。这时执行 ANALYZE TABLE 表名; 往往能让执行计划立刻改变。
Extra 列:额外信息
这一列信息量最大,几个关键词必须认识:
Using index:覆盖索引,只读索引不回表,非常好。Using where:server 层在拿到数据后又过滤了一遍,配合rows大就要注意。Using filesort:需要额外排序,无法利用索引顺序。数据量大时非常慢。Using temporary:用了临时表,常见于GROUP BY和DISTINCT,数据量大时会写磁盘。Using index condition:索引下推,通常是好事。
Using filesort 和 Using temporary 同时出现,基本可以判定这条 SQL 在数据量涨上来之后一定会出问题。
最常见的四种索引失效场景
1. 复合索引的最左前缀被破坏
建了 idx(a, b, c),查询写成 WHERE b = 1 AND c = 2,索引完全用不上。复合索引遵循最左前缀原则:只有从 a 开始连续使用的列才能利用索引。所以复合索引的列顺序要按「查询中使用频率 + 选择性」来排,最常用的等值条件放最左。
另外,WHERE a = 1 AND c = 2 这种跳过 b 的写法,a 能用上索引,c 不行(MySQL 8 的索引下推能让 c 在索引层过滤,但依然是扫描)。
2. 对索引列做函数运算或隐式转换
-- 失效:索引列被函数包裹
SELECT * FROM posts WHERE DATE(created_at) = '2026-10-18';
-- 改写:范围查询,可用索引
SELECT * FROM posts WHERE created_at >= '2026-10-18 00:00:00'
AND created_at < '2026-10-19 00:00:00';隐式转换更隐蔽。user_id 是 varchar,查询写 WHERE user_id = 123,MySQL 会把列转成数字比较,索引失效。反过来,如果列是 int 而传字符串 '123',MySQL 会把字符串转成数字,索引仍然可用。所以规则是:参数类型必须和列类型一致,尤其是 varchar 列千万不要传数字。
3. LIKE 以通配符开头
WHERE title LIKE '%关键词%' -- 索引失效
WHERE title LIKE '关键词%' -- 可以使用索引前缀通配符无法利用 B+ 树的顺序性。真要做全文搜索,MySQL 8 的 FULLTEXT 索引(需要 ngram 分词器支持中文)或者外接 Meilisearch 都比 LIKE '%x%' 靠谱得多。
4. 隐式排序导致 filesort
分页查询最常见的写法 ORDER BY created_at DESC LIMIT 20,如果 created_at 没索引,每次都要把整表读出来排序。加上索引之后,如果 WHERE 条件和 ORDER BY 列能组成一个复合索引,排序就能直接利用索引顺序。这也是为什么 idx(status, created_at) 这样的组合索引比单独两个索引更好用。
一个真实案例:WordPress 的 wp_postmeta
前面那条 1843 次、平均扫描两万行的查询,是很典型的 WordPress 慢查询。修复方式不是加索引(meta_value 是 longtext,加 B-Tree 索引无效),而是改查询结构或者加针对性索引:
-- 针对 meta_key + post_id 的组合查询
ALTER TABLE wp_postmeta ADD INDEX idx_key_post (meta_key(32), post_id);
-- 检查索引效果
EXPLAIN SELECT * FROM wp_postmeta
WHERE meta_key = '_thumbnail_id' AND post_id = 12345;注意 meta_key(32) 是前缀索引:只索引前 32 个字符。对于 key 名普遍很短的情况足够用,而且能显著减小索引体积。前提是前 32 字符的选择性足够,可以先用这个语句验证:
SELECT COUNT(DISTINCT LEFT(meta_key,32)) / COUNT(*) AS selectivity FROM wp_postmeta;结果越接近 1 越好。低于 0.9 就说明前缀不够长。
优化之后怎么确认真的变快了
改完索引不要凭感觉。清空统计、跑一段时间、再看同一张摘要表:
TRUNCATE performance_schema.events_statements_summary_by_digest;
-- 等一段时间业务跑过之后
SELECT DIGEST_TEXT, COUNT_STAR,
ROUND(AVG_TIMER_WAIT/1e9,2) AS avg_ms,
SUM_ROWS_EXAMINED/COUNT_STAR AS avg_rows
FROM performance_schema.events_statements_summary_by_digest
ORDER BY SUM_TIMER_WAIT DESC LIMIT 10;对比优化前后的 avg_ms 与 avg_rows,两个数都应该明显下降。如果只有时间下降而行数没变,说明改善来自缓存而不是索引,不能算真优化。
小结
慢查询治理的完整链条是:设对阈值 → 用 mysqldumpslow 或 performance_schema 做汇总找出 TOP 语句 → 用 EXPLAIN 定位 type/rows/Extra 的问题 → 针对性改索引或改查询写法 → 数字化验证效果。
对个人站来说,真正需要优化的 SQL 永远不会超过十条。把这几条找出来、修掉,网站的后台响应速度和数据库压力就能有一个数量级的改善,而这往往比升级服务器配置划算得多。慢查询日志的价值不在于「开着」,而在于你有没有把它当成一个每周固定读一遍的运维材料。