WordPress 数据库慢查询排查(2026):1734 条记录里,最慢那条在等锁

上周整理服务器,顺手翻了一眼 MySQL 的慢日志,文件 916KB,里面攒了 1800 条记录,最早的一条是 2023 年 11 月,最新的是前天。站点本身没感觉慢,未缓存首页也就 0.7 秒上下,但这堆记录总得有个说法,不然下次真出问题又得从头猜。

翻完的结论有点反直觉:最慢的那条不在查表,在等锁;真正值得动手的是另一批老式查询,还有 wp_options 里一个 1.85MB 的自动加载项。

先看库有多大,哪张表最胖

WP-CLI 自带一个查表大小的子命令,不用连数据库:

wp db size --tables
Name                Size
wp_options          4243456 B
wp_posts            3817472 B
wp_postmeta          704512 B
wp_usermeta          360448 B

整库 10.6MB,wp_options 和 wp_posts 占了八成,这个比例对 WordPress 来说正常。想看得再细一点,包括行数和碎片,可以问 information_schema:

wp db query "SELECT table_name, ROUND((data_length+index_length)/1024) kb, table_rows \
FROM information_schema.tables WHERE table_schema='littlesheep_cc' \
ORDER BY (data_length+index_length) DESC LIMIT 5"
wp_options    4144 kb   282
wp_posts      3728 kb   448
wp_postmeta    688 kb  2184

10MB 的库怎么都不会是瓶颈,内存里 256MB 的 buffer pool 能把整库装进去躺着。所以慢日志的问题不在容量,在别处。

慢日志里最慢的那条,在等锁

宝塔装的 MySQL 默认开了慢日志(/etc/my.cnf 里 slow_query_log=1),文件在 /www/server/data/mysql-slow.log,long_query_time=3,也就是超过 3 秒才记。日志格式固定,# Query_time 那一行同时带着 Lock_time:

awk '/^# Query_time:/ {qt=$3; lt=$5; getline; getline sql; \
printf "%9.2f %9.2f  %s\n", qt, lt, substr(sql,1,55)}' \
/www/server/data/mysql-slow.log | sort -rn | head -8

  5705.62   5695.03  SELECT option_name, option_value FROM wp_options WHERE
  5705.34   5694.81  SELECT option_name, option_value FROM wp_options WHERE
  3084.33      0.00  # administrator command: Quit;
  1734.78      0.00  SELECT   wp_posts.ID
   860.43      0.00  SELECT SQL_CALC_FOUND_ROWS  wp_posts.ID

第一、二名都是 5705 秒,看着像一条 SQL 卡了 95 分钟。但 Lock_time 是 5694 秒,也就是说 99.8% 的时间在等表锁,不是在扫数据(Rows_examined 只有 203 行)。这两条的时间戳是 2023 年 12 月 22 日,那天正好是备份任务在跑,mysqldump 锁表期间所有查询排队的记录。

这类记录别当性能问题处理,该做的是把备份挪到凌晨并把锁方式改掉,我在全站自动备份那篇里写过怎么改。

真正属于「SQL 写得不好」的是另外一族,换个聚合方式就看到了:

awk '/^# Query_time:/ {qt=$3; re=$0; sub(/.*Rows_examined: /,"",re); \
getline; getline sql; if (sql ~ /SQL_CALC_FOUND_ROWS/) {n++; t+=qt; \
if(qt>m)m=qt; e+=re}} END {printf "n=%d avg=%.2fs max=%.2fs examined=%.0f\n", \
n, t/n, m, e/n}' /www/server/data/mysql-slow.log

n=609 avg=8.88s max=860.43s examined=558

609 条 SELECT SQL_CALC_FOUND_ROWS wp_posts.ID,平均 8.88 秒,平均扫 558 行。这是老式 WP_Query 分页查询的写法,先查一遍列表再算总数。我这边跑的是 WordPress 6.9,核心文件里还留着这个写法,多半是子比主题或者某个插件用自己的 SQL 在查列表,升级 WP 不会自动修掉它。查清是哪个插件之后,要么换更省的查询,要么给这张表补上合适的复合索引。

日志顺手提醒一句:里面混着 # administrator command: Quit;、SET timestamp=... 这种客户端输出,不是 SQL。聚合的时候先过滤掉,不然 top 榜会被噪声占掉半屏。

wp_options 里藏着一个 1.85MB 的备份

关键词是 autoload。WordPress 每次请求都会把所有 autoload 的 option 一次性读出来反序列化,所以这一栏越肥,每个页面都要多扛一份。

wp db query "SELECT option_name, ROUND(LENGTH(option_value)/1024) kb FROM wp_options \
WHERE autoload IN ('yes','on','auto','auto-on') ORDER BY LENGTH(option_value) DESC LIMIT 3"

zibll_options_backup   1849
zibll_options           123
ACG                      16

zibll_options_backup 一行 1849KB,是子比主题保存的一份选项快照,等于每次打开页面都把整个主题配置备份读进来再丢掉。把它按未压缩 JSON 导出一份留底(我这边 1.66MB),然后关掉自动加载:

wp option get zibll_options_backup --format=json > /root/zibll-options-backup-20261013.json
wp option set-autoload zibll_options_backup off

wp db query "SELECT ROUND(SUM(LENGTH(option_value))/1024) kb FROM wp_options \
WHERE autoload IN ('yes','on','auto','auto-on')"
kb
216

自动加载总量从 2065KB 掉到 216KB,减掉的 1849KB 就是那一行。option 本身没删,需要它的代码照样按名字取,只是不再跟着每个请求预加载。

验证:两个数字的意思完全不一样

改完我想看首页快不快了,5 次未缓存请求(带随机参数绕开缓存)跑下来:改前 0.85/0.82/0.73/0.75/0.74 秒,改后 0.92/0.71/0.69/0.82/0.77 秒。两组数据完全重叠,看不出提升。

原因不复杂,这个表常驻 buffer pool,省下的是一次无意义的大 option 反序列化,不是省了磁盘 IO。真正吃掉那 0.7 秒的是 PHP 执行和主题模板。所以这次改动的收益是「少做一件没意义的事」,不是「网站变快了」,宣传成后者就是骗自己。

顺手跑了 wp db optimize,整库从 10.61MB 降到 9.36MB。输出里每张表都写着 Table does not support optimize, doing recreate + analyze instead——InnoDB 不认这条命令的默认语义,实际是重建表再分析统计信息,会短暂锁表,别在访问高峰做。

坑和回滚

回滚就一条命令,wp option set-autoload zibll_options_backup on,我试过,自动加载总量立刻回到 2065KB,站点全程 200。有个坑是官方文档和网上教程常写 auto,但 WP-CLI 只认 on/off/yes/no:

wp option set-autoload zibll_options_backup auto
Error: Invalid value specified for positional arg.

auto 是 WordPress 6.6 之后表里能存的值之一(同批还加了 auto-on,并且新写入超过 150KB 的 option 不再默认自动加载),但 WP-CLI 这个子命令的取值没跟着放开,手写 SQL 时别把两套搞混。老库里还留着一批 yes/no,含义等同于 on/off,统计时要一起算进去,只查 autoload='yes' 会漏。wp option set-autoload 从 WP-CLI 2.8.0 起才有,版本太老就先升 WP-CLI。

顺便说三件不用做的事

wp_postmeta 只有 2184 行,网上那些「给 meta_key 加索引提速」的文章可以关掉了,行数这么少加索引只会拖慢写入。孤儿 postmeta 查出 2 条,过期 transient 是 0 条——0 条的意思是别照抄清理脚本,先把数量查出来再决定。

数据库只是服务器上的一环,占磁盘的大头往往在别处,比如我上次查出来的那个 4.4G 的监控库文件;而数据库压力本身,我这边主要靠 Redis 对象缓存挡在前面,命中率上去之后 mysqld 的负载会明显降。

慢日志看久了会有点上瘾,但真正管用的动作就那么几个:分清等锁和慢查,认清哪些 SQL 是自己写坏的,再确认 autoload 里没躺着巨物。

© 版权声明
THE END
喜欢就支持一下吧
点赞12 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容