推移的な関係を直結しない
推移的関係をショートカットすると、親を飛ばして子が存在できる矛盾設計になる。
短絡線が生む矛盾
「ユーザーは注文を持ち、注文は明細を持つ」という関係は、本来 2 本の線で段階的に描くべきものです。
users 1──N orders 1──N order_items
ここに「ユーザー ── 明細」の線を直接引いてしまうと、「注文を介さずに明細が存在できる」モデルになります。
- どの注文にも紐付かない明細レコードが存在し得る
- 注文が削除されても明細が宙に残る(別経路で参照され続ける)
- 「合計金額 = 明細の総和」といった業務ルールが、注文を無視して成立してしまう
ER 図の線は「存在の依存関係」そのものです。短絡線を引くと、依存関係が複線化され、どちらの経路が正なのか分からなくなります。
実例 — 会社・部署・社員、ブログの記事とコメント
推移的関係の短絡は、実務で頻繁に出る 2 つの形で覚えておくと気付きやすくなります。
- 会社 → 部署 → 社員: 会社 1:N 部署、部署 1:N 社員。ここに
employees.company_idを追加で生やすと、「部署無しで会社に所属する社員」や「会社と部署で所属先が矛盾する社員」が作れてしまう。素直にemployees.department_idのみにして、会社はJOIN departmentsで辿る - 記事 → コメント → 返信: コメントは記事に、返信はコメントに属するのが自然。ここに
replies.article_idを冗長に持たせると「削除された記事の返信が残る」「別記事のコメントへの返信ができる」等の抜け道ができる
「存在依存を飛び越える線が引かれていないか?」をレビューで必ず確認します。短絡線の誘惑はほぼ全て「クエリを 1 回少なくしたい」という実装都合から来ますが、それは後述の冗長 FKとして明示的に扱うべき設計判断で、こっそり線を引くべきではありません。
※ 逆に「短絡線に見えて実は独立した関係」のケースもあります。例えば「社員 ── メンター社員」の自己参照関係は、階層構造の上下関係とは別軸の情報なので、推移関係ではありません。
JOIN で辿れる関係は線を引かない
実用的な経験則として、「既存の線を辿って JOIN で到達できる関係は、新しい線を引かない」を守ると短絡は起きません。
「ユーザーが持つ明細一覧」は users JOIN orders JOIN order_items で取れるので、ER 図には書かなくてよい情報です。「そこに線が無くても、データモデルとしては到達可能」である点が重要です。
※ これは 推移従属の除去(第 3 正規形)と同じ発想。同じ事実を 2 箇所に持たない、関係も 2 経路に持たない、という原則が通底しています。
集約ルートの考え方
「明細は注文を介してしか操作しない」というルールを、集約ルート(aggregate root) と呼びます。DDD の用語ですが、純粋な DB 設計でも役立つ発想です。
- 集約ルート:
orders(外部から直接触れるエンティティ) - 集約内部:
order_items(必ずordersを経由する)
集約の外側から order_items に直接線を引く = 集約の境界を破る、と捉えると、「短絡線を引くべきか」が見分けやすくなります。迷ったら「このテーブルを単独で削除できるか?」を問うのが有効です。単独で意味を持たないテーブルは、外から直接線を引くべきではありません。
例外: パフォーマンス目的の冗長 FK
短絡が意図的に許容されるケースもあります。order_items に user_id 列を冗長に持たせ、「ユーザーごとの明細合計」を 1 回の JOIN で出したい、といった場面です。
- 用途はパーティショニング・検索高速化・集計用の限定的な目的に
- 正規化に反する冗長列であることを コメントや命名で明示(
denorm_user_id等) - 親(
orders.user_id)と子(order_items.user_id)の同期をトリガーやアプリで保証(差異が出るのが最大の罠)
冗長 FK は「短絡が許される例外」であって、最初から引くものではありません。計測してホットスポットと判断してから導入するのが鉄則です。
