階層型DBMSの逆襲:現代に通じる「二次索引(Secondary Indexing)」の極意
おい、設計レビューの手を止めてくれ。
今、君が書こうとしているそのスキーマ定義、本当に「現場で生き残る」代物か?
「階層型DBMS? 古臭い遺物だろ」——そう嘲笑う者ほど、実務の現場で複雑な多対多の関連や、予測不可能なアドホッククエリに殴られ、夜間バッチの海で溺れることになる。IMSやCODASYL、あるいは現代のドキュメント指向DBに内包される階層モデルの本質を理解していないエンジニアは、ポインタの迷宮で迷子になる運命なのだ。
今回は、階層型DBMSの最大の弱点であり、同時に「正しく飼い慣らせば最強の武器になる」二次索引(Secondary Indexing)について、チーフアーキテクトの私から容赦ない知見を授けよう。
—
1. なぜ階層型DBMSに「二次索引」が必要なのか?
階層型DBMS(Hierarchical DBMS)の美しさは、その厳格な親子関係(Parent-Child Relationship)にある。親セグメントを起点としたルートパス探索は、リレーショナルデータベース(RDB)のB+樹木探索をも凌駕する圧倒的なパフォーマンスを叩き出す。物理的なディスク上の近接性(Clustering)が保たれているからだ。
だが、現実のビジネス要件は残酷だ。
「顧客ID(親)を起点としない、『メールアドレス』や『契約ステータス』による横断的な逆引きをしたい」
階層のルートを通らないこのリクエストに対し、愚直な全件スキャン(Hierarchical Sequential Scan)を選ぶようなエンジニアは、即座にコードレビューで差し戻す。ここで登場するのが二次索引(Secondary Indexing / 索引セグメント)だ。
—
2. 内部構造:ポインタの迷宮をどう構築するか
RDBのインデックスとは異なり、階層型DBMSにおける二次索引は、データ構造の「外側」に独立した索引ツリーを構築し、そこから実データのセグメントを指す物理ポインタ(または論理アドレス)を引く仕組みをとる。
模式的に見ると、こうだ。
[Secondary Index Root]
├── Key: “user_a@example.com” —> [Pointer] ==(直結)==> [Customer Segment (Node)]
└── Key: “user_b@example.com” —> [Pointer] ==(直結)==> [Customer Segment (Node)]
ここで実務上、極めて重要なアーキテクチャ上の選択を迫られる。
「ポインタの安定性(Pointer Stability)」の問題だ。
階層型データベースでは、セグメントの挿入や削除、あるいは可変長データの拡張によって、物理的な格納アドレスが頻繁に変動する。安易にダイレクト・アドレス・ポインタ(物理アドレス)を索引に持たせると、データが移動した瞬間にインデックスが破壊される(いわゆる「ダングリング・ポインタ」の発生だ)。
堅牢な設計パターン:IDT(Indirect Data Table)方式の採用
実務で堅牢なシステムを構築する場合、二次索引の葉(Leaf)から直接データセグメントを指すな。間に間接参照テーブル(IDT: Indirect Data Table)を挟め。
1. 二次索引は「検索キー」から「一意の内部識別子(DBL/RBA)」を引く。
2. その識別子を元にIDTをルックアップし、最新の物理アドレスを特定する。
この2段構えにすることで、データ再編成(Reorganization)時のインデックス再構築コストを劇的に抑え込むことができる。
—
3. 実践:DDLとスキーマ定義の作法
では、概念をコードに落とし込もう。
仮想的な階層型DBのDDL(Data Definition Language)を用いて、主階層と、そこにぶら下がる二次索引の定義を示す。
— ==========================================
— 1. プライマリ階層スキーマの定義
— ==========================================
DATABASE EnterpriseDB;
SEGMENT TYPE Company
ROOT
PREFIX (CompanyID CHAR(8))
DATA (
CompanyName CHAR(50),
FoundedDate DATE
);
SEGMENT TYPE Department
PARENT Company
PREFIX (DeptCode CHAR(4))
DATA (
DeptName CHAR(30)
);
SEGMENT TYPE Employee
PARENT Department
PREFIX (EmployeeID CHAR(6))
DATA (
FullName CHAR(40),
Email CHAR(60), — ← これを二次索引のキーにしたい
Status CHAR(10) — ← 「ACTIVE」「RETUNED」など
);
— ==========================================
— 2. 二次索引(Secondary Index)の定義
— ==========================================
— Emailアドレスによる逆引きを高速化する索引
INDEX TYPE EmployeeEmailIndex
ON Employee (Email)
TARGET EmployeeID — ターゲットとなるプライマリキー
OPTIONS (
UNIQUE = TRUE, — メールアドレスは一意制約
POINTER = INDIRECT — 前述のIDT方式によるポインタ保護
);
— ステータスによるバッチ処理用非ユニーク索引
INDEX TYPE EmployeeStatusIndex
ON Employee (Status)
TARGET EmployeeID
OPTIONS (
UNIQUE = FALSE, — 同一ステータスの従業員が複数存在するため
POINTER = INDIRECT
);
コードレビューのポイント
- `TARGET EmployeeID` の指定: 索引がどのセグメントのどのキーを指しているのかを明示的にバインドしている。ここが曖昧な設計は論外だ。
- `POINTER = INDIRECT`: 実務ではパフォーマンスの微差よりも、データ再編成時の可用性を優先せよ。直リンクはデッドロックの温床になる。
—
4. パフォーマンス上の罠と「現場の鉄則」
二次索引は万能薬ではない。むしろ、使い方を誤ると階層型DBMSの最大の武器である「書き込み性能」を完全に殺す毒薬となる。
罠1:多すぎる二次索引による「書き込み増幅(Write Amplification)」
親セグメントの下に子、孫とぶら下がる階層構造において、末端のセグメントに頻繁なUPDATE/INSERTが発生する場合、それに紐づくすべての二次索引ツリーが同期して更新される。
RDBと同様だが、階層型では構造のメンテナンストポロジーが複雑なため、「インデックスの数に比例して、トランザクションのロック保持時間が幾何級数的に伸びる」現象が起きる。
> 【チーフアーキテクトの鉄則】
> 「迷ったら作るな。どうしても必要なリードパス(Read Path)が全体の20%以上を占め、かつフルスキャンコストが許容できない場合のみ導入せよ」
罠2:非ユニーク索引の「スフェロイド(膨張)問題」
`Status CHAR(10)` のように、カーディナリティ(値の重複度)が低いフィールドに二次索引を作るとどうなるか?
例えば、100万件の社員データのうち、`Status = ‘ACTIVE’` が90万件あるとする。この状態で `EmployeeStatusIndex` を経由して検索を行うと、DBMSはインデックス側で90万件のポインタリストをスキャンすることになり、実データへのランダムアクセス(ランダムI/O)の嵐が発生する。結果として、フルスキャンした方が遥かに速かったという笑えない事態を招く。
> 【チーフアーキテクトの鉄則】
> カーディナリティの低い(重複度が高い)カラムに二次索引を張るな。もしステータス検索が必要なら、階層構造のセグメント分割そのものを見直せ(例:アクティブ社員セグメントと退職者セグメントを物理的に分離する等)。
—
5. おわりに
二次索引は、階層型DBMSの「美しき縦社会(階層)」に、現代的な「自由な横断検索」を持ち込むための巧妙なブリッジだ。
しかし、その便利さの裏側には、ポインタ管理のコスト、書き込み時のロック競合、そしてカーディナリティの見極めという、エンジニアとしての基礎体力が露骨に試される罠が仕掛けられている。
「なぜこのインデックスが必要なのか」
「この索引は、夜間バッチの書き込み性能を何パーセント劣化させるのか」
これらをロジカルに説明できないうちは、私のレビューを通すことはできない。
さあ、手を動かし、自分の書いたスキーマのポインタの動きを脳内でトレースしてみせろ。健闘を祈る。
コメント