はじめに
この記事では、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を解説し、シリーズ全体の比較表でまとめます。