SQL Server Index の内部構造を徹底解説:Clustered Index、Columnstore Index、Include 列まで

はじめに

この記事では、SQL Serverのインデックスの内部実装を深掘りします。

シリーズ4本目です。SQL ServerはInnoDBと同じくClustered Indexを持ちますが、Columnstore IndexというDWH向けの列指向インデックスが特徴的です。SQL Server 2019 / 2022を中心に解説します。

Clustered IndexとHeap

SQL ServerはInnoDBと同じくClustered Indexをサポートしますが、Heapテーブルも選択できます。

Clustered Index

PRIMARY KEYを作成すると、デフォルトでClustered Indexになります:

CREATE TABLE employees (
    employee_id INT PRIMARY KEY,  -- Clustered Index
    name NVARCHAR(100),
    department_id INT
);

InnoDBとの違い: – InnoDBは常にClustered Index(選択の余地なし) – SQL ServerはClustered Indexなし(Heap)も選べる

-- Heapテーブル(Clustered Indexなし)
CREATE TABLE logs (
    log_id INT,
    message NVARCHAR(MAX),
    created_at DATETIME2
);
-- PRIMARY KEYにNONCLUSTEREDを指定してもHeapにできる
ALTER TABLE logs ADD CONSTRAINT PK_logs PRIMARY KEY NONCLUSTERED (log_id);

RID(Row Identifier)

HeapテーブルではPostgreSQLのctid、OracleのROWIDに相当するRIDで行を特定します:

RID = FileID : PageID : SlotNumber
InnoDB PostgreSQL Oracle SQL Server
デフォルト Clustered Index Heap Heap Clustered Index
行の物理ID ― ctid ROWID RID
Nonclustered Indexのリーフ PK値 ctid ROWID Clustered Key or RID

Nonclustered Index

Clustered Tableの場合

Nonclustered IndexのリーフにはClustering Key(Clustered Indexのキー)が格納されます。InnoDBのSecondary Indexと同じです。

Nonclustered Index on name:
  Leaf: [name=Alice, Clustering Key=1]
  → Clustered Indexを検索して行データ取得(Key Lookup)

Heapの場合

Nonclustered IndexのリーフにはRIDが格納されます。PostgreSQLやOracleと同じです。

Nonclustered Index on name (Heap table):
  Leaf: [name=Alice, RID=(1:100:0)]
  → Heapに直接アクセス(RID Lookup)

INCLUDE列(カバリングインデックス)

SQL Serverの強力な機能がINCLUDE列です。インデックスのリーフにキー以外のカラムを追加できます:

-- nameで検索し、departmentも取得したい
CREATE NONCLUSTERED INDEX idx_name
ON employees(name)
INCLUDE (department_id);

-- このクエリはインデックスだけで完結(Key Lookupが不要)
SELECT name, department_id FROM employees WHERE name = 'Alice';

INCLUDE列はリーフノードにのみ格納され、内部ノードには含まれません。そのため木の高さに影響しません。

PostgreSQLも同じ機能を持っています(CREATE INDEX ... INCLUDE、PostgreSQL 11〜)。InnoDBにはこの機能がありません。

Columnstore Index

SQL Server 2012で導入された列指向インデックスです。DWH/分析ワークロードに特化しています。

行指向 vs 列指向

行指向(B-tree):
  Page: [id=1,name=Alice,age=25] [id=2,name=Bob,age=30] [id=3,name=Carol,age=22]

列指向(Columnstore):
  id列:   [1, 2, 3, 4, 5, ...]      ← 同じ型のデータが連続
  name列: [Alice, Bob, Carol, ...]
  age列:  [25, 30, 22, ...]          ← 圧縮が非常に効く

内部構造

Columnstore IndexはRow GroupとSegmentで構成されます:

テーブル(100万行)
  ├─ Row Group 1(約100万行)
  │    ├─ Segment: id列(圧縮済み)
  │    ├─ Segment: name列(圧縮済み)
  │    └─ Segment: age列(圧縮済み)
  ├─ Row Group 2(約100万行)
  │    ├─ Segment: id列
  │    ├─ Segment: name列
  │    └─ Segment: age列
  └─ Delta Store(未圧縮の新規行)← B-tree構造
  • Row Group:約100万行をまとめた単位
  • Segment:1つのRow Group内の1カラム分のデータ(圧縮済み)
  • Delta Store:新規INSERT分を一時的に保持するB-tree。バックグラウンドでRow Groupに圧縮統合

Clustered Columnstore Index

テーブル全体をColumnstoreにできます:

CREATE CLUSTERED COLUMNSTORE INDEX cci_sales ON sales;

これにより、テーブルのベース構造自体が列指向になります。B-tree Clustered Indexとの共存はできません。

各DBのDWH向けインデックス比較

InnoDB PostgreSQL Oracle SQL Server
DWH向け なし BRIN Bitmap Index Columnstore Index
圧縮 ページ圧縮 TOAST HCC(Exadata) Columnstore圧縮(10x〜)
列指向 なし なし In-Memory Column Store Columnstore Index

Filtered Index

SQL Server固有の機能で、条件付きインデックスを作成できます:

-- アクティブなユーザーだけのインデックス
CREATE NONCLUSTERED INDEX idx_active_users
ON users(name)
WHERE is_active = 1;

PostgreSQLのPartial Indexと同じ概念です:

-- PostgreSQL版
CREATE INDEX idx_active_users ON users(name) WHERE is_active = true;

InnoDBとOracleにはこの機能がありません。

まとめ

トピック InnoDB PostgreSQL Oracle SQL Server
テーブル構造 Clustered Index(常に) Heap Heap(IOT可) Clustered or Heap(選択可)
主要インデックス B+Tree B-tree, GiST, GIN B-tree, Bitmap B-tree, Columnstore
INCLUDE列 なし あり(11〜) なし あり
条件付きIndex なし Partial Index なし Filtered Index
列指向 なし なし In-Memory Columnstore Index
NULLの扱い 含む 含む 全キーNULL除外 含む

次回はDb2 Indexを解説し、シリーズ全体の比較表でまとめます。

参考文献

コメントする