階層型DBからRDBへの亡霊:移行期に遭遇する「非正規化」の罠と、現代的エンジニアリングによる鎮魂歌
テックリードの私だ。コードレビューや設計レビューの場において、最近こんなセリフを聞いたことはないか?
> 「レガシーなメインフレームの階層型DB(IMSなど)にあるマスターデータを、そのままリレーショナルデータベース(RDB)に移行する。整合性を担保するため、当然第3正規形まで綺麗に正規化しよう」
……待て。その設計、秒速で差し戻しだ。
いや、理論の美しさだけを見ればその通りだ。データ重複を排し、アノマリーを防ぐ。教科書通りの美しい正規化。しかし、お前が移行対象としているそのデータ構造は、「物理的なポインタによるハードリンク」を前提に最適化された階層型DBの残骸だということを忘れている。
今回は、階層型DBからRDBへシステムを移行する際、誰もが直面する「非正規化問題」の本質と、現代のシニアエンジニアが取るべき実践的解法を授けよう。
—
1. なぜ階層型DBの「当たり前」は、RDBの「毒」になるのか?
まず、敵を知るために階層型DB(Hierarchical DBMS)の本質を振り返る。
階層型DBは、データを「親-子」のツリー構造で保持する。物理レベルにおいて、子レコードは親レコードの物理アドレス(ポインタ)を保持するか、隣接ブロックに物理的に配置される。
この構造が意味することは一つだ:「結合(JOIN)のコストがゼロ、または極めて低い」。
階層型DBにおけるツリー構造のイメージ
[会社: ACME Corp] (物理ポインタ)
└── [部門: 開発部] (物理ポインタ)
├── [社員: Alice]
└── [社員: Bob]
階層型DBでは、「会社」から「社員」までをたどる際、インデックススキャンやハッシュ結合のような複雑なクエリ最適化は不要だ。単にポインタを辿るだけ($O(1)$に近いオーダー)で、一撃で目的のデータに到達できる。
そのため、旧来のアプリケーションロジックは、この「一網打尽のツリー取得」を前提にベタ書きされている。
これをそのままRDBへ持ち込み、律儀に正規化(部門マスター、社員マスター、所属テーブルへ分割)した途端、何が起きるか?
— 階層型DBなら1回のポインタ走査で済むデータ構造を、RDBで再構築した場合の悪夢
SELECT
c.company_id, c.company_name,
d.department_id, d.department_name,
e.employee_id, e.employee_name
FROM companies c
JOIN departments d ON c.company_id = d.company_id
JOIN employees e ON d.department_id = e.department_id
WHERE c.company_id = ‘ACME-001’;
データ量が数千万件規模に膨れ上がったとき、この結合クエリの嵐がCPUを焼き尽くす。
「正規化は正義」という信仰のもとに生み出されたピカピカのRDBは、かつて一瞬で返ってきた画面を描画するために、無数のJOINと重い一時テーブルの生成を強いられ、見事にレスポンスタイムを悪化させるのだ。これが移行時における「非正規化問題」の正体である。
—
2. アプリケーションロジックの断絶とN+1問題の亡霊
パフォーマンスの低下だけではない。より深刻なのはアプリケーション層への波及だ。
階層型DBを使っていたシステムでは、データの取得APIが「ツリー構造そのもの(JSONや独自ドメインモデルのツリー)」を返す設計になっていることが多かった。
これをRDBの正規化されたテーブル群マッピングに置き換えようとすると、ORM(Object-Relational Mapping)の罠にハマる。
【アンチパターン】ORMのデフォルト動作によるN+1問題の発生例
def get_company_tree(company_id: str):
# 1. 会社を取得
company = CompanyRepository.find_by_id(company_id)
# 2. 部門を取得 (1回のクエリ)
departments = DepartmentRepository.find_by_company(company.id)
tree_data = []
for dept in departments:
# 3. 各部門に属する社員を取得 (ここで部門数分だけクエリが発火 = N+1問題)
employees = EmployeeRepository.find_by_department(dept.id)
tree_data.append({
“department”: dept.name,
“employees”: [e.to_dict() for e in employees]
})
return tree_data
このコードをレビューに持ってきたプログラマがいたら、私は即座にコーヒーを一杯奢ったあと、静かに差し戻す。
階層型DBの「一括取得」という強烈なアドバンテージを無視してRDB上でこれをやると、ネットワークラウンドトリップとクエリパースのオーバーヘッドで、サーバーは即座に息絶える。
—
3. 現場で使える「堅牢な設計パターン」:意図的な非正規化の技術
では、どう設計すべきか?
「階層型DBからの移行だからといって、何でもかんでも非正規化していいわけではない」という意見もあるだろう。その通り。無秩序な非正規化は、将来的なデータ不整合(Anomalies)の温床になる。
プロフェッショナルとして、以下の2つの解法(デザインパターン)を使い分けろ。
パターンA:CQRS(Command Query Responsibility Segregation)による「書き込みと読み込みの分離」
もっとも推奨されるモダンなアプローチだ。
- 書き込み系(OLTP): データの整合性を担保するため、RDB上で厳格に正規化する。
- 読み込み系(CQRS / View / Cache): 階層型DBが提供していたようなツリー構造をあらかじめ構築(非正規化)し、ドキュメントDB(MongoDBやPostgreSQLのJSONB型)やRedis、あるいは検索用マテリアライズドビューに非同期で同期する。
— PostgreSQLのJSONBを活用した、あえての「非正規化ドキュメント保存」の例
— 読み込み専用のキャッシュテーブルを用意し、ツリー構造そのものをJSONとして保持する
CREATE TABLE company_tree_cache (
company_id VARCHAR(32) PRIMARY KEY,
tree_payload JSONB NOT NULL,
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
— アプリケーションはこのJSONを1発SELECTするだけで、JOINコストなしでツリーを取得できる
SELECT tree_payload FROM company_tree_cache WHERE company_id = ‘ACME-001’;
このアプローチであれば、データの更新時は正規化されたトランザクションテーブルを安全に叩き、画面表示などの参照時は非正規化されたフラット/ツリーデータを爆速で引くことができる。
パターンB:リレーショナルDB内での「戦略的冗長化(Controlled Denormalization)」
もしインフラの制約上、ドキュメントDBなどを追加できず、単一のRDBで完結させなければならない場合は、「頻繁に結合される親の属性を子テーブルにあえて持たせる」。
例えば、受注履歴と顧客情報の関係において、顧客の「氏名」や「住所」は変更される可能性があるため正規化するのが定石だ。しかし、過去の受注時点の情報をスナップショットとして保持する必要がある場合や、参照頻度が圧倒的に高い場合は、子テーブル(受注テーブル)側に親の情報を冗長に持たせる。
— 受注テーブルにあえて顧客名を冗長保持する設計(戦略的非正規化)
CREATE TABLE orders (
order_id SERIAL PRIMARY KEY,
customer_id INT NOT NULL,
customer_name_snapshot VARCHAR(100) NOT NULL, — ★あえて非正規化して持たせる
total_amount DECIMAL(10, 2) NOT NULL,
ordered_at TIMESTAMP WITH TIME ZONE
);
これにより、過去の顧客名変更の影響を受けずに高速な参照が可能になり、かつJOINの嵐を防ぐことができる。ただし、「どのデータが真実のソース(Single Source of Truth)か」をアプリケーションのドメイン層で厳密に管理する義務が生じることを忘れるな。
—
4. チーフアーキテクトからの提言
階層型DBMSからの移行プロジェクトは、単なる「データベースの引っ越し」ではない。それは「データアクセスのパラダイムシフト」なのだ。
過去のアーキテクチャが持っていた「構造的な強み(ポインタによる高速な階層走査)」を、RDBという全く異なるパラダイムの上でどう再定義するか。そこに必要なのは、教科書通りの正規化を盲信する姿勢ではなく、ビジネス上の要件(Read/Writeの比率、レイテンシ要件、データのライフサイクル)から逆算して物理設計をねじ伏せるプロフェッショナルとしての胆力だ。
「なぜこのテーブルは非正規化されているのか?」
そう問われたとき、パフォーマンス、アプリケーションロジックの複雑性、そしてデータ整合性のトレードオフを論理的に説明できるようにしておけ。
さあ、設計書を開け。そのJOIN、本当に必要か?
コメント