- PostgreSQL クエリ性能を高めるための、従来の枠にとらわれない最適化手法として、インデックス追加やクエリ書き換えではない 3つの創造的なアプローチを紹介
- チェック制約に基づくフルテーブルスキャンの除去、関数ベースインデックスによる低カーディナリティ最適化、Hash インデックスによるユニーク制約の実装
- 1.
constraint_exclusion 設定を活用し、誤った条件のクエリで不要なスキャンを防止
- 2. 関数ベースインデックスと 仮想生成列(virtual generated column) を利用して、インデックスサイズを削減しつつクエリの一貫性を確保
- 3. Hash インデックスと 排他制約(exclusion constraint) を組み合わせ、大容量テキスト列のユニーク制約を効率的に実装し、保存領域を大幅に節約
チェック制約に基づくフルテーブルスキャンの除去
plan 列で 'free', 'pro' の値のみを許可する CHECK 制約があるにもかかわらず、'Pro' という誤ったクエリを実行すると PostgreSQL はテーブル全体をスキャンする
- 実行計画では 10 万行をすべて読み込み、実際の結果は 0 行
constraint_exclusion パラメータを 'on' に設定すると、PostgreSQL が制約条件を考慮して スキャンを完全に省略する
- 実行時間は 7.4ms から 0.008ms に短縮
- デフォルト値は
'partition' であり、単純なクエリではむしろ プランニングのオーバーヘッドが大きくなる可能性がある
- ただし、BI・レポーティング環境ではユーザーが誤った条件を頻繁に入力するため、
'on' 設定が有用
関数ベースインデックスによる低カーディナリティ最適化
- 1,000 万件の販売データを持つ
sale テーブルで 日次売上集計クエリを実行すると、フルスキャンで 627ms かかる
sold_at 列に B-Tree インデックスを追加すると 187ms に短縮されるが、インデックスサイズは 214MB と大きい
date_trunc('day', sold_at) 式に 関数ベースインデックスを作成すると、サイズは 66MB に減り、実行時間も 145ms とさらに高速になる
- 低カーディナリティであるため インデックス deduplication が可能
- ただし、クエリ式がインデックス定義と正確に一致している必要があるため、式の一貫性維持が必要
- そのために同一式を含む VIEW を作成する、または
- PostgreSQL 18 からサポートされる 仮想生成列(virtual generated column) を追加して一貫性を自動化できる
- 仮想生成列を使えばインデックスが自動的に活用され、小さなインデックス・高速なクエリ・式の一貫性をすべて確保できる
- ただし、PostgreSQL 18 では 仮想列へのインデックス作成はまだ未サポートで、将来の 19 バージョンで対応予定
Hash インデックスによるユニーク制約の実装
- 長い URL を保存する
urls テーブルで 重複 URL 防止のために B-Tree ベースのユニークインデックスを作成すると、インデックスサイズは 154MB に達する
- Hash インデックスは実際の値を保存せずハッシュ値だけを保存するため、はるかに小さい
- PostgreSQL は標準では ユニーク Hash インデックスをサポートしていないが、
- 排他制約(exclusion constraint) を用いて
EXCLUDE USING HASH (url WITH =) の形で ユニーク制約を回避的に実装できる
- この方法でも重複挿入時にはエラーが発生し、クエリ性能も B-Tree より高速 (0.022ms vs 0.046ms)
- インデックスサイズは 32MB で、B-Tree と比べて 5 倍以上小さい
- 欠点:
- 外部キー参照不可 (
REFERENCES 制約は不可)
INSERT ... ON CONFLICT 構文との互換性に制限がある
ON CONFLICT ON CONSTRAINT または MERGE 構文で代替可能
- Hash インデックスは 大容量テキスト列のユニーク保証に適しており、外部キーが不要な場合には 空間効率の高い代替手段として有用
1件のコメント
Hacker Newsの意見
インデックスが214MBで、テーブル全体の半分ほどのサイズになっている
アナリストの立場では良いが、書き込み性能の観点ではwrite amplificationの問題が生じる
インデックス設計は読み取り/書き込み比率によって変わるため、データウェアハウスやリードレプリカを置く理由にもなる
非常に多くのユーザーを相手にするなら、OLTP DBにBI/OLAP向けインデックスを置かない方がよい
テーブルのアクセスパターンが一定なら、テーブル自体をインデックスにしてwrite amplificationなしで効率を得られる
最初の例では、
Planをenum型として定義する方がよいと思うテキストより軽く、誤ったフィルタ入力時に空結果ではなくエラーになるため安全性が高い
素晴らしい記事だった。PostgreSQLとMySQLを何十年も使ってきたが、この記事を読んでもなお可能性の一部しか分かっていなかったと感じた
記事の最後で触れられていた
MERGE構文が最も興味深かった普段は
INSERT ... ON CONFLICT DO UPDATEでupsertを処理しているが、MERGEはより強力で、さまざまな状況で使えそうだMERGEはかなり前からSQL標準にあったが、PostgresはMVCCモデルにおける非原子的な問題のため導入を遅らせていたpganalyzeのブログ記事でも説明されている
個人的には
INSERT ... ON CONFLICTの方を好み、どうしても必要なときだけMERGEを使い、エラー処理は慎重に行うINSERT ... ON CONFLICTの方が予測しやすいmodern-sql.comの比較記事を参照
COPY INTOをbinary形式で使うのが最速だ。サーバー側のオーバーヘッドがほとんどない記事で扱われていなかったBRINインデックスが興味深かった
データが単調増加するなら、非常に小さく高速なインデックスとして理想的だ
たとえばサーバーで受信するtimestampデータのように、少し順序が前後する場合でも有効だ
UUIDv7では
pages_per_rangeを調整する必要があるかもしれないhashインデックスで一意制約を付けられない点がいつも残念だ
単にexclusion constraintへ変換するglueコードさえあれば解決しそうなのに、なぜまだ存在しないのか気になる
ハッシュベースの一意性検証は衝突処理ができないため、インデックスではサポートされていない
提案された解決策も同じ問題を抱えている
Postgresはハッシュと実際の値の両方が一致したときにのみ重複と見なす
記事の内容は新鮮だった。仮想カラムとhashインデックスは興味深いが、まだエコシステムに完全には統合されていない印象がある
hashインデックスは長い間制約が多かったが徐々に改善されており、自動一意制約が残る課題だ
stored generated columnを使えば、そのままインデックスを作れるのではないかと思う
PostgreSQL 14からサポートされているが、結果が物理的に保存されて追加ストレージを消費するためだ
クラウドへ移行してからは、固定サーバー環境でpgsqlを直接扱うことが減った
記事に出てきたSQL構文ハイライトが組み込み機能なのか、それとも別ツールなのか気になる
ただし長いクエリをコピーすると、改行の後に自動で空白が付くのが不便だ