TL

ヒープファイルとテーブルアクセスメソッドの内部

インデックスなしで行はどう置かれ、どう見つかるのか。ヒープ編成・FSM・可視性マップ・TID の仕組みが分かれば、シーケンシャルスキャンや INSERT が速い/遅い理由まで腑に落ちます。

応用ヒープファイルFSM可視性マップTIDデータベース最終更新: 2026-06-21
TL;DR要点だけ先に
  • 1.ヒープは行を順序なしで詰める表構造。各ページはヘッダ+末尾から伸びる行ポインタ配列+先頭から伸びるタプル本体で、TID(ページ番号, スロット番号)が行の物理位置を一意に指す。
  • 2.空き領域マップ(FSM)が「どのページに何バイト空きがあるか」を木で持ち、INSERT の置き場所を全ページ走査なしで決める。可視性マップ(VM)は「全タプルが全トランザクションから可視なページ」を1ビットで記録する。
  • 3.シーケンシャルスキャンはヒープを物理順に舐め、各タプルの可視性を MVCC で判定して返す。インデックスを介さない物理走査の速さは、ページ密度・HOT 更新・VM ヒットに左右される。

ヒープとは何か――順序を持たない表の置き方

リレーショナル DB の表は、論理的には行の集合ですが、物理的には何らかのファイル編成でディスク上に並びます。最も基本的なのがヒープファイル(heap file)です。ヒープとは「行を特定の順序で並べず、入る場所に入れていく」非整列の集まりを指します。データ構造のヒープ(優先度付きキュー)とは無関係で、「がらくたを山積みにした状態」という語感に近いものです。

PostgreSQL・Oracle・SQL Server(既定)などはテーブル本体をヒープとして持ち、インデックスはそれとは別の B+Tree として独立に置きます。行の物理位置は挿入順とフリースペースの都合だけで決まり、列の値とは無関係です。だからこそ「キー順に物理整列される」クラスタ化インデックスとは対照的で、ヒープ自体には順序がない代わりに、どのインデックスからも公平な距離で参照できます。

アクセスメソッドという抽象

PostgreSQL では「ヒープ」は テーブルアクセスメソッド(table access method, heap AM)の一実装です。実行エンジンは「タプルを順に返せ」「この TID のタプルを取れ」といった抽象 API を呼ぶだけで、物理的にヒープなのか別実装なのかを意識しません。インデックスアクセスメソッド(btree/gin など)と対になる概念で、ストレージ層を差し替え可能にする境界です。

ページ内レイアウト――スロット化ページ

ヒープは固定サイズのページ(PostgreSQL なら 8KB)の連なりです。各ページは可変長のタプルを格納するため、スロット化ページ(slotted page)という巧妙な配置を採ります。1ページの中身は3つの領域に分かれます。

領域位置中身
ページヘッダページ先頭(固定長)空き領域の開始・終了オフセット、LSN、各種フラグ
行ポインタ配列(line pointer)ヘッダ直後から下へ伸びる各タプルの (オフセット, 長さ) を指す小さな固定長エントリ
タプル本体ページ末尾から上へ伸びる実際の行データ。可変長で末尾側から詰める

ポインタ配列は前から、タプル本体は後ろから伸び、両者が出会った地点が満杯です。この設計の要点は、タプルを直接アドレスで指さず、必ず行ポインタ(line pointer / item ID)を経由して指すことにあります。間接参照を1段挟むことで、ページ内でタプルを物理的に動かしても(後述の VACUUM による空き詰めなど)、外部のアドレスを書き換えずに済みます。ポインタの番号さえ保てば、本体はページ内のどこに移しても構いません。

8KB ページの内部(概念図)
+----------------------------------------------------------+
| ヘッダ | LP1 | LP2 | LP3 |→  ……空き……  ←| tup3 | tup2 | tup1 |
+----------------------------------------------------------+
  LP(行ポインタ)は前から、タプル本体は後ろから詰める。
  LP2 = (offset, length) でページ内の tup2 を指す。

TID――行の物理アドレス

ヒープ上の1タプルを一意に指す住所が TID(Tuple Identifier、別名 CTID)です。TID は (ブロック番号, スロット番号) の2要素からなります。ブロック番号はヒープファイル内のページ番号、スロット番号はそのページの行ポインタ配列の何番目か、を表します。SQL Server や Oracle の RID(Row Identifier)も同じ発想で、「何番ページの何番スロット」を符号化したものです。

TID が重要なのは、インデックスの葉が指す先がこの TID だからです。セカンダリインデックスを下って葉に着くと、そこには列値ではなく TID が入っており、その TID でヒープを1回ランダム参照して行本体を得ます。この二段参照の仕組みはクラスタ化/非クラスタ化インデックスの内部差で詳述しています。逆に言えば、TID が安定して同じ行を指し続けることが、インデックスの整合性の前提になります。

TID は永続的な行 ID ではない

TID は「現在の物理位置」であって、論理的な行 ID ではありません。行を UPDATE すると MVCC では新しいバージョンが別の場所に作られ、TID が変わり得ます。テーブルを CLUSTER で並べ替えたり、VACUUM FULL で再構築したりしても TID は変動します。アプリが ctid を主キー代わりに保存して後で参照する、といった使い方は壊れます。永続的な識別子には必ずサロゲートキーを使ってください。

空き領域マップ(FSM)――INSERT の置き場所を即決する

新しい行を INSERT するとき、DB は「十分な空きがあるページ」を探さねばなりません。全ページを順に調べていては表が大きいほど INSERT が遅くなります。これを避ける索引が空き領域マップ(Free Space Map, FSM)です。

FSM は各ヒープページの空きバイト量を粗い分解能(PostgreSQL ではページ1枚あたり1バイト=256段階、約32バイト刻みの線形な区分)で記録し、それを木構造で持ちます。各ノードが配下の最大空き量を保持するので、「Nバイト以上空いているページ」を根から下りながら対数時間で1つ見つけられます。FSM 自体もヒープと並ぶ別ファイルで、表の数百分の一程度と小さく、メモリに載りやすいのが利点です。

INSERT に必要な空き = タプル長 + 行ポインタ1個ぶん
1) FSM の根から「必要量以上の空きを持つページ」を探索(対数時間)
2) 見つかればそのページに追記、見つからなければ末尾に新ページを割り当て
3) 実際の残り空きで FSM のそのページのエントリを更新
fillfactor と更新の空き確保

ページを 100% 埋めると、その行を更新したとき同じページに新バージョンを置けず、別ページへ飛ばざるを得ません(後述の HOT が効かなくなる)。そこで fillfactor で「INSERT 時に各ページを何 % まで埋めるか」を指定し、更新の余地をページ内に残せます。更新の多い表で fillfactor を下げると、同一ページ内更新が増えてインデックス更新を節約できます。FSM はこの「予約済みの空き」を加味して置き場所を返します。

可視性マップ(VM)――スキャンと VACUUM を加速する

MVCC では1行に複数バージョンが存在し、各タプルが「どのトランザクションから見えるか」は、タプルに刻まれた xminxmax(生成・削除トランザクション ID)と各トランザクションのスナップショットで決まります(MVCC の内部実装参照)。この可視性判定は1タプルごとに行う必要があり、コストがかかります。

可視性マップ(Visibility Map, VM)は、ヒープページ1枚につき2ビットを持つ別ファイルで、片方のビットが「このページ上の全タプルが、現在実行中・今後開始する全トランザクションから可視か(=未来のどのスナップショットでも見える、いわゆる all-visible)」を表します。このビットが立ったページは、個々のタプルの可視性チェックを丸ごと省けます。

VM が効く代表がインデックスオンリースキャンです。本来インデックスから TID を得たら可視性確認のためヒープを読みに行く必要がありますが、その TID のあるページの VM ビットが all-visible なら、ヒープを読まずにインデックスの値だけで結果を返せます。ヒープへのランダム参照を丸ごと省けるため、カバリングインデックスの効果が跳ね上がります。もう片方のビット(all-frozen)は VACUUM が「凍結済みで再走査不要」と判断する印で、トランザクション ID 周回(wraparound)対策の凍結 VACUUM が、変化のない巨大領域を読み飛ばすのに使われます。

VM は VACUUM が立て、更新が倒す

VM の all-visible ビットを立てるのは VACUUM です。あるページの不要タプルを掃除し、全タプルが all-visible だと確認できたらビットを立てます。一方、そのページに新たな INSERT/UPDATE/DELETE が入ると、未確定バージョンが生じるのでビットは即座にクリアされます。つまり VM は「掃除されてから次に変更されるまで」だけ真を保つ、保守的(誤って all-visible とは言わない)な近似情報です。だから VACUUM を怠ると VM が立たず、インデックスオンリースキャンが効かなくなります。

シーケンシャルスキャン――インデックスを介さない走査

ヒープの最も素朴なアクセスがシーケンシャルスキャン(seq scan)です。ヒープファイルを物理的に先頭ページから順に読み、各ページの行ポインタ配列を辿って全タプルを取り出し、1件ずつ可視性を判定して、可視なものを実行エンジンへ返します。

for ページ p in ヒープの全ページ(物理順):
    p をバッファプール経由で読む
    for 行ポインタ lp in p:
        if lp が使用中:
            tup = lp が指すタプル
            if MVCC 可視性判定(tup, スナップショット) が真:
                上位ノードへ tup を返す

物理順の読み出しなのでディスクはシーケンシャル I/Oになり、1ページ当たりの単価が安いのが特徴です。だから「表の大半の行が条件に合う」クエリでは、インデックスで TID を引いてヒープをランダム参照する二段参照より、ヒープを丸ごと舐めるほうが速くなります。オプティマイザが選択率(条件に合う行の割合)を見てインデックスを捨て seq scan を選ぶのは、このシーケンシャル I/O < ランダム I/O 連発という単価差が根拠です。

HOT 更新――インデックスを汚さない更新

UPDATE で更新列がどのインデックスにも含まれない場合、PostgreSQL は HOT(Heap-Only Tuple)更新 を使えます。新バージョンを同じページ内に置き、旧タプルの行ポインタから新タプルへ HOT チェーンで繋ぎます。新バージョンは独自の行ポインタを持たないため、どのインデックスも更新不要です。インデックスは旧 TID を指したままでよく、ヒープ側のチェーンを辿れば最新版に届きます。fillfactor で同一ページの空きを残しておくほど HOT 更新が成立しやすく、インデックスの肥大化と更新コストを抑えられます。

VACUUM とデッドタプル

MVCC では DELETE や UPDATE は古いバージョンを即時に消さず、誰からも見えなくなったデッドタプルとして残します。これを回収するのが VACUUM です。VACUUM はヒープを走査してデッドタプルの行ポインタを解放し、FSM に空き量を反映し、ページが all-visible になれば VM ビットを立てます。回収を怠るとデッドタプルがページを占有し続け(テーブル肥大化, bloat)、seq scan が読む無駄ページが増え、ページ密度が下がって性能が落ちます。

VACUUM が触ったページや FSM・VM の更新もまた WAL に記録され、クラッシュ後に整合した状態へ復旧されます。ヒープの物理変更が安全に永続化される土台はWAL(先行書き込みログ)の仕組みにあります。

試験・面接で問われる定番

「インデックスがあるのに seq scan が選ばれた、なぜ」は選択率とシーケンシャル/ランダム I/O 単価差で答えます。「インデックスオンリースキャンが効く条件」は、必要列が索引に含まれ、かつ対象ページが VM で all-visible(=VACUUM 済み)であること。「ctid を行 ID に使ってよいか」は否(UPDATE/VACUUM で変動する物理アドレス)。FSM=置き場所探索、VM=可視性スキップ、TID=物理アドレス、と役割を一言で言い分けられると強いです。

まとめ

ヒープファイルは「行を順不同で詰め、TID という物理アドレスで指す」だけの素朴な表構造ですが、その周りに3つの補助構造が効いています。FSM は INSERT の置き場所を全走査なしで即決し、VM は可視性判定とヒープ参照を丸ごと省いてインデックスオンリースキャンと凍結 VACUUM を加速し、スロット化ページの行ポインタは VACUUM がページ内でタプルを動かしても外部アドレスを壊さない自由を与えます。

インデックスを介さない物理走査の速さは、ページ密度・HOT 更新・VM ヒット率という地味な要素で決まります。索引側から TID を辿る二段参照の代償はクラスタ化/非クラスタ化インデックスの内部差を、可視性判定そのものの仕組みはMVCC の内部実装を参照してください。

データベース Article

ヒープファイルとテーブルアクセスメソッドの内部を実務で読む

TL;DRは入口です。実際に選ぶ・使う段階では、何を解決するか、何と比較するか、導入後にどこで詰まるかまで見る必要があります。

解決すること

ヒープファイル

比較で見る軸

難易度: advanced / カテゴリ: データベース / タグ数: 5

導入後に効く点

空き領域マップ(FSM)が「どのページに何バイト空きがあるか」を木で持ち、INSERT の置き場所を全ページ走査なしで決める。可視性マップ(VM)は「全タプルが全トランザクションから可視なページ」を1ビットで記録する。

先に潰すリスク

用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。

数字・仕様の読み方
難易度
advanced
カテゴリ
データベース
タグ数
5

判断チェックリスト

  • 自社の用途が「ヒープファイル / FSM」に近いか確認する。
  • 強みである「ヒープは行を順序なしで詰める表構造。各ページはヘッダ+末尾から伸びる行ポインタ配列+先頭から伸びるタプル本体で、TID(ページ番号, スロット番号)が行の物理位置を一意に指す。」が本当に評価軸になるか確認する。
  • 注意点の「用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。」を運用で吸収できるか確認する。
  • 公開値や仕様値は、対象プラン・対象機種・対象リージョンまで確認する。
  • 既存システム、ID、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
  • 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。

次に確認する観点

ヒープファイルFSM可視性マップTIDデータベースヒープファイルFSM可視性マップ
参考: 公式情報