Vacuum Is a Lie: About Your Indexes

2025/12/14 22:13

Vacuum Is a Lie: About Your Indexes

RSS: https://news.ycombinator.com/rss

要約

Japanese Translation:

PostgreSQL の

VACUUM
はテーブルから死んだタプルを削除しますが、空の B‑tree インデックスページはそのままにしておくため、大規模な削除後にはテーブルと主キーインデックスが膨張した状態のまま残ります。約 80 % の行を削除するデモでは、ヒープは約 7 472 kB のままで、主キーインデックスは 2 208 kB に留まりました。
pg_stat_index
は平均葉密度 (avg_leaf_density) を 86 % と報告しましたが、bloat_ratio が 2.8 であるため、インデックスは期待値のほぼ三倍に膨らんでいることを示しています。
VACUUM
はページを再利用可能としてマークするだけで OS に返却しないため、物理ページ数は増え続け、プランナーがそのカウントに基づく推定値でコスト計算を誤り、不要なシーケンシャルスキャンを引き起こすことがあります。

テーブルをオフラインにせずにスペースを回収するには、管理者は

REINDEX CONCURRENTLY
でインデックスを再構築できます。これにより demo_pkey のサイズが 2 208 kB から 456 kB(約 79 % 削減)になりました。オンラインツールの
pg_squeeze
は論理デコードを使ってテーブルとインデックスの両方を縮小します。完全な
VACUUM FULL
も同様の結果が得られますが、ACCESS EXCLUSIVE ロックとダウンタイムが必要です。

B‑tree インデックスのデフォルト fillfactor が 90 % であっても削除による膨張は防げず、

ANALYZE
は統計情報のみを更新し(
relpages
を変更しない)、bloat_ratio が約 1.8–2.0 を超えると調査のトリガーとして有効です。閾値は環境ごとにチューニングできます。この結果、クエリ計画が高速化され、ストレージコストが削減されつつデータベースをオンラインで維持できます。

本文

よくある誤解

「VACUUM(または VACUUM FULL)を実行すれば PostgreSQL が健全に保たれる」という広まった考えがあります。
死んだタプルが除去され、トランザクション ID が再利用され、スペースが回収されると信じられていますが、実際には VACUUM はインデックスの膨張を解消しません
それはヒープ(テーブル)のデータのみを掃除しており、B‑tree 構造自体はそのままです。


ストレージ構成

レイヤー何を保存するか動作
Heap(テーブル)順序付けられない行を 8 KB ページに格納行は空き領域があればどこでもスロットされる。削除された行は隙間となり、後の挿入で埋められる。
Indexヒープタプルへのポインタを並べた B‑tree ページ並び順を維持しなければならないため、ページ間で自由にエントリを移動できない。

VACUUM が異なる理由

  • ヒープでは、VACUUM はページ内の行を詰め直し、空きページを回収できる。
  • インデックスでは 「死んだインデックスエントリだけを削除」 する。
    • 空きページを完全に削除して再利用可能にすることはない。
    • 木構造の深さを減らしたり、物理的な B‑tree 配置を変えたりしない。

ヒープはテトリスみたいだが、インデックスは並べ替え済みの本棚のようです。VACUUM は本を取り出すだけで、残ったものをまとめ直せません。


実験

CREATE EXTENSION IF NOT EXISTS pgstattuple;
CREATE TABLE demo (id integer PRIMARY KEY, data text);

-- 100 000 行
INSERT INTO demo(id, data)
SELECT g, 'Row number '||g||' with some extra data'
FROM generate_series(1,100000) g;

ANALYZE demo;

ベースライン

SELECT relname,
       pg_size_pretty(pg_relation_size(oid)) AS file_size,
       pg_size_pretty((pgstattuple(oid)).tuple_len) AS actual_data
FROM pg_class
WHERE relname IN ('demo', 'demo_pkey');
relnamefile_sizeactual_data
demo7 472 kB6 434 kB
demo_pkey2 208 kB1 563 kB

大半の行を削除

DELETE FROM demo WHERE id BETWEEN 10001 AND 90000;
VACUUM demo;

VACUUM 後のスナップショット:

relnamefile_sizeactual_data
demo7 472 kB1 278 kB
demo_pkey2 208 kB1 563 kB
  • テーブルは縮小した。
  • インデックスのサイズは変わらない。
  • VACUUM は OS にスペースを返さず、ページを再利用可能にしかマークしない。

ページ状態

状態密度備考
Full (>80 %)使い切りに近い-
Partial (40–80 %)少し余白があるエッジや軽い更新時は普通。
Sparse (<40 %)主に空き8 KB ページを数行だけ読む。
Empty (0 %)ライブ行なしだがツリー内に残るオーバーヘッド。
VACUUM
で回収できる。

フィルファクター

  • 挿入時にページをどれくらい埋め込むかを決定。
  • デフォルトは B‑tree の fillfactor が 90 %。
  • 低く設定(例:
    WITH (fillfactor = 70)
    )すると将来の挿入余地が増えるが、ほとんど行を削除した場合には膨張が 悪化 します。

プランナーの誤推定

EXPLAIN ANALYZE SELECT * FROM demo WHERE id BETWEEN 10001 AND 90000;

プランナーは 200 行と想定したが、実際は 0 行。
インデックスに空きページが多いためです。

ANALYZE
は行数を更新するだけで
relpages
を縮小しないので、プランナーはまだ 276 ページとみなす。

インデックス統計

SELECT * FROM pgstatindex('demo_pkey');
フィールド
tree_level1
leaf_pages57
empty_pages0
deleted_pages217
avg_leaf_density86.37

:86 % の密度は健全に見えるが、インデックスファイルの 79 % が不要である。

膨張率

SELECT c.relname AS index_name,
       pg_size_pretty(pg_relation_size(c.oid)) AS actual_size,
       pg_size_pretty((c.reltuples * 40)::bigint) AS expected_size,
       round(pg_relation_size(c.oid)/NULLIF(c.reltuples*40,0),1) AS bloat_ratio
FROM pg_class c
JOIN pg_index i ON c.oid = i.indexrelid
WHERE c.relkind = 'i'
  AND c.reltuples > 0
  AND c.relname NOT LIKE 'pg_%'
  AND pg_relation_size(c.oid) > 1_048_576;
index_nameactual_sizeexpected_sizebloat_ratio
demo_pkey2 208 kB781 kB2.8

約 2.0 を超える比率は調査のサインです。


問題解決策

REINDEX

REINDEX INDEX CONCURRENTLY demo_pkey;

再構築後:

relnamefile_sizeactual_data
demo7 472 kB1 278 kB
demo_pkey456 kB313 kB

インデックスは 2.2 MB → 456 KB(約 79 % 削減)。

Tip:

REINDEX CONCURRENTLY
は排他ロックを避けられ、PostgreSQL 12 以降で利用可能です。

pg_squeeze

  • テーブルとインデックスをオンラインで再構築し、ロックを最小限に抑える。
  • ロジカルディコードを使用するため、稼働中のシステムへの影響が少ない。
CREATE EXTENSION pg_squeeze;
SELECT squeeze.squeeze_table('public', 'demo');

注意

pgstatindex()
はインデックス全体を読み込むので、大きなインデックスでは避けるべきです。


いつ対処すべきか

状況対策
大量削除(保持ポリシー、GDPR 削除)再構築または REINDEX
bloat_ratio
 > 2.0 が上昇中
監視し、メンテナンスを計画
インデックス列で順序付きスキャンが増える膨張を調査
行数に対してインデックスサイズが不釣り合いREINDEX を検討

結論

  • VACUUM はヒープの膨張は解消する が、インデックス構造はそのまま。
  • インデックスの膨張には明示的な操作(REINDEX、pg_squeeze、あるいは VACUUM FULL)が必要です。

差異を理解し、適切な指標を監視して、VACUUM の静寂がすべて正常であると勘違いしないようにしましょう。

同じ日のほかのニュース

一覧に戻る →

2025/12/15 6:53

Anthropic Outage for Opus 4.5 and Sonnet 4/4.5 across all services

## Japanese Translation: アノマリーは複数のAnthropicサービスに影響を与え、特にSonnet 4.0、Sonnet 4.5、およびOpus 4.5モデルが対象でした。事件は2025年12月14日21:31(UTC)に初めて報告され、同日の21:46(UTC)に調査更新が行われました。この更新で問題は該当するモデルバージョンに起因することが確認されました。影響範囲は複数のAnthropicプラットフォームに及びます——claude.ai、platform.claude.com(旧console.anthropic.com)、Claude API(api.anthropic.com)およびClaude Codeです。タイムラインや次のステップについてはまだ発表されていません。

2025/12/15 6:05

2002: Last.fm and Audioscrobbler Herald the Social Web

## Japanese Translation: *変更は不要です。要約はすべての列挙された重要ポイントを正確に反映していますが、商業的実現可能性について小さな推測上の結論も追加されています。

2025/12/15 1:55

Hashcards: A plain-text spaced repetition system

## Japanese Translation: Hashcards は、すべてのフラッシュカードデータをプレーンマークダウンファイル(例:`Cards/Math.md` や `Chemistry.md`)として保持する軽量でローカル優先のスペースドリピテーションアプリです。ウェブ UI を起動すると (`hashcards drill <path>` が `localhost:8000` を開く)、レビュー履歴は SQLite データベースに保存されますが、カード自体は Git に対応し続けます。各カードはテキストのハッシュでコンテンツアドレス化されているためです。インターフェイスは作成をスムーズにすることを優先しており、クローズ削除は Mochi の冗長な `{{ }}` ではなく `[ ]` を使い、単一行ブロック(例:`Q: … A:` や `C: …`)でカードを定義します。 著者は Anki の煩雑な UI、“すべて期限切れを学習” ボタンの欠如、WYSIWYG エディティング、そして不安定なプラグインサポートを批判しています。Mochi は過度に冗長なクローズ構文、ノートタイプ自動化がないこと、および長期的には性能が劣る単純な倍率ベースのスケジューラーという欠点があります。Hashcards はこれらの痛みを解消し、最小限の摩擦、最適なスケジューリングのための高度な FSRS アルゴリズム、および任意のエディタや Unix ユーティリティ(例:`wc`、`awk`)でカードを編集できる機能を提供します。デッキは Git でバージョン管理・ブランチ化・マージが可能で、サードパーティサービスなしにユーザーがデータを完全にコントロールできます。 将来的には CSV ベースのカードインポート、Python で書かれたノートタイプロジック、および Git 主導の共有ワークフロー拡張をロードマップに含めています。これらの機能は協力を容易にし、コンテンツ生成を自動化するとともに、開発者が Hashcards を自身のプロジェクトへ拡張できるようにし、オープンソーススペースドリピテーション エコシステムを形成する可能性があります。