慢查询日志开了却看不到内容:90% 是阈值和权限问题
个人站长优化数据库,第一步永远是打开慢查询日志。但真正操作过的人都知道,slow_query_log = ON 写进 my.cnf 之后,MySQL 重启了,文件也创建了,却一直是空的。这个时候多数人会在网上搜到"重启 MySQL 服务"这种废话建议,然后浪费一整个下午。这篇文章把慢查询日志从开启、分析到真正定位问题的完整链路讲清楚,重点放在那些"配置看起来对但就是不生效"的排查方法上。
先确认日志到底开没开、写到哪了
不要相信配置文件,要相信运行时状态。登录 MySQL 执行:
SHOW VARIABLES LIKE 'slow_query_log%';
SHOW VARIABLES LIKE 'long_query_time';
SHOW VARIABLES LIKE 'log_output';你会看到类似这样的输出:
+---------------------+---------------------------+
| Variable_name | Value |
+---------------------+---------------------------+
| slow_query_log | ON |
| slow_query_log_file | /var/lib/mysql/slow.log |
+---------------------+---------------------------+
+------------------+----------+
| Variable_name | Value |
+------------------+----------+
| long_query_time | 10.000000|
+------------------+----------+
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| log_output | FILE |
+---------------+-------+这里第一个坑就是 long_query_time 的默认值 10 秒。一个正常的网站查询,如果跑到 10 秒才记录,那站点早就挂了,你根本不会等到看日志就发现了。所以日志空着的最大原因就是:你的慢查询还不够"慢",没到阈值。个人站应该设成 1 秒,甚至 0.5 秒,先看清楚有哪些查询偏慢,再逐步收紧。
第二个坑是 log_output。如果它是 TABLE,日志会写进 mysql.slow_log 表,而不是文件,你去 /var/lib/mysql/slow.log 里当然什么也找不到。TABLE 模式的缺点很明显:每写一条慢查询就是一次 INSERT,本身会加重数据库负担,而且表会越来越大。建议明确设成 FILE。
运行时动态开启(不用重启)
调整这些变量绝大多数不需要重启 MySQL,用 SET GLOBAL 即可,立即生效。这在生产环境特别有用:
SET GLOBAL slow_query_log = 'ON';
SET GLOBAL long_query_time = 1;
SET GLOBAL log_output = 'FILE';
SET GLOBAL slow_query_log_file = '/var/log/mysql/slow.log';
SET GLOBAL log_queries_not_using_indexes = 'ON';注意 SET GLOBAL 只影响之后建立的新连接,已经存在的连接仍用旧值。对于 long_query_time 来说这一点尤其容易让人困惑——你在当前会话里 SELECT @@long_query_time 看到的可能还是 10。想看全局值要用 SELECT @@global.long_query_time。
还有一个真正隐蔽的坑:log_queries_not_using_indexes 打开后会让日志爆炸。所有没走索引的查询都会被记录,包括那些在小表上全表扫描只要 0.1ms 的查询。开了这个选项,日志文件会以每秒几十条的速度增长,磁盘很快写满。正确的用法是:只在排查阶段临时打开几分钟,抓到线索后立刻关掉。
写进配置文件永久生效
动态设置重启就丢了,所以要固化到配置文件。Debian/Ubuntu 下是 /etc/mysql/mysql.conf.d/mysqld.cnf(或 /etc/mysql/my.cnf),CentOS 下是 /etc/my.cnf。注意必须写在 [mysqld] 段落里,写到 [client] 段是无效的(这是新手最常见的错误之一):
[mysqld]
slow_query_log = 1
slow_query_log_file = /var/log/mysql/slow.log
long_query_time = 1
log_output = FILE
log_queries_not_using_indexes = 0
min_examined_row_limit = 100这里引入一个非常有用的参数 min_examined_row_limit:只有当查询扫描的行数超过这个值时才记录。设成 100 的含义是"扫描少于 100 行的慢查询不用记",它能有效过滤掉那些"逻辑上慢但实际无害"的查询,让日志聚焦在真正的问题上。这是把 log_queries_not_using_indexes 从"噪音制造机"变成"有用工具"的关键搭配。
改完配置文件,别用 kill -9 粗暴重启,用 systemctl:
systemctl restart mysql
systemctl status mysql --no-pager权限问题:为什么日志文件创建了但一直是 0 字节
这是最让人抓狂的一类问题。日志文件确实存在,路径也对,权限看起来也没问题,但就是空的。排查思路如下:
第一,确认 mysql 用户对该目录有写权限。比如你把日志指到 /var/log/mysql/,需要确认这个目录的属主是 mysql:mysql:
ls -ld /var/log/mysql/
chown mysql:mysql /var/log/mysql/
chmod 755 /var/log/mysql/第二,注意 AppArmor / SELinux。Debian 系上 AppArmor 默认会限制 mysqld 只能写特定路径。如果你把 slow log 指到了 AppArmor 规则之外的地方(比如 /data/mysql-slow.log),日志只会静默地写不进去,而且不会报错。检查方法:
dmesg | grep -i apparmor | tail -20
aa-status | grep mysqld看到 DENIED 字样就说明被拦了。修改 /etc/apparmor.d/usr.sbin.mysqld 添加路径,然后 systemctl reload apparmor。CentOS 上则是 SELinux:
ls -Z /var/log/mysql/slow.log
# 正确应该带有 mysqld_log_t 上下文
restorecon -Rv /var/log/mysql/第三,确认 log_output 真的改成了 FILE。不少人以为改配置文件就行,但配置文件里可能同时存在两处 log_output(比如 /etc/my.cnf 和 /etc/mysql/conf.d/ 下都有),MySQL 读取顺序里后读的覆盖先读的,导致你改的那份没生效。用 mysqld --help --verbose | grep -A2 "Default options" 可以看到配置文件的加载顺序。
用 mysqldumpslow 快速看排行
慢查询日志是纯文本,人工翻看效率极低。MySQL 自带的 mysqldumpslow 可以把相同"查询模式"的语句聚合起来排序:
# 按平均耗时排序,显示前 15 条
mysqldumpslow -s at -t 15 /var/log/mysql/slow.log
# 按总耗时排序(能发现"单次不快但调用极其频繁"的查询)
mysqldumpslow -s t -t 15 /var/log/mysql/slow.log
# 只看 SELECT,忽略数字差异
mysqldumpslow -s c -t 10 -g "SELECT" /var/log/mysql/slow.log参数含义:-s 指定排序字段(at=平均耗时、t=总耗时、c=出现次数、l=锁时间),-t 是取前 N 条,-g 是正则过滤。它会把 WHERE id = 5 和 WHERE id = 99 归一化成同一个模式,这正是我们想要的——要优化的是查询模式,不是某一次具体调用。
如果日志很大(几百 MB),用 -a 会让它显示具体的数字而不是 N/S 占位符,但输出会变得非常长,通常不需要。
pt-query-digest:更专业的选择
Percona Toolkit 里的 pt-query-digest 比 mysqldumpslow 强得多,它会输出一份带百分比、响应时间分布、以及"这个查询占了多少总耗时"的完整报告:
# 安装(Debian/Ubuntu)
apt-get install percona-toolkit
# 生成报告
pt-query-digest /var/log/mysql/slow.log > /tmp/slow_report.txt
# 只看前 20 条
pt-query-digest --limit 20 /var/log/mysql/slow.log报告里最该关注的是第一段 "Profile" 表格里的 Response time 和 Calls 两列。一个查询如果只占总时间的 0.5%,那优化它的性价比就很低;如果某个查询占了 40%,那它是明确的第一优先级。这个视角比"哪个查询最慢"有用得多,因为一个耗时 3 秒但一天只跑一次的查询,远不如一个耗时 0.2 秒但每秒跑 50 次的查询值得优化。
从 EXPLAIN 到索引:闭环的最后一步
日志只能告诉你哪个查询慢,不能告诉你为什么慢。拿到慢查询之后,用 EXPLAIN 看执行计划:
EXPLAIN SELECT id, title FROM typecho_contents
WHERE type = 'post' AND created > 1758000000
ORDER BY created DESC LIMIT 10;重点看 type、key、rows、Extra 四列:
type为ALL表示全表扫描,是最差的情况;index是全索引扫描,range是范围扫描,ref是等值匹配,const最好。key为 NULL 说明没用上索引。rows是预估扫描行数,数字越大越要警惕。Extra里出现Using filesort表示需要额外排序,Using temporary表示用了临时表——这两个都是性能红灯,尤其是在 ORDER BY + LIMIT 的组合里。
一个典型的个人站场景:文章列表按发布时间倒序取 10 条,如果 created 字段没索引,MySQL 要把整张表读出来做 filesort,几千篇文章还不明显,几万篇就会出现明显的秒级延迟。加一个 KEY created (created) 就能把 filesort 消除掉。
但注意前面写过的最左前缀原则:如果你已经有 KEY type_created (type, created) 这样的复合索引,那 WHERE type = 'post' ORDER BY created DESC 是可以完全走索引的,不需要再单独建 created 索引。
长期策略:把它变成定期巡检
慢查询日志不该是"出问题才开"的应急工具,而应该是常态化的健康指标。我的做法是每周日跑一次 pt-query-digest,把报告存到 /root/reports/slow-$(date +%Y%m%d).txt,对比上一周的变化。如果某个查询的占比突然上升,通常意味着数据量增长到了一个临界点,或者某个功能上线后引入了坏 SQL。用 crontab 自动化:
# 每周日凌晨 3 点生成慢查询报告并轮转日志
0 3 * * 0 pt-query-digest --limit 20 /var/log/mysql/slow.log > /root/reports/slow-$(date +\%Y\%m\%d).txt && mysql -e "SET GLOBAL slow_query_log='OFF'; SET GLOBAL slow_query_log='ON';"最后一行通过"关掉再打开"来清空日志文件,是 MySQL 里轮转 slow log 的常用技巧(等价于外部 mv slow.log slow.log.old 后 FLUSH LOGS)。注意别用 rm slow.log 删文件——MySQL 持有文件描述符,删除后它会继续往那个"已消失"的文件句柄写,磁盘空间不会释放,这就是之前文章里讲的 lsof +L1 场景之一。
小结
慢查询日志之所以"开了没内容",九成是 long_query_time 阈值过高、log_output 指向了表、或者文件权限/AppArmor 拦截。把阈值降到 1 秒,配合 min_examined_row_limit 100 过滤噪音,用 pt-query-digest 按"总耗时占比"排序,而不是按"单次最慢"排序,最后用 EXPLAIN 验证索引是否真的被用上。这条路走通之后,数据库优化就不再是靠感觉猜了。