网站运维

MySQL 慢查询拖垮独立站:从定位到优化的完整过程

网站打开要等五六秒,你加了缓存、换了 CDN、压了图片,还是慢。

这时候大概率是数据库在拖后腿。WordPress 的每一次页面加载都要查几十上百次数据库,其中只要有一条 SQL 写得不好或者没走索引,整个页面就跟着慢。

这篇文章用一个真实的排查思路,讲清楚怎么找到问题、怎么改。

一、先确认瓶颈是不是在数据库

别上来就查慢日志。先做个快速判断。

看服务器负载

top
  • mysqld 长期占 CPU 很高 → 数据库在拼命算,基本确认是它
  • %wa(iowait)很高 → 磁盘 I/O 瓶颈,可能是数据库在大量读写临时表
  • CPU 都是 php-fpm 占的 → 那问题在 PHP 层面,不是数据库,这篇不适用

装个插件直接看(最简单)

装 Query Monitor 插件,前台打开页面,看顶部工具栏:

  • Page Generation Time:总耗时
  • Database Queries:查询次数和耗时
  • 点进去能看到最慢的那几条 SQL 原文,以及是哪个插件/主题发起的

正常水平:

指标 健康 需要处理
单次页面查询次数 30-80 次 > 200 次
数据库总耗时 < 100ms > 500ms
单条最慢查询 < 50ms > 500ms

如果查询次数超过 300,通常是装了太多插件或者某个插件设计得很差。如果单条查询上秒,那就是索引问题。

二、开启慢查询日志,拿到证据

Query Monitor 只能看当下这个页面,要全面排查还得靠慢查询日志。

开启

在 MySQL 配置文件(/etc/my.cnf 或宝塔里的 MySQL 配置)加:

[mysqld]
slow_query_log = 1
slow_query_log_file = /www/server/data/mysql-slow.log
long_query_time = 1        # 超过 1 秒的记录下来
log_queries_not_using_indexes = 1

重启 MySQL 生效。

注意:生产环境建议 long_query_time 设 1-2 秒,别设 0.1——日志会爆炸。排查期间可以临时调小,排查完调回来。

看日志

跑一天之后看:

# 看有多少条慢查询
wc -l /www/server/data/mysql-slow.log

# 用 mysqldumpslow 汇总(按耗时排序,取前 10)
mysqldumpslow -s t -t 10 /www/server/data/mysql-slow.log

或者用更现代的 pt-query-digest(Percona Toolkit 里的工具),输出更详细:

pt-query-digest /www/server/data/mysql-slow.log | head -50

它会把相似的 SQL 归成一类,按总耗时排序。排第一的那个,就是你要优化的目标。

三、看懂执行计划:EXPLAIN

拿到慢 SQL 之后,不要凭感觉改。用 EXPLAIN 让 MySQL 告诉你它打算怎么执行:

EXPLAIN SELECT * FROM wp_posts WHERE post_type = 'product' AND post_status = 'publish';

重点看这几列:

列 看什么 危险信号
type 访问方式 ALL = 全表扫描,最差。目标是 ref 或 range
rows 预估扫描行数 远大于返回行数 = 扫了很多没用的
key 实际用到的索引 NULL = 没走索引
Extra 附加信息 出现 Using filesort 或 Using temporary 要注意

全表扫描(type=ALL)是最常见的问题:表里 10 万行,它一行行翻过去找你要的 20 行。加个索引就能从几秒变几毫秒。

四、WordPress 里最常见的三类慢查询

1. 查 wp_postmeta(头号问题)

WordPress 把大部分自定义数据都存在 wp_postmeta 表里,这个表又大又容易出问题。

典型慢查询:

SELECT * FROM wp_postmeta WHERE meta_key = '_price' AND meta_value > 100

meta_value 字段默认没有索引。一旦你的文章/产品超过几千条,这类查询就会很慢。

解法:

  • 如果业务上必须按 meta_value 查,给它加索引:
    ALTER TABLE wp_postmeta ADD INDEX meta_key_value (meta_key, meta_value(50));

    (注意 meta_value 是 TEXT 类型,必须指定长度)

  • 但更推荐的做法是:减少这类查询。很多插件在列表页对每篇文章都查一遍 meta(N+1 查询问题),换成一次性批量查,性能提升几十倍

2. 查 wp_options 的 autoload 数据

WordPress 每次加载都会把所有 autoload='yes' 的 options 一次性读出来。这个数据会越来越大——尤其是装了又删的插件会留下垃圾数据。

检查它有多大:

SELECT SUM(LENGTH(option_value)) / 1024 AS size_kb
FROM wp_options WHERE autoload = 'yes';
  • < 800KB:正常
  • > 2MB:明显拖慢每一个页面,必须清理

找出最大的几项:

SELECT option_name, LENGTH(option_value) AS size
FROM wp_options WHERE autoload = 'yes'
ORDER BY size DESC LIMIT 20;

看到已经卸载的插件留下的 option,删掉(删之前确认插件确实已卸载)。

3. 插件的统计/日志表无限增长

很多统计插件、表单插件、日志插件会往自定义表里塞数据,而且不清理。

看看哪些表最大:

SELECT table_name,
       ROUND((data_length + index_length) / 1024 / 1024, 2) AS size_mb,
       table_rows
FROM information_schema.tables
WHERE table_schema = '你的数据库名'
ORDER BY size_mb DESC LIMIT 15;

如果发现某个插件的日志表几百 MB 甚至几 GB,而你可能根本用不到那些历史数据——直接在插件设置里清理,或者定期清空。

五、优化的几个层次(从易到难)

层次 1:清理(零风险,立刻见效)

  • 清理文章修订版本(revisions):一个几百篇文章的站,修订版可能有几千条
  • 清理自动草稿、垃圾评论、回收站内容
  • 清理过期的 transient 数据

用 WP-Optimize 或 Advanced Database Cleaner 插件,点几下就完成。操作前先备份数据库。

也可以手动:

DELETE FROM wp_posts WHERE post_type = 'revision';
DELETE FROM wp_postmeta WHERE post_id NOT IN (SELECT ID FROM wp_posts);
DELETE FROM wp_options WHERE option_name LIKE '_transient_timeout%' AND option_value < UNIX_TIMESTAMP();

层次 2:加索引(中等风险,效果显著)

针对 EXPLAIN 发现的全表扫描加索引。操作前一定先备份数据库。

常用索引:

ALTER TABLE wp_postmeta ADD INDEX idx_post_meta (post_id, meta_key);
ALTER TABLE wp_posts ADD INDEX idx_type_status_date (post_type, post_status, post_date);

层次 3:加缓存(最有效)

前面做的都是「让查询更快」,缓存是「根本不查」。

  • 对象缓存(Redis / Memcached):把数据库查询结果缓存起来,同一个查询第二次直接读内存。对动态页面效果最好
  • 页面缓存:整个页面生成静态 HTML,完全不碰数据库

宝塔里装 Redis 扩展 + WordPress 装 Redis Object Cache 插件,十分钟的活,数据库查询量通常能降 70% 以上。

层次 4:升级硬件或迁移数据库

如果数据量真的很大(比如 WooCommerce 几万订单),前面都做了还是慢,考虑:

  • 把数据库独立到单独一台机器
  • 升级到 SSD(机械硬盘跑数据库是灾难)
  • 增加 innodb_buffer_pool_size(一般是物理内存的 60-70%)

六、一个关键配置:innodb_buffer_pool_size

这是 InnoDB 引擎的缓存池大小,对性能影响最大的单个参数。默认可能只有 128M,对独立站太小了。

查看当前值:

SHOW VARIABLES LIKE 'innodb_buffer_pool_size';

建议设置:

  • 2G 内存服务器:512M – 1G
  • 4G 内存服务器:2G
  • 8G 以上:物理内存的 60-70%

在 my.cnf 里改,重启 MySQL。

最后

数据库优化有个特点:往往 1-2 条 SQL 就吃掉 80% 的时间。所以不要试图优化所有查询——找到最慢的那几条,改掉,效果立竿见影。

而且对大多数独立站来说,加 Redis 对象缓存 + 清理 wp_options 垃圾这两件事做完,数据库基本就不会是瓶颈了。不需要成为 DBA。