コンテンツへスキップ

リビジョン・自動下書きの仕組み — DBに溜まる履歴はどう管理すべきか

WordPressで記事を編集して「更新」を押すと、画面の右側に「リビジョン」という項目が現れ、過去の版と見比べたり、以前の状態に戻したりできるようになる。書き損じや誤った上書きから記事を守ってくれる、ありがたい仕組みだ。

一方で、保守の現場では「リビジョンが溜まってDBが重くなる」「上限を設定したほうがよい」という話もよく耳にする。リビジョンは実際にどこに、どのような形で保存されているのか。似た名前の「自動保存(autosave)」や「自動下書き(auto-draft)」とは何が違うのか。

前回の「autoloadオプションとは」の末尾で予告した通り、今回はこのブログ自身が動いているWordPress(7.1系)のデータベースを読み取り専用のクエリで調べた実測値と、コアのソースコードを材料に、wp_posts テーブルに少しずつ積み上がっていく履歴の正体と、その管理の考え方を整理する。

リビジョンは「記事と同じテーブル」に入っている

最初に押さえておきたいのは、リビジョンが専用のテーブルを持っていないという点である。リビジョンは、記事や固定ページと同じ wp_posts テーブルに、post_type が revision の行として保存されている。

このブログの wp_posts を post_type と post_status の組み合わせで集計すると、次のようになった(wp db query で SELECT のみを実行・DBへの変更はしていない)。

SELECT post_type, post_status, COUNT(*) AS cnt,
       SUM(LENGTH(post_content)) AS bytes
FROM wp_posts GROUP BY post_type, post_status;
post_type post_status 行数 本文の合計バイト数
post publish 126 1,144,680
revision inherit 42 396,549
page draft 1 6,788
wp_navigation publish 1 22

公開済みの記事126本に対して、リビジョンが42行ある。リビジョンの post_status は常に inherit(親の状態を引き継ぐ)で、post_parent 列に親記事のIDが入る。post_name は 7-revision-v1 のように「親のID-revision-v1」という形式になっていた。

ここで注目したいのは本文のバイト数だ。リビジョン42行で約39万バイト、記事126本で約114万バイト。リビジョンは差分ではなく、その時点の本文を丸ごと保存している。親記事ごとに集計すると、4回分のリビジョンを持つ記事(ID 7)だけで約6万4千バイトあった。長い記事を何度も更新すれば、その回数分だけ全文のコピーが積み上がることになる。

リビジョンが作られるタイミング

では、リビジョンはいつ作られるのか。コアの wp-includes/default-filters.php を見ると、リビジョン作成の関数は記事の保存に関係するフック(フックの仕組みは「WordPressのフック機構の基礎」で解説した)に登録されている。

add_action( 'wp_after_insert_post', 'wp_save_post_revision_on_insert', 9, 3 );
add_action( 'post_updated', 'wp_save_post_revision', 10, 1 );

そして wp_save_post_revision_on_insert() の冒頭には、次の分岐がある。

function wp_save_post_revision_on_insert( $post_id, $post, $update ) {
    if ( ! $update ) {
        return;
    }
    // ...
    wp_save_post_revision( $post_id );
}

つまりリビジョンは、既存の記事が「更新」されたときに作られる。本体の wp_save_post_revision() はさらに、次のような場合には何もせずに戻る。

  • 自動保存の処理中(DOING_AUTOSAVE)である
  • その投稿タイプがリビジョンに対応していない
  • 投稿の状態が auto-draft(後述)である
  • リビジョンが無効化されている
  • 直前のリビジョンと比べて、タイトル・本文・抜粋のいずれも変わっていない

最後の条件は見落とされがちだが重要だ。「更新」ボタンを押しても、中身が変わっていなければリビジョンは増えない。変わったかどうかの判定には空白の正規化(normalize_whitespace())がかかっているため、行末の空白の違い程度では新しいリビジョンにならない。

このブログの実測 — 「更新したことのある記事」だけにリビジョンがある

このブログの記事は、管理画面のエディタではなく WP-CLI の wp post create で投稿している。新規作成は「更新」ではないので、投稿した時点ではリビジョンは作られない。リビジョンが作られるのは、後から内部リンクを追記するために wp post update を実行したときだけである。

実際に数えてみると、この関係がそのまま数字に表れていた。

集計 件数
記事(post)の総数 126
post_modified が post_date と異なる(公開後に更新された)記事 31
リビジョンを1件以上持つ記事(post_parent の種類数) 31
リビジョンの総数 42

公開後に更新した31本の記事にだけリビジョンがあり、一度も更新していない95本にはリビジョンが1件もない。

リビジョンは「保存した後の状態」の記録である

この実測で、もう1つ大事なことが分かった。各記事について、最新のリビジョンと最古のリビジョンの本文が、現在の記事本文と一致するかを比べてみた。

記事ID リビジョン数 最新リビジョン = 現在の本文 最古リビジョン = 現在の本文
7 4 一致 不一致
113 3 一致 不一致
39 ほか(該当6本) 2 一致 不一致
13 ほか(該当23本) 1 一致 一致

最新のリビジョンは、どの記事でも現在の本文と一致していた。コアのコメントにも「最新のリビジョンは常に現在の投稿と一致する」と書かれている通り、リビジョンは「保存する前の版」ではなく「保存した後の版」のスナップショットなのである。

ここから、少し意外な事実が導かれる。リビジョンを1件だけ持つ記事では、そのリビジョンは現在の本文と同じ内容だ。では、更新する前の最初の版はどこにあるのか。答えは「どこにもない」である。

管理画面のエディタで新規作成した記事の場合、編集画面を開いた時点で下書き用の行が作られ、「公開」ボタンを押す操作が既存の行の更新として扱われるため、公開時の版がリビジョンとして残る。しかし wp post create のように最初から公開状態で作成した記事は、その時点の版がリビジョンに残らない。その後に一度更新すると、残るのは更新後の版だけで、最初の版は失われる。

これは不具合ではなく設計どおりの挙動だが、「リビジョンがあるから、いつでも元に戻せる」とは限らないことを示している。WP-CLIや外部ツール・インポート処理など、エディタを経由しない経路で記事を作成・更新しているサイトでは特に意識しておきたい点だ。リビジョンはバックアップの代わりにはならない(バックアップの考え方は「3-2-1バックアップルールとは何か」を参照)。

自動保存(autosave)はリビジョンと何が違うか

エディタで文章を書いている最中、WordPressは定期的に内容を自動保存する。この間隔は定数 AUTOSAVE_INTERVAL で決まり、既定値は60秒である(このブログの環境でも60だった)。ブロックエディタの設定にもこの値がそのまま渡されている。

自動保存の保存先は、記事の状態によって変わる。

記事の状態 自動保存の保存先
下書き(本人が編集中) 記事の行そのものを上書き
公開済みなど 別の行に「自動保存用リビジョン」として保存

公開済みの記事を編集している最中に、その都度記事本体を書き換えてしまうと、「更新」を押す前の書きかけの文章が公開ページに出てしまう。そのため公開済みの記事では、自動保存は post_name が「親のID-autosave-v1」となる専用のリビジョン行に保存される。この行はユーザーごとに1件で、自動保存のたびに上書きされるので、通常のリビジョンのように数が増え続けることはない。

また、前述の通り wp_save_post_revision() は自動保存の処理中には何もしないので、60秒ごとの自動保存で通常のリビジョンが増えることもない。

このブログはエディタを使わずに投稿しているため、自動保存用のリビジョンは0件だった(42件すべてが -revision-v1 形式)。

自動下書き(auto-draft)は「編集画面を開いただけ」で作られる

もう1つ、名前の似た auto-draft という状態がある。これは管理画面で「新規投稿を追加」を開いた時点で、まだ何も書いていなくても作られる仮の行だ。記事IDを先に確保しておくことで、画像のアップロードなどを保存前から紐づけられるようにするための仕組みである。

「新規投稿を追加」を開いて何も書かずに閉じた場合、この仮の行はそのまま残る。これを掃除するのが、WP-Cron(WP-Cronの仕組みは「crontab構文の基礎とWP-Cronとの違い」で解説した)に1日1回登録されている wp_scheduled_auto_draft_delete というイベントで、実体の関数 wp_delete_auto_drafts() は次のようになっている(要点のみ)。

// Cleanup old auto-drafts more than 7 days old.
$old_posts = $wpdb->get_col(
    "SELECT ID FROM $wpdb->posts
     WHERE post_status = 'auto-draft'
     AND DATE_SUB( NOW(), INTERVAL 7 DAY ) > post_date"
);
foreach ( (array) $old_posts as $delete ) {
    wp_delete_post( $delete, true );
}

作成から7日を過ぎた auto-draft は、WP-Cronによって自動的に削除される。auto-draft の状態ではリビジョンも作られないので、通常の運用であれば溜まり続けることはない。逆に言えば、WP-Cronが動いていないサイトでは、この掃除も行われない。auto-draft が大量に残っている場合は、WP-Cronの実行状況を確認する手がかりになる。

同じくWP-Cronの日次イベント wp_scheduled_delete は、ゴミ箱に入れてから EMPTY_TRASH_DAYS(既定値30日)を過ぎた投稿を完全に削除する。このブログの環境でも30日で、ゴミ箱の投稿は0件だった。

種類 何か 増え方 自動の後始末
リビジョン 更新ごとの本文のスナップショット 更新のたびに1件(中身が変わった場合のみ) 上限を設定した場合のみ、古い順に削除
自動保存 編集中の書きかけの内容 ユーザーごとに1件を上書き なし(増え続けない)
自動下書き 新規作成画面を開いたときの仮の行 画面を開くたびに1件 7日経過でWP-Cronが削除
ゴミ箱 削除操作をした投稿 削除のたびに1件 30日経過でWP-Cronが削除

この表から分かる通り、4つのうち既定の設定で上限なく増え続けるのはリビジョンだけである。

リビジョンの上限を決める WP_POST_REVISIONS

リビジョンの保存件数は、wp-config.php の定数 WP_POST_REVISIONS で制御する(wp-config.php の主要な定数は「wp-config.phpの主要定数解説」でも扱った)。コアの wp_revisions_to_keep() を読むと、値は次のように解釈される。

$num = WP_POST_REVISIONS;
if ( true === $num ) {
    $num = -1;   // 無制限
} else {
    $num = (int) $num;
}
設定値 意味
true(既定値) 無制限に保存する
正の整数(例: 5) 記事ごとに最新の5件まで保存する
false または 0 リビジョンを作らない

このブログは定数を設定していないため既定値の true(無制限)で、wp_revisions_to_keep() は -1 を返していた。さらに、この値は wp_revisions_to_keep フィルターや、投稿タイプごとの wp_{post_type}_revisions_to_keep フィルターで上書きできる。プラグインがこのフィルターを使っている場合、wp-config.php の設定がそのまま効いていないように見えることがある。

上限を設定しても、既存のリビジョンはすぐには減らない

見落とされやすいのが、上限による削除がいつ行われるかである。wp_save_post_revision() の後半を読むと、古いリビジョンの削除は「新しいリビジョンを保存した直後」に、その記事についてだけ行われる。

$return = _wp_put_post_revision( $post );

$revisions_to_keep = wp_revisions_to_keep( $post );
if ( $revisions_to_keep < 0 ) {
    return $return;
}
// 古い順に並べ、上限を超えた分を削除

つまり、WP_POST_REVISIONS を 5 に設定しても、すでに20件のリビジョンを持つ記事は、次にその記事を更新するまで20件のままである。二度と更新しない古い記事のリビジョンは、上限を設定しただけでは残り続ける。上限の設定は「これから増える分を抑える」ためのもので、「今ある分を整理する」ものではないと理解しておく必要がある。

無効化(false)を選ぶ前に考えたいこと

DBの容量だけを考えれば false にしたくなるが、リビジョンは誤って本文を消したり上書きしたりしたときに、記事単位で元に戻せる数少ない手段である。バックアップからの復元はサイト全体(またはDB全体)が単位になるため、「この記事の、昨日の午後の版に戻したい」という用途には向かない。

保守の観点からは、無効化よりも「件数に上限を設ける」ほうが扱いやすい。何件が適切かは、そのサイトの更新頻度や記事の長さ、複数人で編集するかどうかで変わるが、記事ごとに数件〜10件程度に抑えれば、直近の編集をさかのぼる用途は十分に満たせることが多い。

溜まったリビジョンの確認と整理

リビジョンの量は、DBを変更しない読み取り操作だけで確認できる。

# リビジョンの件数
wp post list --post_type=revision --format=count

# 設定上の上限(true / 数値 / 未定義)
wp eval 'var_dump( defined( "WP_POST_REVISIONS" ) ? WP_POST_REVISIONS : "undefined" );'

記事ごとの件数や本文の合計バイト数を知りたい場合は、本記事で使ったような SELECT 文を wp db query で実行すればよい。見るべきポイントは次の3つだ。

  1. リビジョンの総数と本文の合計サイズ: 記事本体と比べてどの程度の量か
  2. 件数の多い記事: 特定の記事に数十件〜数百件が集中していないか
  3. 上限の設定: 無制限のままか、フィルターで上書きされていないか

整理するときの原則

整理が必要と判断した場合も、進め方は前回のautoloadと同じく「消す前に、読む・確かめる・残す」が基本になる。

  1. 先にバックアップを取る: 削除したリビジョンは元に戻せない
  2. 先に上限を設定する: 整理しても上限が無制限のままなら、また同じように積み上がる
  3. 削除はWordPressの関数を通す: リビジョンの削除には wp_delete_post_revision() や wp post delete のようなWordPressの関数・コマンドを使う
  4. 変更後に確認する: 主要な記事の表示と、管理画面のリビジョン画面が正常に開くかを確かめる

3番目の「WordPressの関数を通す」が大事な理由は、wp_posts の行を SQL の DELETE 文で直接消すと、その行に紐づくメタ情報(wp_postmeta)などが後始末されずに残るためである。WordPressの関数を通せば、関連するデータの削除や、削除時に実行されるべきフックの処理もあわせて行われる。このブログではリビジョンに紐づく wp_postmeta の行は0件だったが、プラグインによってはリビジョンにもメタ情報を保存するものがあり、そうしたサイトでは直接の DELETE が孤立したデータを生む。

なお、リビジョンを削除しても、データベースのファイルサイズはすぐには小さくならないことが多い。削除した行の領域は再利用のために確保されたまま残るためで、この領域の整理が wp db optimize の役割である(「wp db check / wp db optimize — 見落とされがちなDB健全性コマンド」で解説した)。

リビジョンはサイトの表示を重くするのか

「リビジョンが溜まるとサイトが重くなる」という話も、少し分けて考える必要がある。

記事の表示やアーカイブの一覧は、post_type = 'post' かつ post_status = 'publish' のような条件で wp_posts を検索する。このとき、wp_posts には type_status_date(post_type・post_status・post_date などの複合インデックス)があるため、リビジョンの行は条件の段階で絞り込まれ、読み込まれない(インデックスの仕組みは「データベースインデックスの基礎」を参照)。前回のautoloadのように「使わなくても毎回読み込まれる」性質のデータではない。

影響が出やすいのは、次のような場面である。

  • バックアップや移行の容量と所要時間: DBのダンプやサイトの移行では、リビジョンもすべて対象になる
  • エディタのリビジョン画面: 1記事に数百件のリビジョンがあると、比較画面の読み込みに時間がかかる
  • wp_posts 全体を走査する処理: 検索系のプラグインや一括置換の処理など、インデックスを使わずに全行を読む処理
  • 共用サーバーのDB容量の上限: 契約プランによっては、DBの容量に上限がある

つまりリビジョンの問題は、日々の表示の重さよりも、保守作業(バックアップ・移行・一括処理)の負担として表れやすい。このブログの規模(リビジョン42件・本文約39万バイト)なら何も問題にはならないが、長文の記事を毎日のように更新するサイトや、多人数で編集するサイトでは、何年も経つうちに記事本体の何倍ものリビジョンが溜まることもある。

よくある落とし穴

  • リビジョンがあれば最初の版に戻せると思い込む: リビジョンは保存後の版の記録で、エディタを経由せずに作成した記事では最初の版が残らない。バックアップの代わりにはならない
  • 上限を設定すれば既存のリビジョンも減ると考える: 削除はその記事を次に更新したときにしか行われない
  • SQLで直接 DELETE する: 関連するメタ情報などが残り、孤立したデータが生まれる
  • auto-draft の山を手作業で消して終わりにする: WP-Cronが止まっていることが原因なら、また溜まる。WP-Cronの実行状況を先に確かめる
  • 自動保存が60秒ごとにリビジョンを増やしていると考える: 自動保存はユーザーごとに1件を上書きし、通常のリビジョンは増やさない
  • リビジョンを無効化して容量を減らす: 記事単位で元に戻す手段がなくなる。無効化より上限の設定を検討する

まとめ

リビジョンは、記事と同じ wp_posts テーブルに post_type = 'revision' の行として、差分ではなく全文のコピーで保存される。作られるのは既存の記事が更新され、中身が変わったときだけで、その内容は「保存した後の版」である。このブログの実測では、公開後に更新した31本の記事にだけリビジョンがあり、WP-CLIで作成した記事では最初の版がリビジョンに残っていなかった。

自動保存はユーザーごとに1件を上書きし、自動下書きとゴミ箱はWP-Cronが期限付きで掃除する。既定の設定で上限なく増え続けるのはリビジョンだけで、その上限を決めるのが WP_POST_REVISIONS だ。ただし上限は次の更新時にしか適用されないため、既存の分を整理するときは、バックアップ → 上限の設定 → WordPressの関数を通した削除 → 動作確認、の順で進めるのが安全である。