Index Only Scanとは

Index Only Scanはインデックスだけを読んでテーブル本体(ヒープ)にアクセスせず結果を返す実行方式である。テーブルへのアクセスが不要な分、通常のIndex Scanより高速になる。

SELECTWHEREで参照する列がすべてインデックスに含まれていると、Index Only Scanが使われる。

通常のインデックスでは対応できない例

balance列にインデックスを作り、balanceとともにname列も取得するクエリを実行する。

CREATE INDEX idx_accounts_balance ON accounts (balance);

EXPLAIN (ANALYZE, BUFFERS) SELECT balance, name FROM accounts WHERE balance BETWEEN 10000 AND 10010;
 Index Scan using idx_accounts_balance on accounts  (cost=0.29..48.51 rows=11 width=13) (actual time=0.014..0.293 rows=13 loops=1)
   Index Cond: ((balance >= 10000) AND (balance <= 10010))
   Buffers: shared hit=15
 Planning:
   Buffers: shared hit=85
 Planning Time: 0.242 ms
 Execution Time: 0.317 ms

idx_accounts_balanceにはbalance列しか含まれておらずname列を取得できないため、Index Scanが選択されテーブル本体へのアクセスが発生する。

INCLUDE句でIndex Only Scanにする

CREATE INDEXINCLUDE句を使うと、検索条件には使わないが結果として返したい列をインデックスに追加できる。

DROP INDEX idx_accounts_balance;
CREATE INDEX idx_accounts_balance_include ON accounts (balance) INCLUDE (name);

同じクエリを実行すると、実行計画がIndex Only Scanに変わる。

 Index Only Scan using idx_accounts_balance_include on accounts  (cost=0.42..4.68 rows=13 width=13) (actual time=0.032..0.033 rows=13 loops=1)
   Index Cond: ((balance >= 10000) AND (balance <= 10010))
   Heap Fetches: 0
   Buffers: shared hit=1 read=3
 Planning:
   Buffers: shared hit=16
 Planning Time: 0.181 ms
 Execution Time: 0.054 ms

name列をインデックスに含めたことでSELECTに必要な列がすべてインデックスから取得できるようになり、Index Only Scanが選択されている。Heap Fetches: 0は、テーブル本体への参照が一切発生しなかったことを示す。コストは4.68、実行時間は0.054msであり、Index Scanのコスト48.51、実行時間0.317msよりも大幅に小さい。

Index Only Scanはテーブルの可視性情報を管理するvisibility mapを利用するため、事前にVACUUMを実行しておく必要がある。更新が多いテーブルではvisibility mapが古くなり、Heap Fetchesが0にならないことがある。

通常の複合インデックスとの違い

balancenameの両方をキーにした複合インデックス(balance, name)でもIndex Only Scanは実現できる。INCLUDE句にはそれとは異なる利点がある。

CREATE UNIQUE INDEX idx_accounts_id_include_balance ON accounts (id) INCLUDE (balance);
Index "public.idx_accounts_id_include_balance"
 Column  |  Type   | Key? | Definition 
---------+---------+------+------------
 id      | integer | yes  | id
 balance | integer | no   | balance
unique, btree, for table "public.accounts"

\dKey?列から分かるように、INCLUDEで追加したbalance列はインデックスのキーではなく付随データとして扱われる。そのためUNIQUE制約はid列のみに適用され、balance列の重複は許容される。複合インデックスの末尾に列を追加する代わりにINCLUDE句を使うと、キー以外の列をB-treeの検索キー比較の対象から外せるため、インデックスのサイズや検索効率の面で有利になる場合がある。