
2026/07/06 4:55
PostgreSQL の B-Tree インデックスを理解する:包括的ガイド~パート 1~
RSS: https://news.ycombinator.com/rss
要約▶
Japanese Translation:
PostgreSQL は、B 木、ハッシュ、GiST、SP-GiST、GIN、BRIN など多様なインデックスタイプを提供することで、高いパフォーマンスのクエリを実現します。それぞれは、異なるアクセスパターンとデータ分布に対応する専用のアルゴリズムで設計されています。検索効率の核となるのは B-木であり、これはすべてのリーフノードが同じレベルにあり、その上位に空の部分木が存在しないバランストな構造です。データはソート順(左が最小、右が最大)で保持され、標準的な B 木インデックスは挿入と檢索を効率的にサポートするために Lehman–Yao アルゴリズムを依存しています。ディスク上で B-木は有向グラフとして機能します:内部ノードは下方向に指し、リーフページは橋渡しポインタ (
btpo_prev, btpo_next) によってリンクされ、ルートへ再三登降することなく前後の遍歴を可能にします。
インデックスのリーフは完全な行データを格納せず、代わりにキーと
ctid を通じた行への参照のみ保持します。これは、格納ファイル内の物理ブロック番号とオフセットを符号化しており、インデックスを使用しない場合のようにすべてのエントリを上回る高価なシーケンシャルスキャンを行わずに特定の行を直接位置付けることを可能にします。各インデックスレコードは、対象とする範囲の上界を示す「high key」を含み、その後実際のインデックスされたデータ値(例:bigint)が続きます。
インデックスに加え、PostgreSQL はテーブルの格納を自動的に管理します:1 GB 未満のファイルは単一のオブジェクトとして残され、より大きなテーブルは複数の 1 GB セグメントに分割されます。各データページは、利用可能なスペース、トランザクションポインタ、タプルに関するメタデータを追跡するヘッダーを持っています。エンジニアは
pg_class(オブジェクト OID、関係名、スキーマ、所有者、テーブルスペース、ページ数、タプル数、タイプ、ファイルパスを報告)のようなシステムビューや、pageinspect などの拡張機能を通じて、これらの内部構造に深く洞察を得ることができます。これらの拡張機能には、トポロジーとページ統計を分析するための bt_metap, bt_page_stats, bt_page_items といった関数が含まれます。これらアーキテクチャ上の特徴は、共に高精度なパフォーマンスチューニング、高速なクエリ実行、そして高ボリュームかつ複雑なデータ環境において削減された計算量を実現します。本文
PostgreSQL バルックツリーインデックス:内部機構と実装
大規模なテーブル上のクエリ最適化、特にフィルタリング操作が低下する際の実装戦略について解説します。効果的なインデックス利用には、内部機構の理解が不可欠です。
目次
- PostgreSQL のインデックスの種類
- バルックツリー(B-Tree)の論理的スキーマ
- データ格納方式とメタデータ復習
- バルックツリーインデックスの論理的表現
- インデックスの実装詳細(PageInspect を活用して)
- 結論:構造からの学び
PostgreSQL のインデックスの種類
PostgreSQL は、異なるアルゴリズムに基づいた以下のインデックスを提供します。用途に合わせて適切に選択する必要があります。
- B-Tree: バランストツリー(デフォルト)
- Hash: 等値比較に適したハッシュテーブル
- GiST: 空間データや全般的な検索木
- SP-GiST: 二次的な階層検索木
- GIN: 逆インデックス(テキスト検索など)
- BRIN: ブロック範囲インデックス(大規模テーブル用)
バルックツリー(B-Tree)の論理的スキーマ
「B-Tree」は**バランスド・ツリー(平衡木)**を意味し、すべてのレベルでルートからの距離が一致した構造を持っています。
主な特性
- ノードの柔軟性: ノードは通常 2 つ以上の子ノードを持ち、樹の順序 $M$ によって定義される範囲内で変動します。
- 高さ管理: 検索操作の最適化のために、高さを常に $\log_M N$ に保ちます。
- ソート順序: データはソート順に格納され、左側に最小値、右側に最大値が配置されます。
- 一貫性と効率性: すべてのリーフノードが同一レベルにあり、リーフ上方には空の部分木が存在しません。
比較:インデックス有無の複素性
- インデックスなし: デフォルトでシークウェントスキャンを実施。全データエントリを反復処理するため、読み込み量は多いですが検索が遅いです($O(N)$)。
- インデックスあり: 情報取得が劇的に早くなります。
[復習] PostgreSQL のデータ格納方式
テーブルは順序付けされたデータとして期待されますが、実際にはページ内や異なる位置に分散して格納されています。
メタデータの確認
テーブル
t を作成し、以下のクエリで物理情報を取得します。
SELECT class.oid AS "OID", relname AS "Relation Name", schema.nspname AS "Schema", usr.rolname AS "Object Owner", coalesce(tblsp.spcname, 'pg_default') AS "Tablespace", relpages AS "Amount Pages", reltuples AS "Amount Tuples", reltoastrelid AS "TOAST table", CASE WHEN relpersistence = 'p' THEN 'Permanent' WHEN relpersistence = 't' THEN 'Temporary' ELSE 'Unlogged' END AS "Type", pg_relation_filepath(class.oid) AS "File Path", pg_size_pretty(pg_relation_size(class.oid)) AS "Relation Size" FROM pg_class class INNER JOIN pg_namespace schema ON schema.oid = class.relnamespace INNER JOIN pg_authid usr ON usr.oid = class.relowner LEFT JOIN pg_tablespace tblsp ON tblsp.oid = class.reltablespace WHERE relname = 't';
メタデータ解説
- OID: システムシーケンスに基づき割り当てられた一意の識別子。
- デフォルトテーブルスペース:
pg_default - スキーマ: デフォルトは
public - 所有者: アクセスを管理する作成者
- ページ・タプル: 物理的なページ数と記録(行)の数
- TOAST テーブル: 長すぎるデータを分割して格納する補助テーブル
- ファイルパス:
ディレクトリ内のバイナリファイルパスpg_home
ファイル構造の特徴
- 1GB 未満のテーブルは単一ファイルに格納されます。
- 1GB を超える場合、データは最大 1GB の複数のファイルに分割されます。
ctid
とページの構造
ctid- CTID: テーブル内の各行の一意な物理的識別子(ブロック番号とオフセット)。
- ページ構造: ヘッダー部を含み、下から上へと成長します。データ挿入時は左下から書き込まれ、ポインタ値を参照します。
- バルックツリーインデックスのリーフノードは、特定のファイル内の特定の
を指しています。ctid
バルックツリーインデックスの論理的表現
インデックスはテーブルデータファイルと隣接する独立したファイルとして存在します。作成命令は以下の通りです。
CREATE INDEX index_name ON table_name (column_name);
検索プロセス(例:値 37 の検索)
- ルートから開始: ルートノードと比較開始。
- 分岐:
- $13 < 37$ なので、右へ移動。
- ノードで $31 < 37 < 41$ と判断。
- リーフ到達: 第 3 レベルの対応するノードに進み、一致する行の住所(
)を発見。ctid - データ取得: 特定のページへ移動し、情報を取得します。フルテーブルスキャンを回避できます。
インデックスの実装詳細:PageInspect を活用して
インデックスの内部構造を確認するには、拡張機能
pageinspect を使用します。PRIMARY KEY や UNIQUE 制約もデフォルトで B-Tree インデックスを作成します。
ステップ 1: 拡張と単純なデータ挿入
CREATE EXTENSION IF NOT EXISTS pageinspect; -- テーブル作成 CREATE TABLE IF NOT EXISTS public.client ( id int, name varchar); CREATE INDEX idx_client ON public.client (id); -- データ挿入 INSERT INTO public.client VALUES (1, 'Row #1'); -- メタデータ確認 SELECT * FROM bt_metap('idx_client');
- Fast Root: ページ番号
。木のトポロジーと検索方法を定義します。1 - bt_page_stats: 特定ページの統計情報(タイプ、アイテム数など)を取得します。
- bt_page_items: ページ内の具体的な要素(
など)を返します。ctid
この時点では、タイプは "L" (Leaf) であり、ルートも葉として機能しています。
ステップ 2: データ量増加に伴う構造変化
データを 1,000 件追加し、インデックスの再構築を確認します。
TRUNCATE TABLE public.client; INSERT INTO public.client SELECT i, 'Row #'||i::VARCHAR FROM generate_series(1,1000) AS k(i); SELECT * FROM bt_metap('idx_client');
構造変化の観察
- タイプ: "Root" に変化します。
- アイテム数: ルートノードには複数の子へのポインタ(例:2 つ目以降)が生成されます。
- 範囲管理: ルートからはリーフへジャンプするブリッジ(ページ間リンク)が形成されます。
- レーフ 1: 0 〜 366
- レーフ 2: 367 〜 732
- レーフ 3: 733 以降
ポインタと効率化の仕組み
- ブリッジ: ページ間を結ぶポインタ(
,btpo_prev
)。btpo_next - ハイキー: ページ内の最初のレコード。境界を示し、後続のデータから始まります。
- 最適化のポイント: ルートを再訪問するのではなく、ポインタを辿って次の状態へ直接移動できるため検索が高速化します。
結論:構造からの学び
厳密に言えば、B-Tree は「木」ではなく、リーフ同士が相互接続された有向グラフです。
- ブリッジ(ページ間リンク): オプティマイザが次にどこに進むべきかを理解させます。
- 効率性の向上: ルートを介して全ページを再訪問するのではなく、ポインタを経由して素早くターゲットに到達できます。
- 原子単位での管理: ページはテーブルとインデックスの両方で原子として機能し、統一された構造がストレージニーズをサポートしています。
この内部機構を理解することで、より効率的なクエリプランニングやシステム設計が可能になります。