「単独主キー主義」には重大な欠陥があると書いたが、それがもたらす弊害の一例を挙げたい。単独主キーにこだわると、主キーそのものを軽視するようになり、DB設計の基本である「正規化」がよくわからなくなる。とくに第2正規形と第3正規形の違い、言い換えると部分関数従属と推移関数従属の違いが、理屈ではわかっても開発実務で生かせなくなる。
そこらへんを曖昧にしたままでは、「構造計算されていない高層ビルの図面」のようなDB設計が生み出され、業務システム等のDBをコアとするアプリの開発・保守に混乱をもたらす。DB設計に関わる技術者はキャリアの初期に基本をきっちり学んでほしい。そうでないと、正規形がわからないのでパフォーマンスを改善するための正規化崩しも安全に実施できないし、SQLやテーブル操作のスキルも深化しないからだ。
そんなわけで、第2正規形と第3正規形の違いが初心者にはわかりにくい解説記事を取り上げたい。2025年2月18日付のQiitaの記事「リレーショナルデータベース設計の完全ガイド」である。言うまでもなく書いた方を揶揄するつもりなどなく、今日では広く見られる曖昧な理解の一例として取り上げさせていただく所存である。
第3正規形の説明
説明上のわかりやすさのために、まずは記事中での第3正規形についての例を引用しよう。Aにおいて部門名と部門所在地が部門IDに推移関数従属(主キーでない項目への関数従属)しているので、A1とA2に分割される。結果的に第3正規形になる。社員IDに関数従属する項目として部門IDの他に社員名あたりもほしいところだが、説明としてはわかりやすい。
<A:正規化前の社員テーブル>
| 社員ID | 部門ID | 部門名 | 部門所在地 |
| 1 | 10 | 営業 | 東京 |
| 2 | 20 | 開発 | 大阪 |
| 3 | 10 | 営業 | 東京 |
↓↓↓
<A1:正規化された社員テーブル>
| 社員ID | 部門ID |
| 1 | 10 |
| 2 | 20 |
| 3 | 10 |
<A2:正規化された部門テーブル>
| 部門ID | 部門名 | 部門所在地 |
| 10 | 営業 | 東京 |
| 20 | 開発 | 大阪 |
| 10 | 営業 | 東京 |
第2正規形の説明:受注テーブル
続いて第2正規形の例を見よう(次表)。Bにおいて商品名は商品IDに「部分関数従属」しているので、B1とB2に分割されて第2正規形になる――これだけ説明されたら違和感はないだろうか。Aの社員テーブルの例と同型のような気がして、第2正規形と第3正規形は何が違うのだろうと、初心者であれば考えてしまいそうだ。
<B:正規化前の受注テーブル>
| 受注ID | 商品ID | 商品名 | 数量 |
| 1 | 101 | りんご | 10 |
| 2 | 102 | バナナ | 5 |
| 3 | 101 | りんご | 8 |
↓↓↓
<B1:正規化された受注テーブル>
| 受注ID | 商品ID | 数量 |
| 1 | 101 | 10 |
| 2 | 102 | 5 |
| 3 | 101 | 8 |
<B2:正規化された商品テーブル>
| 商品ID | 商品名 |
| 101 | りんご |
| 102 | バナナ |
種明かしをしよう。じつは本文中で、
このテーブル(B)では、主キーは「受注ID」と「商品ID」の組み合わせです。
と説明されているのだ。「えっ、そうなの?」という感じだが、それでようやく、Bにおいて商品名が商品IDに推移関数従属ではなく部分関数従属(主キーの一部への関数従属)していることに合点がいく。説明されなければ、{受注ID}だけが主キーと思ってしまいそうだ。紛らわしいことに、添えられているインスタンス(データの具体例)もそうであるかのように見える。
形式的には問題ないが、正規化操作の具体例としては改善の余地がある。まず、インスタンスは次表のようであってほしい。受注IDや商品IDだけでは値が重複するが、両者の組み合わせでは重複しない。これで主キーが{受注ID+商品ID}であることが、インスタンスからも読み取れる。また、主キーが何であるかはDB設計において決定的に重要なので、主キー項目をこのように強調表示してほしい。
<B:正規化前の受注テーブル(インスタンスを改変)>
| 受注ID | 商品ID | 商品名 | 数量 |
| 1 | 101 | りんご | 10 |
| 1 | 102 | バナナ | 5 |
| 2 | 101 | りんご | 8 |
| 2 | 102 | バナナ | 5 |
Bの主キーが{受注ID+商品ID}であるならば、{受注ID}を単独主キーとする「受注見出し」のようなテーブルが別途存在することが予想できる。そのテーブルがあるからこそ、主キーが{受注ID+商品ID}であるような「受注(受注明細)テーブル」の位置づけが明確になる。
<受注見出しテーブル>
| 受注ID | 顧客ID | 受注日 |
| 1 | 123 | 7月10日 |
| 2 | 256 | 7月25日 |
さてここで、経験豊かな開発者であれば、受注明細の主キーを{受注ID+商品ID}とすることに危うさを覚えるのではないだろうか。次表のように、同一商品向けに希望納期のような条件が異なる受注が同時発生する可能性があるからだ。受注データが顧客の需要を忠実に写し取ったものと考えれば、受注明細の主キーは{受注ID+商品ID}ではなく、この表のように{受注ID+行番}とするのが無難だし、一般的である。別の言い方をすると、ことさらに受注IDと商品IDとを複合させて、1回の受注において商品の重複を排除しようとがんばることに意義があるとはまるで思えないのだ。
<主キーに「商品ID」の代わりに「行番」を組み込んだ受注明細テーブル>
| 受注ID | 行番 | 商品ID | 希望納期 | 数量 |
| 1 | 1 | 101 | 7月21日 | 10 |
| 1 | 2 | 102 | 7月21日 | 5 |
| 1 | 3 | 102 | 8月1日 | 5 |
| 2 | 1 | 101 | 8月5日 | 8 |
| 2 | 2 | 102 | 8月15日 | 5 |
そこで、受注明細としてより自然な{受注ID+行番}を主キーとするテーブルを用いて、部分関数従属の排除、すなわち第2正規化の事例としてみよう(次表)。Cにおいて顧客IDと受注日は、主キーの一部である{受注ID}に部分関数従属している。そこでCをC1とC2に分割することで第2正規形となる。
<C:正規化前の受注テーブル>
| 受注ID | 行番 | 顧客ID | 受注日 | 商品ID | 希望納期 | 数量 |
| 1 | 1 | 123 | 7月10日 | 101 | 7月21日 | 10 |
| 1 | 2 | 123 | 7月10日 | 102 | 7月21日 | 5 |
| 1 | 3 | 123 | 7月10日 | 102 | 8月1日 | 5 |
| 2 | 1 | 256 | 7月25日 | 101 | 8月5日 | 8 |
| 2 | 2 | 256 | 7月25日 | 102 | 8月15日 | 5 |
↓↓↓
<C1:正規化された受注見出しテーブル>
| 受注ID | 顧客ID | 受注日 |
| 1 | 123 | 7月10日 |
| 2 | 256 | 7月25日 |
<C2:正規化された受注明細テーブル>
| 受注ID | 行番 | 商品ID | 希望納期 | 数量 |
| 1 | 1 | 101 | 7月21日 | 10 |
| 1 | 2 | 102 | 7月21日 | 5 |
| 1 | 3 | 102 | 8月1日 | 5 |
| 2 | 1 | 101 | 8月5日 | 8 |
| 2 | 2 | 102 | 8月15日 | 5 |
形式的には問題ないが、事例としては不自然だ。受注テーブルの主キーが{受注ID+行番}であるからには、上述したように{受注ID}を単独主キーとする受注見出しテーブルが最初から想定されているように見えるからだ。ようするに。行番(*1)を主キーに含むトランザクション系のテーブルは、第2正規形の説明に向いていない。
第2正規形の説明:在庫テーブル
向いている例を挙げよう。次表は典型的な在庫テーブルである(商品テーブルが別途存在するはずだが省略)。物品販売において、扱い商品が増えたり事業エリアを拡大する過程で、倉庫が1個では済まなくなる。これを見越して、在庫テーブルには{商品ID+倉庫ID}の複合主キーを付与しておいたほうがいい。ところがこのテーブルでは、倉庫名と倉庫所在地が{商品ID+倉庫ID}ではなく{倉庫ID}に部分関数従属してしまっている。それらを別テーブル(倉庫テーブル)の属性項目として切り出すことによって、第2正規形が実現される。
<D:正規化前の在庫テーブル>
| 商品ID | 倉庫ID | 倉庫名 | 倉庫所在地 | 発注点 | 在庫数 |
| 101 | 10 | 大阪倉庫 | 大阪 | 5 | 10 |
| 101 | 20 | 横浜倉庫 | 横浜 | 5 | 5 |
| 102 | 10 | 大阪倉庫 | 大阪 | 0 | 20 |
| 102 | 20 | 横浜倉庫 | 横浜 | 10 | 30 |
↓↓↓
<D1:正規化された在庫テーブル>
| 商品ID | 倉庫ID | 発注点 | 在庫数 |
| 101 | 10 | 5 | 10 |
| 101 | 20 | 5 | 5 |
| 102 | 10 | 0 | 20 |
| 102 | 20 | 10 | 30 |
<D2:正規化された倉庫テーブル>
| 倉庫ID | 倉庫名 | 倉庫所在地 |
| 10 | 大阪倉庫 | 大阪 |
| 20 | 横浜倉庫 | 横浜 |
ここまでの説明で、第2正規形が「複合主キー」にもとづく議論であることが理解できたと思う。この点が強く意識されていないと、部分関数従属と推移関数従属のような素朴なレベルで正規化がわからなくなる。それほどに複合主キーの考え方は重要だし、論理要件としても日常的に登場する。
「複合主キー」を避けてはいけない
このように説明すると、「でも、複合主キーの要件って変化するじゃないですか。変化に振り回されないために、主キーは{id}にしておくのが無難ではないでしょうか」と反論されることがある。主キーとは別のユニーク制約を必要に応じて組み込めばいい、という主張だ。
私にはそれは「ビルが傾いたらつっかえ棒をすればいい」としか聞こえない。たしかに当初の要件が変化することはあり得るが、「複合主キーの要件が変化した」と説明されるケースのほとんどが、単に主キー設計に関する洞察や知見が足りなかったためではないだろうか。また、つっかえ棒の理由を「設計の失敗」でなく「地球の重力が強くなったため」などと外部条件の変化に帰する限り、プライドは守られるが技術者としては成長できない。自分の未熟さが身に染みるからこそ、より学ぼうという気にもなる。ようするに、単独主キー主義では「きちんと失敗できない」ので成長できなくなる。
また、後付けで「つっかえ棒」を入れるにしても、アプリやデータの修正に想像以上のコストがかかることも知っておいてほしい。たいしてかからないように思えるとしたら、つっかえ棒の組み込みに伴う論理関係の精妙な変化に気づいていないだけのことだ。じっさい単独主キー主義ではデータの不整合が障害として顕在化しにくいため、「どこがどうとは言えないが、なんとなくデータがおかしい」という最悪な状況を招く。こうなるともう対処不能だし、失敗から学ぶこともできない。
「主キーの軽視」という悪癖
では、けっきょく何が問題だったのか。今回取り上げた記事を書かれた方が、単独主キー主義の発想で開発されているかどうかはわからないが、例示されるテーブルレイアウト上で何が主キーであるかが示されていない点が気になる。説明文にはあるが、テーブル上では視覚的に強調されていない。
些末な指摘のように思えるかもしれないが、これは単独主キー主義がもたらす象徴的な悪癖といっていい。なにしろ全テーブルの主キーが{id}なので、いちいち主キーを意識する必要がない。結果的に、主キー設定の重大さや難しさが実感できなくなって、主キーそのものを軽視するようになる。
似たような話で、概念データモデルと呼ばれる図面があるのだが、私はそれを作成することに反対している。「論理データモデル」では主キーが明示されるが、「概念データモデル」では主キーや属性項目まで省略されてしまうからだ。主キーはテーブルのレゾンデートル(存在理由)である。主キーのない概念モデルを作ったからといって論理モデルを作りやすくなるわけでもない。多くの場合、低スキルのベンダーが要件定義支援などと称して工数を稼ぐための高価なポエムでしかない(参考記事)。
重要なので繰り返そう。IT技術者はDB設計に取り組む前に集合論の基礎(関数従属性と定義域制約)と正規化手順を学んで、主キー設定の切実さを会得してほしい。これは経理担当者にとっての「貸借一致の原則」くらいの体感的常識である。ちなみに企業システムに関わるIT技術者にとっても簿記は常識なので、それも学んでほしい。簿記のデータモデルも書けないままで企業システム設計を担うのは、航空力学を知らないままで飛行機を設計するような無謀だ。
*1.在庫は「商品と倉庫を親とする子テーブル」であるが、受注明細は「受注見出しだけを親とする子テーブル」である。受注明細の主キーの一部である「行番」を主キーとするテーブルが存在しないのはなぜか。それは、「明細データの発生順序」を表す「行番」に関数従属する項目がたまたま存在せず、かつその定義域が「整数」としてRDMS上でたまたま規定できるからだ。「片親の子テーブル」をもたらすとしても、行番はデータ項目として特別なものではない。