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 GroupSegmentで構成されます:

テーブル(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を解説し、シリーズ全体の比較表でまとめます。

参考文献

コメントする