前提と環境
運用中のWordPressサイトで、溜まりに溜まったリビジョン(投稿の編集履歴)を整理したときの記録です。結論を先に書くと、リビジョンの件数だけを見て容量と所要時間を見積もったのが間違いでした。見積もりの基準は「件数 × 1件あたりの postmeta 行数」です。
検証・作業環境は以下のとおりです。
| 項目 | バージョン / スペック |
|---|---|
| WordPress | 6.4.x |
| PHP | 8.1(FPM) |
| MySQL | 8.0 / InnoDB |
| WP-CLI | 2.9.0 |
| サーバ | 2vCPU / メモリ4GB のVPS |
| サイト規模 | 投稿 約1,200件、対象リビジョン 1,540件 |
| プラグイン | リビジョン制限系は未導入(WP_POST_REVISIONS は既定の true) |
つまり「リビジョン無制限」で6年近く運用してきたサイトです。
何が起きたか
消す前に退避しようと、対象のリビジョン1,540件を1リクエストでまとめて書き出したところ、出力43MB・3分超でタイムアウト。処理が途中で落ち、その日の作業はそこで止まりました。
# 最初にやらかしたやり方(1リクエストで全件)
wp eval-file export-revisions.php > revisions.json
# → 43MB / 3分超 / PHP max_execution_time でタイムアウト
落ちた原因を追うと、リビジョン1件あたりの wp_postmeta(投稿に紐づく付帯情報の行)が平均1,640行。合計で250万行を超えていました。事前の見積もりは「1件300行くらい」だったので、5倍以上です。
ここで重要なのは、wp_posts 上のリビジョン件数は1,540件でも、実際のデータ量の重心は wp_postmeta 側にあるという点です。リビジョンは post_parent で本体投稿に紐づき、本体が使っているカスタムフィールドをリビジョン側にもコピーして持っています。ビルダー系プラグインやACFを入れているサイトでは、この行数がそのまま膨らみます。
見積もりが外れた理由:postmeta 1件あたり1,640行
作業前に叩いておくべきだったSQLはこれだけです。件数ではなく「メタ行数の分布」を見ます。
-- リビジョン件数
SELECT COUNT(*) AS revisions
FROM wp_posts
WHERE post_type = 'revision';
-- 1リビジョンあたりのメタ行数:重い順に上位10件
SELECT p.ID, p.post_parent, COUNT(pm.meta_id) AS meta_rows
FROM wp_posts p
LEFT JOIN wp_postmeta pm ON pm.post_id = p.ID
WHERE p.post_type = 'revision'
GROUP BY p.ID
ORDER BY meta_rows DESC
LIMIT 10;
平均と分布も取ります。平均だけだと一部の「爆弾リビジョン」を見落とすので、バケット分けが有効でした。
-- 平均メタ行数
SELECT AVG(c) AS avg_meta_rows
FROM (
SELECT COUNT(*) AS c
FROM wp_postmeta pm
JOIN wp_posts p ON p.ID = pm.post_id
WHERE p.post_type = 'revision'
GROUP BY pm.post_id
) t;
-- 分布(500行刻み)
SELECT FLOOR(c / 500) * 500 AS bucket, COUNT(*) AS revisions
FROM (
SELECT COUNT(*) AS c
FROM wp_postmeta pm
JOIN wp_posts p ON p.ID = pm.post_id
WHERE p.post_type = 'revision'
GROUP BY pm.post_id
) t
GROUP BY bucket
ORDER BY bucket;
実測はこうなりました。
| 項目 | 事前見積もり | 実測 |
|---|---|---|
| リビジョン件数 | 1,540件 | 1,540件 |
| 1件あたりメタ行数 | 約300行 | 1,640行 |
| 総メタ行数 | 約46万行 | 250万行超 |
| 退避ファイルサイズ | 数MB | 43MB |
| 退避所要時間 | 数十秒 | 3分超(タイムアウト) |
見積もりが外れるときは、たいていこの掛け算を飛ばしたときです。
対処1:退避フォーマットを「本体の全列+メタは件数とMD5ハッシュのみ」に変更
メタの中身まで退避JSONに詰め込むのをやめました。復元が必要になったらDB全体のバックアップから戻す前提にして、退避ファイルは同一性確認ができる最小限に絞ります。
// export-revisions.php(抜粋)
global $wpdb;
$id = (int) $args[0];
// GROUP_CONCAT はデフォルト 1024 バイトで切られる点に注意
$wpdb->query("SET SESSION group_concat_max_len = 16777216");
$stats = $wpdb->get_row( $wpdb->prepare(
"SELECT COUNT(*) AS cnt,
MD5(GROUP_CONCAT(CONCAT(meta_key, ':', meta_value) ORDER BY meta_id SEPARATOR '|')) AS hash
FROM {$wpdb->postmeta}
WHERE post_id = %d",
$id
) );
$rev = get_post( $id );
$row = array(
'ID' => $rev->ID,
'post_parent' => $rev->post_parent,
'post_date' => $rev->post_date,
'post_title' => $rev->post_title,
'post_content'=> $rev->post_content,
'meta_count' => (int) $stats->cnt,
'meta_md5' => $stats->hash,
);
echo wp_json_encode( $row ) . "\n";
効果は劇的でした。
| 指標 | 変更前 | 変更後 |
|---|---|---|
| 退避サイズ | 43MB | 1.2MB |
| 所要時間 | 3分超(タイムアウト) | 38秒 |
| 復元性 | JSON内に完全なメタ | DBバックアップ+MD5で照合 |
ハッシュを持たせたことで、「このリビジョンのメタは削除前と同一だった」ことを後から検証できます。バックアップと突き合わせれば、復元の正しさも確認できます。
対処2:削除はタイムボックスで区切って実行
wp_delete_post_revision() を一気に回すと、また同じタイムアウトを踏みます。そこで時間予算8秒で区切り、外側のシェルで回しました。
// delete-revisions.php(抜粋)
$budget = 8; // 秒
$start = microtime(true);
$deleted = 0;
foreach ( $ids as $id ) {
if ( microtime(true) - $start > $budget ) {
break; // 予算超過で安全に抜ける
}
wp_delete_post_revision( (int) $id );
$deleted++;
}
echo "deleted={$deleted}\n";
# 8秒 × 繰り返しで、1,510件を約20分で完走
while :; do
out=$(wp eval-file delete-revisions.php)
echo "$out"
deleted=$(echo "$out" | sed -n 's/deleted=\([0-9]*\)/\1/p')
[ "$deleted" -eq 0 ] && break
done
結果、1,510件を20分で完走。8秒という短い予算にしたのは、SSHセッションが切れても途中状態が壊れにくく、進捗がログに残るためです。
副作用:管理画面の応答が13.0秒 → 2.6秒
整理後、編集画面を開いたときのサーバ応答が 13.0秒 → 2.6秒 になりました。リビジョン1件ごとにメタを引いていた負荷が、そのまま画面の待ち時間になっていたようです。リビジョン整理は「容量削減」の文脈で語られがちですが、実際は編集画面のUX改善として効きます。
ハマりポイント
-
GROUP_CONCATの上限:デフォルトはgroup_concat_max_len = 1024バイト。長いmeta_valueがあるとハッシュが途中で切れ、静かに壊れます。SET SESSIONで明示的に上げること。 -
post_type = 'revision'の絞り忘れ:wp_posts全体に対して集計すると、本体・添付・自動下書きまで混ざって数字が意味を失います。 -
wp_delete_post_revisionを直接叩く前に「何を残すか」を決めきる:削除中に迷うと、判断のためのクエリが増えてタイムアウトの原因になります。今回は退避を使い捨ての一時プラグインで済ませ、削除中に迷う時間をゼロにしました。 -
オブジェクトキャッシュ:削除後もキャッシュに古いメタが残ることがあります。
wp cache flushを忘れずに。 -
max_allowed_packet:大きいpost_contentを持つサイトでは、ダンプ系の処理で引っかかります。
FAQ
Q. メタを退避しないと、復元できなくないですか?
A. DB全体のバックアップを正とし、退避ファイルは「どのリビジョンを消したか」「削除前のメタと同一か(MD5)」を示す台帳として使います。完全なメタをJSONに持つ必要はありません。
Q. そもそも増やさない方法は?
A. wp-config.php に以下を追加します。
// 直近5件だけ保持する
define( 'WP_POST_REVISIONS', 5 );
Q. 削除中にタイムアウトしたら?
A. 予算方式なら途中で抜けた分だけが残ります。次回実行時に残件を再取得すれば、冪等に近い形で進められます。
Q. 件数と容量、どちらを先に確認しますか?
A. リビジョン整理では先に「1件あたりのメタ行数」、次に件数です。掛け算した総行数が、実際の重さに一番近い指標になります。
まとめ
- 見積もりの単位は「件数」ではなく**「1件あたりのメタ行数 × 件数」**
- 退避は本体の全列+メタは件数とMD5のみ(43MB→1.2MB、3分→38秒)
- 削除は8秒の時間予算で区切る(1,510件を20分)
- 副作用として編集画面の応答が13.0秒→2.6秒
重い処理の見積もりが外れるのは、たいてい掛け算を飛ばしたときです。まず COUNT(*) と AVG() を叩いてから着手する。それだけで作業の1日が潰れる事故はかなり防げます。
この記事を書いた人
BENTEN Web Works — 業務自動化・システム開発のフリーランスエンジニアです。
GAS / Python / RPA を使った業務自動化や、Web制作・システム開発のご相談を承っています。
「こんなこと自動化できる?」というご質問だけでもお気軽にどうぞ。
👉 BENTEN Web Works — 詳細・お問い合わせはこちら
🐦 X(旧Twitter) — 日々の知見を発信中