2017.05.23

単独主キーでもDB設計は楽にならない

 我ながらしつこいが、どんな開発環境においても「id」のような単独主キーを用いる設計手法の危険性について、もう一度指摘したい。この手法を「単独主キー主義」と呼んでおくが、これに関して「複合主キーを用いると主キーが変化しやすいから」とか「単独主キーのほうが仕様変更に対応しやすい」といった擁護意見がある。それらの主張が無効であるばかりか有害でさえあることを説明しよう。

 なお、私はこの議論で「だからナチュラルキーが正しい」と言おうとしているのではない。じっさいのところ私はナチュラルキーを主キーにすることに反対しているが、それは本稿の主張と矛盾しない(参考記事)。ここで私が言いたいのは、複合主キーを用いたオーソドックスな設計手法を身につけたうえで、利用する開発環境の制約にもとづいて慎重に正規化崩しできるようになろう、ということだ。

■サンプルのデータ要件

 複合主キーを要求するデータ要件として、前の記事で挙げた素朴な例をここでも用いることにする。仕入先と商品と契約開始年月の組み合わせに関数従属する契約仕入単価の例である。関数式と制約をモデル([…]はテーブル名、{…}は主キーを表す)とともに示すと次のようになる。

 F(仕入先id、商品id、開始年月)= 契約仕入単価
 ユニーク制約:仕入先idと商品idと開始年月で値が重複してはいけない
 更新不可制約:仕入先idと商品idと開始年月の値が変化してはいけない

 オーソドックスなモデル:
 [商品仕入契約]{仕入先id、商品id、開始年月}、契約仕入単価

 「ユニーク制約」がなぜ必要かというと、上記の関数従属性が成り立つことを保証するためには、識別子(仕入先id,商品id,開始年月)の値に対応する従属子(契約仕入単価)の値が1個に定まらなければいけないからだ。テーブル中に変数の組み合わせが重複して存在することを許せば、そのことが保証できなくなる。

 「更新不可制約」の必要性についてはどうか。識別子は他テーブルとの関係を維持するための基礎となるものなので、レコード上のその値が変化すれば、他テーブル上のレコードとの既存の対応関係が失われる。ゆえに、レコードが追加されてから削除されるまで、識別子の値が変更されることは禁止されなければいけない。

■複合主キーを用いた場合の変遷

 さて、このデータ要件が最初から上記の「オーソドックスなモデル」の形で仕様化されるとは限らない。たとえば次のような経緯をたどったりする。

(1)[商品仕入契約]{仕入先id、商品id}、契約仕入単価
   ↓
(2)[商品仕入契約]{仕入先id、商品id}、契約仕入単価、開始年月
   ↓
(3)[商品仕入契約]{仕入先id、商品id、開始年月}、契約仕入単価

 まず最初に、「仕入先と商品毎に仕入単価が決まります」というユーザの発言を字義通りに受け取って(1)のように実装される。カットオーバー後に、開始年月(契約の発効年月)を管理する必要があることがわかって、(2)のように修正される。さらにその後、開始年月が主キーに含まれるべきであることがわかって(3)のように修正される。想像してもらえばわかるように、(2)→(3)では主キーの作り変えが起こるので、多大な修正コストがかかる。

 ではこれを「複合主キーを用いたばかりに、主キーが変化してひどい目にあった事例」とみなしてよいものだろうか。少なくともこの例では、データ要件が変化したわけではなく、設計者の洞察やスキルが足りなかっただけなのである。契約仕入の一般的なあり方に通じていれば、いくらユーザが「仕入先と商品毎に仕入単価が決まります」と言おうが(*1)、最初の時点で(3)にたどりつける。

 「複合主キーを用いたばかりに、主キーが変化してひどい目にあった事例」の多くは、このような「(a)データ要件に対する洞察が甘かった事例」か、「(b)変化に影響されにくい設計上の工夫が足りなかった事例」に分類できるのではないだろうか。もちろん、「(c)データの管理方針として関数従属性が変化した事例」も起こり得るが、それほど頻度が多いわけではない。

■単独主キーを用いた場合の変遷

 百歩譲って(c)の事例が多いとして、だからといって単独主キーだけを使って設計することで対処しやすくなるかというと、残念ながらそうはいかない。むしろ、混迷の度合いが深まる。単独主キーを用いた場合、たとえば以下のような経過をたどる(<…>は主キー以外のユニーク制約を示す)。(※ユニーク制約を(1),(2),(3)と合わせたほうがよいと指摘されたので(4)と(5)を修正した。おかげで対比がより明確になった)

(4)[商品仕入契約]{id}、契約仕入単価、<仕入先id、商品id>
   ↓
(5)[商品仕入契約]{id}、契約仕入単価、開始年月、<仕入先id、商品id>
   ↓
(6)[商品仕入契約]{id}、契約仕入単価、<仕入先id、商品id、開始年月>

 (4)→(5)の変化は上掲の(1)→(2)に対応するもので、属性項目の追加だけなので(1)→(2)と同様にたいしたコストはかからない。では、(2)→(3)に対応する(5)→(6)はどうだろう。主キーは変わらないので、(2)→(3)よりもコストはかからないように思えるかもしれない。しかし現実は甘くない。

 すなわち、(5)→(6)でユニーク制約を変更する際に、その制約に違反する既存レコードをなんとかしなければいけない。同時に、そのテーブルを参照する他テーブルがあれば、該当レコードとの関係を調整しないとデータの整合性が失われる。そのためのコストは(2)→(3)の場合とまったく変わらない。むしろ、不整合を機械的に検出できないという点でやっかいで、対応関係を個々に調べて丹念に評価しなければならない。余計な項目(id)を扱う羽目になるという意味でも面倒だ。

■データの不整合が「ユーザ側の問題」にされる

 より深刻な問題は、(5)→(6)の仕様変更とそれにともなうデータ移行の必要性が、設計者自身によって気づかれにくい点だ。単独主キー主義にこだわれば、そもそも単独主キー以外のユニーク制約を考慮する習慣が身につかないつきにくい。なぜなら、かれにとってすべてのテーブル上のレコードは「永続化されたオブジェクト」であって、例外なく単独主キー(オブジェクトID)で識別可能なものだからだ。(4),(5)、(6)に付加されているユニーク制約を含め、せいぜいその必要性を第三者に指摘されて、「あ、必要なのね。では付与しましょうか」と受動的に反応するしかない。

 第三者の指摘であっても、それに反応して仕様変更してくれるのであればまだ良い。重複レコード(矛盾レコード)が見つかっても、設計者はそれを「設計上の問題」ではなく「運用上の問題」とみなしてしまうかもしれない。そのような変なデータを入力したユーザが悪い、というわけだ。

 さらに、それらの項目には「更新不可制約」も付与されるべきなのだが、その必要性が認識されないままに項目値が変更されれば、広域のデータ不整合が生じる(*2)。ところが、これがまた大変わかりにくい不整合なので、「なんとなく変なデータがあるような気がする」といった印象をユーザに与える程度で、その対応もユーザまかせにされてしまいそうだ。

 ユニーク制約を構成する項目の値を更新するようなアプリなんてわざわざ作らないから大丈夫、という意見があるかもしれない。だが問題は、そのようなアプリを作らないことが「開発環境やDBによって保証されているわけではない」という点だ(*3)。

 すなわち、カットオーバー当初はそこらへんが周知されているとしても、保守フェーズにおいて、要員が交代するだけでなく、さまざまな環境を介してアプリが作られ始める。その過程でユーザが「複社購買になったので、仕入先を切り替えられるようにしたい」などと言い始めたとしたらどうか(頓珍漢な提案だが、このような言い方をユーザはふつうにするものだ)。既存レコード上の仕入先を変更することをがデータ管理上許されないことを、保守担当者はユーザに対して論理的に説明できなければいけない。そうでなければ、かれは仕入先が変更可能であるようなアプリをむざむざと用意するだろう。

 それでデータの整合性が崩れた場合、誰の責任になるのだろう。本来はユーザの責任ではなく、そのような提案を受け入れた保守担当者(設計者)の責任である。しかも、繰り返すが、その結果として起こる不整合は非常にわかりにくく、一見すると何も問題が起こっていないように見える。かくして整合性がゆっくり失われ、その実態も原因もよくわからないという最悪のデータ状況に陥る。

 以上をもって「単独主キーのほうが仕様変更に強い。複合主キーでは仕様変更の影響が深刻化する」などと天真爛漫に語っていいものだろうか。DBレベルの仕様調整の必要性を設計者が気づかずに、発生する不整合の責任を誰かに押し付けただけの話ではないのか。しかも悪いことに、設計上の失敗が失敗として認識されず、設計者に対するフィードバックが起こらない。そのDBを最初に設計した技術者は、今日も別のプロジェクトで同じようにハタ迷惑な設計を生み出しつつ「何事もシンプルイズベストさ。じっさい単独主キーで問題が生じたことはないよ。なんで複合主キーにこだわるかね」と自分の仕事に満足して美味いビールを飲むのだろうか。

■適切に物理設計できれば大丈夫なのか

 「必要なユニーク制約も更新不可制約も考慮しているから、単独主キーだけでも問題はない」と考える方がおられるかもしれない。そのやり方を「単独主キー専用環境」で実施するのであれば問題はない。私自身、単独主キー専用環境をつかう羽目になったら、議論の余地なくそのようにするだろう。しかし「すべての環境」でそのやり方にこだわるとしたら、どう考えてもおかしい。

 なぜか。必要なユニーク制約や更新不可制約を考慮できているのであれば、少なくとも論理設計の段階では複合主キーを含めた形で構想しているということだ。そのうえで、単独主キーに差し替えた形の物理設計を用意するというのは、実装環境の制約があるゆえの「本来ならばやらなくていい手順」なのである。その必要もないのに、いちいち単独主キーを組み込んで物理設計を別途作るというのは、いたずらに仕様情報の管理コストを増やすだけでメリットは何もない。複合主キーを許容する環境を使えるのであれば、論理設計どおりに実装すればよろしい。けっきょく、「論理設計(理想)=物理設計(現実)」であることが理想なのである。なんと当たり前の話だろう。

 ようするに、単独主キーを活用してもDB設計の難しさは緩和されないということだ。むしろ、単独主キーを使うことで問題が見えにくくなる分、より高度なスキルと慎重さが求められる。とどのつまり、「単独主キー専用環境」だろうと「複合主キー許容環境」だろうと、「(複合主キーを要請し得る)関数従属性」にもとづく正規化と正規化崩しのオーソドックスな設計スキルが求められるということだ。「単独主キー専用環境」は、神経質に使う限り良いものなのだろう。しかし、用いる環境に関わらず単独主キーを置くことにこだわる「単独主キー主義」は混乱しかもたらさない。


★イベントのお知らせ
前回記事の公開直後に、ここらへんの問題を議論するために、IT勉強宴会の緊急イベントが大阪で開催されました。たいへん有意義だったので、東京でもIT勉強宴会と「超高速開発コミュニティ/モデリング分科会」の共同主催で6月28日(水)の夜に「単独主キー主義の是非を問う」を開催することになりました。「我こそは単独主キー主義者なり」という方もふるってご参加ください。対立を煽るのではなく、違いを理解して互いの開発スキルを向上できる場にしたいと思います。


*1.主キーに含まれる項目の値が変更されることを抑止することは、本来であればDBMSの基本機能として提供されるべきだが、そのような「お行儀の良いDBMS」は現状では例外的である。したがって更新不可制約については、開発環境が提供する機能として期待されざるを得ない(関連記事)。
*2.これはユーザが嘘をついたとか不正確なことを言ったということではない。年月で契約仕入が切り替わることは彼らにとって「言わずもがな」のことであるに過ぎない。そして、言わずもがなの事情がDB構造に重大な影響を及ぼすことを、彼らは想像できない。これは、ユーザの語りは重要な手がかりだが、それらを字義どおりに鵜呑みにすべきではないということでもある。だからこそ、DB設計を担当する者には広範囲の業務知識が求められる。「ユーザがこうしてくれと言ったからこうした」の言い訳は通用しない。
*3.たとえば「仕入先id」なり「商品id」なり「開始年月」の値が更新された場合、そのレコードの主キー(id)を参照している別テーブルのレコードとの関係がどうなるかを想像してみてほしい。両者の間には不整合が起こっていると考えられるが、その事実を認識しづらい。一見すると何も障害が生じているようには見えないため、問題は対処されずに潜航して慢性化する。その責任は設計者にある。

| | コメント (5) | トラックバック (0)

«「モデリング&プロトタイピング手法」事例紹介