【実務・中級編】 HISAM (Hierarchical Indexed Sequential Access Method) の定義 – 階層型DBMS

HISAMの極意:なぜ現代の我々が「階層型DBMS」のインデックス構造から学ぶべきなのか

おい、そこの設計レビューの手を止めてこっちを向いてくれ。

今、君が画面に映し出しているそのマイクロサービスのRDBスキーマ、本当にそのトラフィックに耐えられるか? 何でもかんでも正規化して、JOINを何重にも重ね、緩慢なクエリがスローログの常連になってから「インデックスを追加しましょう」で済ませていないか?

少し原点に立ち返ろう。我々が扱うデータの本質、それは多くの場合「ツリー構造(階層構造)」だ。ECの注文・明細・配送先、組織のツリー、あるいはドキュメントの構造。これらをリレーショナルに無理やり平坦化し、複雑なJOINで再構築するアプローチは、本当にベストプラクティスと言えるだろうか?

今回は、メインフレームの時代から生き残り、今なお超高速なバッチ処理や基幹系システムの底を支えるHISAM(Hierarchical Indexed Sequential Access Method)のスキーマ定義と構造の真髄を紐解く。

これは単なるレトロな技術の墓参りではない。「ポインタとインデックスの物理配置を極限までチューニングし、I/Oを最小化する」という、全エンジニアが知るべきデータアクセスのプリミティブな極意なのだ。

—

1. HISAMのアーキテクチャ:なぜ「索引順次」なのか

HISAMを理解するためには、その名の由来を骨の髄まで理解する必要がある。
HISAMとは、「階層型(Hierarchical)」でありながら、「索引付き順次アクセス方式(Indexed Sequential Access Method)」を融合させたものだ。

物理的構造の美学:OSAMとVSAMの協調

現代のRDBのように、行がバラバラのページ(ブロック)に散らばることはHISAMには許されない。HISAMのデータセットは、大別して以下の2つのエリアで構成される。

1. 主データセット(Primary Dataset: KSDS等)

  • ルートセグメント(親)がキー順に並び、それに対するインデックスが張られている。
  • ルートセグメントと、その物理的子孫セグメントの一部が同じ論理レコード(物理ブロック)内に「プレフィックスとデータ」として直列化されて格納される。

2. 副データセット(Overflow Dataset: ESDS等)

  • 1つの物理ブロックに収まりきらなかった従属セグメント(子や孫)があふれた場合、ここにオーバーフローレコードとして飛ばされ、ポインタで結ばれる。

[HISAMの物理ストレージイメージ]

+———————————————————+
| Primary Dataset (KSDS) |
| [Root: Key 001] -> [Child A] -> [Child B (OverflowPtr)] |
+———————————————————+
| (インデックスによる高速ルックアップ)
v
+———————————————————+
| Overflow Dataset (ESDS) |
| [Child B’s overflow data] -> [Grandchild …] |
+———————————————————+

この構造の何が強烈か?
「ルートセグメントをキーで順次スキャンしながら、従属セグメントへはポインタで一撃でジャンプする」という、バッチ処理において最強のアクセスパターンをハードウェアレベルのI/O効率で実現できる点だ。

—

2. DDLとスキーマ定義:論理階層を物理にマッピングする

では、実際の設計レビューに入ろう。
HISAM(あるいはそれを模した階層型DDL)におけるスキーマ定義は、単にカラムの型を並べる作業ではない。「どのセグメントを同じ物理ブロックに同居させ、どのポインタを張るか」をコンパイル時に決定する極めてエンジニアリング的な作業だ。

以下に、典型的な受注・明細・配送情報を表す階層スキーマの定義例を示す。

— =====================================================================
— 【設計レビュー用】HISAM 階層スキーマ定義 (Conceptual DDL)
— =====================================================================

— 1. データベース全体の定義
DATABASE OrdersDB
ACCESS METHOD HISAM;

— 2. ルートセグメント:注文ヘッダ (物理的順序の基準となる)
SEGMENT Root ORDER_SEG
PARENT NONE
— ルートの識別子(これがKSDSのクラスタリングキーになる)
KEY (order_id CHAR(10) UNIQUE)
— 同一ブロック内に優先的に格納するサイズの見積もり
PREFIX LENGTH 16
DATA LENGTH 128;

— カラム定義
COLUMN order_id CHAR(10);
COLUMN customer_id CHAR(8);
COLUMN order_date DATE;

— 3. 従属セグメント:注文明細 (ルートの直下に物理配置される)
SEGMENT Dependent ITEM_SEG
— 親セグメントの指定
PARENT ORDER_SEG
— 明細番号によるソート順
KEY (item_id CHAR(4) ASC)
— オーバーフロー発生時のハンドリング戦略
POINTER IS HierarchicalForward;

COLUMN item_id CHAR(4);
COLUMN product_code CHAR(12);
COLUMN quantity DECIMAL(5, 2);
COLUMN unit_price DECIMAL(7, 2);

— 4. 孫セグメント:配送先情報 (明細のさらに下層、または並列配置)
SEGMENT Dependent SHIPPING_SEG
PARENT ORDER_SEG
KEY (ship_sequence CHAR(2) ASC)
POINTER IS HierarchicalForward;

COLUMN ship_sequence CHAR(2);
COLUMN tracking_no CHAR(20);
COLUMN carrier_code CHAR(4);

チーフアーキテクトからのコードレビュー指摘事項

1. キー設計のミス(致命的)
`ORDER_SEG` のキーに `order_id` を使っているのは良い。しかし、もしこのキーがタイムスタンプベースでランダムに生成されるUUIDだった場合、KSDS(Key-Sequential Data Set)のインデックス分裂(スプリット)が多発し、I/O性能が崩壊する。HISAMのルートキーは、原則として単調増加(Monotonically Increasing)なサロゲートキーか、ドメイン上のシーケンス番号でなければならない。
2. 物理レコード長のサイジング
`ITEM_SEG` や `SHIPPING_SEG` をプライマリデータセットにどこまで詰め込むか。ここを欲張って大きくしすぎると、1つのルートに対する明細が多い注文(例えばまとめ買いなど)で即座にオーバーフロー(ESDS行き)が発生し、ポインタ追跡のオーバーヘッドが増加する。
「平均的な明細数 × レコードサイズ」が、OSの物理ブロックサイズ(通常4KB〜32KB)の範疇に収まるように設計しろ。

—

3. 実務におけるアクセスパターンと最適化設計

「で、実際のアプリケーションからどう叩くんだ?」という声が聞こえてきそうだな。
RDBのSQLにおける `SELECT FROM orders JOIN items ON …` に相当する処理を、HISAMの哲学でどう実装・解釈すべきかを示そう。

パターンA:ルートの順次処理(バッチ処理の王道)

月次売上集計などで全件を舐める場合、HISAMの真価が発揮される。インデックスを辿る必要すらない。プライマリデータセットの物理的な次のブロックを順次読み込むだけだ(Sequential Read)。

// 疑似コード:HISAMカーソルによる高速順次スキャン
DatabaseCursor cursor = db.OpenCursor(“OrdersDB”, “ORDER_SEG”);

while (cursor.MoveNext()) {
OrderHeader order = cursor.GetCurrentRoot();
ProcessOrderHeader(order);

// 従属セグメント(明細)の取得:同一物理ブロック内であればI/Oゼロで走査
ChildCursor childCursor = cursor.OpenChildCursor(“ITEM_SEG”);
while(childCursor.MoveNext()) {
OrderItem item = childCursor.GetCurrent();
ProcessItem(item);
}
}

このコードの美しさは、ディスクヘッドのシークが最小限に抑えられる点にある。 リレーショナルDBのB-Treeインデックスのように、ランダムI/Oの嵐になることがない。

パターンB:特定ルートへのダイレクトアクセス

注文番号を指定して単票を表示する画面などの場合:
1. 主インデックス(KSDSのインデックス部分)を二分探索し、該当するルートセグメントの物理ブロックアドレスをO(log N)で特定。
2. そのブロックをダイレクトに読み込み、必要であればポインタを辿ってオーバーフローセグメントを回収。

これ以上の効率化の余地がないほど無駄のないパスだ。

—

4. パフォーマンスの罠:HISAMが死ぬ瞬間

チーフアーキテクトとして、現場で絶対に避けなければならない「HISAMのアンチパターン」を叩き込んでおく。

1. 頻繁な「挿入」と「削除」による断片化(Fragmentation)

HISAMは、その構造上、データの「更新(特に可変長データの拡張)」や「動的な挿入・削除」に弱い。
特に、既存のルートセグメントの間に新しいキーのルートを挿入する場合、KSDSの物理的な再配置やオーバーフローの連鎖が発生する。

  • 対策: データがイミュータブル(追記型)に近いワークロード、あるいはバッチで一括ロードして日中は参照中心となるマスター・トランザクション系以外には適用するな。頻繁にDELETE/INSERTが走るなら、完全ポインタベースの HDAM (Hierarchical Direct Access Method) へ逃げるか、素直にRDBのLSM-Tree系ストレージを使うべきだ。

2. 「従属セグメント」の肥大化によるオーバーフロー地獄

親(ルート)は1つなのに、子が数千件ぶら下がるようなスキーマ(例:1つの企業に数千人の社員が属する)をHISAMで定義してはならない。
子は瞬時にオーバーフローセットへ追いやられ、ポインタのチェインを辿るためのリニアサーチ(線形探索)が発生する。

  • ルール: 1対多の「多」が大規模になることがわかっている場合、それを物理的な従属セグメントとして同居させてはならない。別の独立したルートセグメントとして定義し、論理ポインタ(Logical Relationship)で結べ。

—

5. 結びにかえて:現代のエンジニアリングへの示唆

HISAMのような階層型DBMSの設計思想は、決して過去の遺物ではない。

現代のNoSQL(Document DBやKey-Valueストア)、あるいはKafkaのようなストリーミングストレージ、さらには分散データベースのパーティショニング戦略においても、「データの物理的な局所性(Locality)をどう定義し、アクセスパスと一致させるか」という問題意識の根底には、このHISAMの血が流れている。

コードレビューで「とりあえずJOINすればいいや」という甘えたプルリクエストを見かけたら、こう問い詰めてほしい。
「そのデータの物理配置とアクセスパスのコストを、HISAMのレベルで説明できるか?」と。

構造を制する者が、システムを制す。次の設計では、データの「階層と物理配置」に、もっと強烈なこだわりを持って臨んでくれ。期待している。

コメント

タイトルとURLをコピーしました