[DBデザイン#28] 素なデータを見つける:第二正規形を理解する

このシリーズの記事は、最初はわかりやすい話が多いのですが、だんだん難しくなってきています。Facebookのいいねが次第に減り、Twitterのリツイートする顔ぶれが変わってきていますが、頑張って続けたいと思います。

さて、前回のキーフィールドの話をしましたが、キーフィールドはnullを取らない前提があるという話を書いておくべきでした。nullは何と比較してもfalseになるので、照合できないから、キーフィールドの値に使えないのです。nullを許すと、照合出来ない、すなわち検索が出来ないレコードが発生するので、キーフィールドによってレコードを特定するという前提が崩れるのです。ちなみに、MySQLだと、論理式の値にNULLがあれば、結果は必ずNULLになります。「SELECT NULL=NULL;」は、NULLを返し、結果的にfalseとみなします。PHPだと通常はnullかどうかを確認するのはis_null関数を使いますが、null == nullの結果は1つまりtrueになります。SQLと言語ではnullの考え方に違いがあります。

前回に関数従属性という考え方を紹介しましたが、ここで、さらに厳密な「完全関数従属」という考え方をまず説明します。関数従属は、あるフィールドの値が、別のフィールドの値により決まるという考え方でした。この時、元になるフィールドの集合の真部分集合をキーフィールドとして関数従属性が成り立っていないような場合を、完全関数従属していると表現します。ざっくり言えば、フィールド同士の関連の中に、異質な関連が混ざっているような感じでしょうか? そして、第二正規形は、第一正規形が成り立つと同時に、キーフィールドに含まれていないフィールドが、それぞれの候補キーに対して完全関数従属している場合のことです。

ややこしいですね。具体例で細かく見ていきましょう。以下は前回の登場した販売明細の表です。赤いボックスは主キーフィールドを意味しています。

これだけのフィールドしかない場合は、候補キー、主キーとも、{販売日, 販売先顧客名, 商品名, 個数} の4つのフィールドを含むものしかありません。よってこの1つの集合に関して完全関数従属しているかどうかを調べるのですが、ここで、FD: {商品名} → {単価} という関数従属があるとします。つまり、商品が決まると、単価が確定すると考えることにします。この辺り、前提条件次第で結果が変わりますので、前提条件をきちんと把握してください。

新たに認識した関数従属のキーフィールドである{商品名}は、候補キーとなっている{販売日, 販売先顧客名, 商品名, 個数}の真部分集合です。完全従属性の定義を検討すると、『元になるフィールドの集合の真部分集合をキーフィールドとして関数従属性が成り立って』います。完全関数従属はそれが否定されないといけないので、完全関数従属していません。また、候補キーに含まれていない{単価}が決定されるという関数従属なので、第二正規形の定義が崩れます。よって、この表は第二正規形を満たしていません。

まず、第二正規形を満たすにはどうすれば良いかということを考えますが、結論は表を分離します。キーフィールドの真部分集合の関数従属が表の中に登場しないように表を分離します。つまり、単価を切り離せば良いということになります。ここで、表は分離して、後から結合できるという事実があり、この辺りは数学で記述するのはかなり難しい話になりますが、このシリーズのだいぶん前に実例で感覚的に説明をしているので、これは既知の事実だとします。つまり、以下のようになります。新たに出てきた商品の表の候補キー、主キーはいずれも{商品名}であるとします。

販売明細は、候補キー、主キーとも、{販売日, 販売先顧客名, 商品名, 個数} の4つのフィールドの集合になります。ただし、そのどの真部分集合を考えても、関数従属性は存在しません。よって、単価を分離し、商品名で照合して元のデータが得られるという状況にすることで完全関数従属性が成り立ち、第二正規形を満たすことになります。

要するにマスターを分離することか?と言われればそうです。ただ、この段階で、全てのマスターが分離できるわけではありません。さらに別の正規化のルールも必要になります。

前回、同じような表で以下のようなものも提示しました。こちらは「販売明細ID」フィールドがあります。ここで、この表の前提を少し拡張します。前回のキーフィールドの議論では、販売日〜個数のフィールドには重複するレコードがないという前提を引き継ぎましたが、それをやめてみましょう。良し悪しの問題はちょっと目を瞑って、番号を振ったのだから、区別できるでしょうという大雑把な設計方針が導入されたと思ってください。{販売明細ID}を候補キー、主キーとして、{販売日, 販売先顧客名, 商品名, 個数}は、もはや候補キーではないと前提を変えてみます。

単一のフィールドが候補キーであり主キーであるような状況は意外にシンプルです。ここで、関数従属性はどうなるかというと、例えば、販売明細IDが1003だと、販売先はAmazen、商品名はロボットさんま1号など、残りのフィールド全て、1つのフィールド販売明細IDが決まれば決まっているということになります。つまり、関数従属としては、FD: {販売明細ID} → {販売日}、FD: {販売明細ID} → {販売先顧客名}、FD: {販売明細ID} → {商品名}、FD: {販売明細ID} → {単価}、FD: {販売明細ID} → {個数}、といった関係があるとみなされます。これも厳密に記述すると、FD: {販売明細ID} → {販売日, 販売先顧客名, 商品名, 単価, 個数} ということになり、矢印の右側の集合の部分集合を持つ関数従属性が大量に存在するということになります。つまり、販売明細ID単独で、レコードの特定が可能となっているということです。

であれば、候補キーの真部分集合はありますか? 単一の集合の真部分集合は空集合を除いてあり得ません。空集合は実はnullなので、これはキーとしては排除するのが定義でした。ということで、真部分集合の関数従属は存在しません。もう一度、完全関数従属の定義に立ち返ると『元になるフィールドの集合の真部分集合をキーフィールドとして関数従属性が成り立っていない』であり、まさにその通りなので、実はこの販売明細IDを主キーにした表は、すでに第二正規形が成り立っています。なお、商品が決まれば単価が決まるという事実は存在していて、FD: {商品名} → {単価} という関数従属性はあるとしても、こちらの表では{商品名}という集合は、候補キーの集合{販売明細ID}の新部分集合ではないので、完全従属性は保持されています。不思議ですが、ここで、{販売日, 販売先顧客名, 商品名, 個数}は主キーではなく、それらが重複するようなレコードの存在を肯定したという状況の変化があったので、実は表としての性質が変わってしまっているということです。

ふと思うのは、Excelの表って、最初から行番号が振られているので、第一正規形をクリアすれば、行番号を主キーと見做せば自動的に第二正規形も成り立つってことになりますね。

あれ?じゃあ、マスターって分離できないのか?と思われるかと思いますが、後半に示した表から商品マスターが取り出されるのは、実は第三正規形を満たすためのテーブル分離で実現されます。次回は、なぜ正規化が必要なのかという議論に必要な更新時異常という考え方を紹介し、さらにその後に第三正規形の説明をしようと思います。

[DBデザイン#27] 素なデータを見つける:キーを考慮する理由

前回は、データはラベルか測定値だろうという話をしましたが、これはあるセミナーでここで説明しているような設計の話をしたとき、受講された方の一番の疑問だったらしく、細かく説明できなかったところなのです。こうしてブログに書く機会ができて個人的にはなんとなく一段落です。今回から、データの集まりである表についての話です。フィールドには名前が付いているので、その名前を手がかりにデータを取り出せますが、レコードにはそうした識別のための材料が、あるのかないのか?そこがポイントです。言い換えれば、レコードを特定する、あるいは一定の条件のレコードに絞り込むなどのいわゆる検索ができる状態の表でなければなりません。今回はとりあえず、架空の会社の例で出てきたこの表からスタートしましょう。営業部門が販売した明細を記録しているという想定です。

もちろん、いろんな意味で省略は多いのですが、この表では5つのフィールドがあってそれでこの会社の販売データが記録されているとします。今、たまたま、5つのフィールドが全て同一のレコードは存在しませんが、さらに3行目と同じレコードが6行目に追加されるという状況はあるでしょうか? あるいかないかは、結果的にその会社の状況によると思います。つまり、前提条件次第で変わります。ロボットさんま1号が非常によく売れて、Amazonは11月11日に何回も注文したとしましょう。個数は毎回違うかもしれませんが、同じかもしれません。個数まで同じなら、行の区別がつかなくなります。ちなみに、そういう状況にしますか? しないでしょう。それを手入力しているとしたら、完全に同じレコードが発生する状況で、「さっきの注文入れたっけ? まだだっけ?」という確認ができません。もちろん、その日の発注を頭からチェックするということもあるかもしれませんが、かえって効率を落とします。現実にはどうしましょう? 多分、販売日と同じかあるいは「入力日時」などのタイムスタンプのフィールドを増やすなど、同じ内容のレコードが発生しないような工夫をすると思います。ID番号を登場させるのは反則かもしれませんが、例えば、納品書IDがあれば、おそらくは区別できるでしょう。ただ、これも、同一の納品書で同じ商品に対する明細は2つ以上存在する事はない、つまり、単一の納品書では、明細には1つの商品は1回登場するか、全く登場しないかのいずれかになるという前提もあります。

まず、こうした表では完全に重複したレコードがないことを前提にします。その結果、表はレコードの集合であると考えることができるわけです。集合は重複した要素を持たないというのは定義です。ただ、センサー記録などでは、タイムスタンプを落としたとしたらそういうこともあるかもしれませんが、これは、センサー入力が重複しないことを前提としているからです。つまり、データ収集が重複がないということがほぼ保証できるような自動化プログラムで行われているような場合には、後から集計した結果が重要であり、元データに重複はあるかもしれませんが、現実は正しく記録されているとも言えます。今回はそのような状況は特殊事例として除外してください。もっとも、そのような場合でもタイムスタンプなどを記録することで完全な重複がないようにするのはおそらく一般的であると思います。

ここで「関数従属性」という考え方を導入します。あるフィールドXの値が決まると、別のフィールドYの値が決まるという考え方です。Wikipediaに倣って、「FD: X → Y 」と記述しましょう。X、Yはいずれも属性の集合です。前の販売明細の表で言えば、商品名に対する単価は、商品名が決まれば単価がどうやら確定するようなので、「FD1: {商品名} → {単価}」とみなします。あるフィールドの値が別のフィールドの値で決まってしまうという特徴が、表全体に渡って成り立つかどうかを検討します。では、販売先顧客名と単価はどうでしょう? たまたまある顧客が同一の商品しか注文しないと表の中では関数従属性があるように見えるかもしれませんが、顧客は複数の商品から選択するという前提があれば、単価は顧客では決まらず商品によってのみ決まるので、一般には関数従属性はないとみなすのが自然です。「関数従属性」と言うと名前が難しそうで、かつ数学的に記述するとさらに難しそうですが、「現実はどうなっている」ということが前提にある概念です。関数だから、その内部の計算方法は?などと疑問に持つかもしれませんが、ここでの関数は「入力を与えれば値が返る」という意味の抽象的な関数で、一般にはプログラムや数式では書けなくはないものの完全には書きにくいものです。データの集合がそういう性質を持つという議論です。

この関数従属性を手がかりにして、「レコードを一意に特定できるフィールドの集合」を考えます。この「レコードを」というところがまずポイントで、この性質を見極めておかないと、実用上支障が出そうです。そして、「一意に」というのも重要です。何か特定のデータに特定できるということです。販売明細は5つのフィールドがあるので、つまりはそれら全てのフィールドを決定可能なフィールドの集合Xとして、「FD2: X → {販売日, 販売先顧客, 商品名, 単価, 個数}」を満たすXを認識する必要があります。この概念を一般に「キー」と呼びます。例えば、Xには矢印の先そのもの、つまり全部のフィールドは多分含まれるでしょう。つまり、Xの1つは「A1 = {販売日, 販売先顧客, 商品名, 単価, 個数}」になります。

販売日がこれ、販売先顧客はこれ、商品名はこれ、単価はこれ、個数はこれ、と列挙すれば、その値を持つレコードを探してきて1つのレコードが特定できると言うことです。販売日がなかったらどうでしょう。同一顧客に同一商品を同一個数、別の日に販売した場合、どっちのレコードなのかが区別がつかなくなり、キーの役割を果たしません。ここですでにFD1が見つかっているとすれば、単価は商品名から求められるため、キーの構成要素としてなくてもいいということになります。つまり、単価を知らなくても、どのレコードかは特定可能なので、「FD2: {販売日, 販売先顧客, 商品名, 個数} → {販売日, 販売先顧客, 商品名, 単価, 個数}」となり、「A2 = {販売日, 販売先顧客, 商品名, 個数}」もFD2のXとして成り立ちます。

このように、キーは複数のフィールドの組み合わせも取り得ます。考えられるありとあらゆるキーを集めたものは「スーパーキー」と呼ばれます。ここまでの議論だと、{A1, A2}がスーパーキーです。その中で、余分なものがないもの、つまりなるべく要素数を少なくとしたものが「候補キー」と呼ばれます。つまり、候補キーとしては「{A2}」となります。さらに、候補キーの中で、実際にシステムの中でレコードの特定に使うものが「主キー」と呼ばれます。ここではA2しかないので、A2を主キーとすることになりますが、現実にはフィールドが増えた時などにもう少し候補キーを探すことになるでしょう。4つのフィールドで常に検索するというのはちょっと面倒ですね。

ここで、IDフィールドを販売明細に加えてみます。現実にやっていることの説明が必要ですね。「販売明細ID」フィールドを追加して、一意な数値をつけてみました。

ここで新たに、A3 = {販売明細ID}が、スーパーキー、候補キーの仲間に入りました。スーパーキーを全部挙げるのは実は大変です。A4 = {販売明細ID, 販売日}、A5 = {販売明細ID, 販売先顧客名} … などと、結果的には販売明細IDとの他のフィールドとのあらゆる組み合わせが、レコードの特定が可能です。全部列挙大会をやってもいいのですが、疲れるのでやめます。販売明細ID単体でレコードの特定が可能なので、冗長性を無視して「可能である」と言う点だけに注目すれば、そうなります。候補キーは冗長なものが全部排除されるとしたら、{A2, A3}となるでしょう。そして主キーはより単純なA3になると言うのが一般的な考え方の1つになります。それがいいかどうかは、結果的にどんなシステムを作るかと言うことと擦り合わせは必要ですが、キーの定義はこのようになります。スーパーキーに関して、一度は手で全部書くのが学習になるかもしれませんが、実用的にはスーパーキーを全部列挙するような仕様書は誰も読みません。場合によっては候補キーを挙げて、2つのうちどちらを選んだかと言う理由を記述する場合もあるかもしれませんが、設計者は候補キーが頭の中でサッと浮かび、サクッと主キーをどうするかという議論に入ります。今回の場合A3のように決まったことしか仕様書に書いてないとしても、A2のような実データフィールドの主キーはあるものの、番号振ってあって一意だからこっちでいいよね的なフィーリングは行間にしか書かれてないことが普通です。

キーの中でも3種類もあってややこしいと思われるかもしれませんが、設計上は主キーを特定できればそれでOKです。しかしながら、なぜ、主キーが存在する意味があるのかと言うことを定義するために、関数従属性という考え方、あらゆるキーから意味のあるキーの選択といったことを経て、主キーの考え方に至ります。スーパーキーのように一度「全て」を考えて絞る、ここでは冗長性のないもの、そして実用性を考えるということに分解して仕組みが成り立っているということは、結構重要なのではないでしょうか。

[DBデザイン#26] 素なデータを見つける:データをどう見るのか

前回は、データベースの基本的な考え方として、もとになる集合があって、そのどれかの要素が1つだけフィールドになるという「ドメイン」の考え方を紹介しました。そのフィールドが集まって表を形成しますが、それを「リレーション」と呼ぶというのが定義です。表そのものや、フィールドという構造がある、レコードという繰り返しがあるという諸々を含めた意味が込められている用語になります。ただ、現場的には「関係性」のこともリレーションと言ってしまうことがあり、ちょっと混乱します。本来は「リレーションシップ」なのでしょうけど、リレーションと言ってしまって通じるところもあって、自分でも時々混乱している時があり、反省する次第です。

さて、フィールドに入れるべきデータの集合そのものがどういう成り立ちをしているのかをさらに探りましょう。都道府県のように、ほぼ誰が見ても明確なドメインはありますが、実際の業務で出てくるデータには、元の集合が書ききれないもの、定義が揺らぐもの、同じものなのに同一名称など、かなり多種多様なものがあって捉えどころがないという考えになるかもしれません。あるいは、「数字」と「文字列」が一般的な言い方でしょう? という言い方もできるのですが、そうすると、なんでもありということになり、ドメインの考え方が揺らぎます。

実際にデータベースに入っているデータを仔細に見ると、結局のところ、「つけた名前」であるものと「測定値」なのではないかということを考えました。「つけた名前」はここでは「ラベル」と呼びます。英語のlabelに「名前を付ける」という意味があるからですが、「ラベル」というより発音に近いのは「レーベル」でしょうけど、日本語として通用している「ラベル」で以下は表現します。

例えば、「埼玉県」というのは、3文字の文字列ですが、この文字列そのものが、都道府県単位の1つの行政区域を示しており(法律的にはもっと細かくあるのでしょうけど)、ある意味、広い土地全体に対して名前をつけたものです。そして、日本国内は、都道府県で地域を区別できるとか、ある一地点はどれか1つの都道府県であるのかということが通常は結論づけられるなど、素なデータであることが誰が見ても意見の相違がないようなラベルなのです。47都道府県はいずれも文字列という見方もできるのですが、47種類のラベルがあるというのがさらに抽象的な見方になります。そのラベルに書いてある名前があるドメインでは「埼玉県」であるとなっているものの、ドメインが微妙に異なる、つまり状況が変われば「Saitama Pref.」となっているのかもしれません。

このように、データはラベルではないかと考えると、現実にデータベースに保存しているデータのほとんどはラベルです。文字列で記述すると、人間が普段のコミュニケーションで利用している言語に登場する形態と同一と見做せる記述が可能なので、文字列で記述することが多いでしょう。しかも、厳密にはなんらかの定義がきちんとされた上で、ラベルの集合があって、その中の1要素がフィールドに収められています。名前も、姓や名に記述される文字列は、いずれもラベルです。そう考えると、ほぼ全てのデータは、誰かがそれに対してつけた名前や、あるいは番号であって、それらのデータの成立過程を考えれば、フィールドの元になる集合というものの存在が明らかになります。商品の表にある商品名は、やはりラベルです。会社で扱っているさまざまな商品には、通常は製造した会社が付けた名前がありますが、基本的にはそれは他の商品と別の商品であって区別ができるように付けた名前、すなわちラベルなのです。「SK-110B」のような実利、すなわち商品名から商品の属性や成り立ちを示唆するような情報を込めたような実利を追求したような商品名もあれば、「ラッキースタンプ」のようなマーケティングの効果を高める意図が込められた商品名もあるでしょうけど、いずれも、ラベルであることには変わりありません。そうしたラベルを記録するのに文字列という表現形態は非常に便利であり、ほぼあらゆる形式のラベルに対応できるので、データベースとして文字列記録機能が実装されているということになります。ですが、フィールドには文字列を記録するのではなく、設計の上では、ラベルが記録されるという考え方が重要なのです。

ただ、データベースの中身には、ラベルでないものも存在します。例えば、体重や温度といった「測定値」です。測定値とは、なんらかの単位をもとにして測定した値です。これはラベルとは言い難いものです。もっとも、群論の考え方からすると、整数や有理数などは要素が無限の集合の要素ということにもなるので、結局はラベルではないかと考えられるのですが、データの成り立ちが違うと考えています。ラベルと測定値は、照合に意味があるかどうかの違いがあると考えられます。ラベルはなんらかの方法で、同一と見做せるものは同一です。つまり、ラベルとしての文字列が同一であれば、それらデータが示す現実のものは、そのラベルの定義範囲(ドメインの定義と同義)において一定であるということになります。ある会社に売った商品Aと、別の会社に売った商品Aは、同一の商品名なので、商品という区分においては同一です。ここでの同一は、以前に記載した「ある会社の営業部における商品の考え方」に基づく判定になりなす。箱が異なり、製造番号が違うなどの物理的な違いはあるかもしれませんが、「商品」というドメインでは同一と見做すということが営業部においては行っていることです。これが照合の意味するところです。ところが、測定値は同一の測定値は、それは偶然そうなっているかもしれません。正しく定義されたドメインではラベルは同一であることを保証できます。いや、むしろ、それがドメインの集合で検討した場合の定義そのものでもあります。「正しい定義」に「ラベルは重複がない」などのルールを入れれば良いのですが、これは第二、第三正規形に繋がりそうなルールです。一方、2つの測定値は等しい値であることはあっても、それらは同一であるかどうかはわからないのです。極端かもしれませんが、測定値に対象と時間軸がぶら下がるとしたら、唯一無二の存在のように思えてくるのではないでしょうか。

ラベルと言えば文字列ということになりそうですが、一方、数値であってもラベル的な存在も扱うことがあります。ある出荷伝票があって、出荷済かどうかを0と1のどちらかで示す場合があります。良し悪しはともかくそういう実装は時々行われています。このとき0が未出荷で、1が出荷済みということはどこかにドキュメント化しないと、他の人には理解できないのですが、これもドメインの定義に当然ながら含まれていると考えます(実際、ドキュメント化されてないことがほとんどだったりしますけどね)。この場合、「0」と「1」は、測定値ではなくラベルです。便宜的に数値を割り振っています。ですが、これを「133」と「59887」の2つの整数を利用しても、ドメインとしては成立します。プログラムなどの記述が気持ち悪くはなりますが、割り当てるという意味ではなんでもいいとも言えます。ちなみに、0と1にしておくことで、プログラミング言語の論理型との変換がスムーズであることを利用して、コードの単純化をするのは基本テクニックですので、0/1の数値置き換えはエンジニアサイドでは当たり前のことでもあります。数値、特に整数は、同一かどうかの判定が非常に安定しています。文字列だと、全角と半角の問題など、ドメイン内では同一と見做せる文字列をコード体系の問題で異なる文字列で示すことができるので、もちろん正しく配慮すれば問題は発生しないのですが、そうした配慮が必要という点では安定性が整数に比べて減少するという考え方ができます。途中に出てきたIDの値を番号にすることが一般的ですが(最近はUUIDも使われますが)、その理由の1つはこうした照合判定の安定性があります。ちなみに、もう1つの大きな理由としては、照合のスピードが文字列よりも一般には早いということがあります。

いずれにしても、ラベルとなる数値は、一般には計算できません。1が出荷済みということで、1+1=2に何か意味があるかというと、2回出荷した? それはちょっと違いますね。出荷はするかしないかのどちらかです。「出荷済み+出荷済み」という演算はこのドメインでは定義されていません。よって、1+1という計算はできるのですが、その計算自体は意味がないのです。こうした数値を「名義尺度」と呼ばれます。

なお、数値には名義尺度以外に、「順序尺度」「間隔尺度」「比例尺度」といった区分があります。「順序尺度」はその大小が順序を決定することが可能なもので、例としては、アンケートでの5段階評価つまり、大変悪い・悪い・どちらでもない・良い・大変良い、に対して、1〜5の整数を割り振ったものです。ちなみに、こういうアンケートの数値の平均を出すのを平気でやる方も結構いらっしゃるようですが、基本は、それぞれの選択肢のヒストグラムを比較検討するのが基本です。どちらでもないと良いの差は1で、良いと大変良いの差も1ですが、本当にこれが等距離かどうかというのは通常は分かりません。なぜなら人間の感覚だからで、厳密で測ったものではないことが一般的で、個人差もあるからです。ただ、平均値同士の違いをざっくりと見るという点では若干意味があるのかもしれませんが、いずれにしても順序尺度であるということは忘れてはいけません。一方「間隔尺度」は数値の差が定量的に意味のある場合です。さらに数値としての0に意味がるような場合は「比例尺度」となります。名義尺度と順序尺度が「ラベル」で、間隔尺度と比例尺度は「測定値」であると言えそうですが、これについては反論が出そうな気もします。

ちなみに、商品IDなどのID番号は基本的には名義尺度です。ただ、システム自体が連番を振るような場合、順序尺度として利用可能ではありますが、これは副次的な作用であって、考え方は「区別する」ということが基本にあるので名義尺度になります。もちろん、ID番号は加算や減産はできたとしても結果に意味はないので、間隔尺度や比例尺度ではありません。ただ、順序尺度として使うには、番号の割り振り自体の管理が必要です。また、順序を変更することがIDの変更ということになり、これは別の回で説明する予定のキーフィールドは変更しない原則にも外れることになります。なので、通常は順序尺度とは考えません。順序をなんらかの方法で記録したい場合は、独立したフィルールドに数値を入れて管理をします。通常はUIとの連携にロジックが絡むことが多いでしょう。

いずれにしても、データベースにあるデータは、ラベルか測定値であり、業務システムではラベルの割合がかなり多くなります。そのような中でも確実に測定値なのは、単価、個数、金額といったあたりのフィールドです。いずれも0の基準は明確なので比例尺度と言えます。そして、金額が同じであれば、同一の販売であるかと考えるかというと、たまたま金額が同じだっただけかもしれないので、同一かどうかは分かりません。これが、測定値を照合には通常は使わないという実例です。

日付や時刻は実はラベルなのか、測定値なのはか難しいですが、差の演算が可能なことを考えれば間隔尺度なので、測定値であると言えます。しかし、年月日時分秒といったコンポーネントの集合のような考え方だと、ラベルのようにも見えます。例えば、年月だけを記録するような場合は、ラベル的な動きをするようなデータであることもあります。このように日付時刻は、単に記録することを超えるとかなり扱いが難しくなります。これも回を改めてですね。

データベースの中身に圧倒的に多い文字列は、ラベルです。つまり、誰かがつけた名前を使わせてもらっていて、それらが共通概念になっているということで、情報が伝達されているということです。ただ、文字列を記録しているだけではなく、ラベルを文字列として記録しているということに思いを巡らせれば、データベースの設計に必要な情報も浮かび上がるのではないかと思われます。

今回も図がないので、何か貼っておきます(笑)

[DBデザイン#25] 素なデータを見つける:第一正規形の意味

これまでに書いてきた内容の1つのまとめは、データの関連性を見つけて、複数の表に分けるということです。特に、1対多の関係を見つけて表に分割するということで、リレーショナルデータベースの設計につながることを説明しました。一方、それでは何を1レコードにするのかということも問題です。この関係を見つけるという問題と、何を1つの要素として見做すのかという問題は表裏一体のものでもあります。1レコードを見つける、そして、そのレコードの中の1つのフィールドを見つける、さらには1つの表は何かを見つけるといった問題を「素なデータを見つける」と示すことにします。

素とは、なんらかのルールで分割できないものです。有名なものは素数で、1とその数以外に、割り切れる整数がないような整数を示します。整数は、整数の掛け算で表現できます。12なら2 x 6 = 2 x 2 x 3 のようになりますが、11は、2から10までの数を考えれば、いずれも余りが出てしまうので割り切れる数はありません。ちなみに、2と3から5(つまり、11➗2 = 5…1なので)の奇数の整数でそれぞれ余りがないことを判定すれば11は素数であることは示すことが可能です。もっと一般的には、素数の列で順番にチェックするということになります。

この考え方をデータに割り当てて考えます。都道府県をともかく記録したいとします。正しくは {北海道, 青森県, 秋田県, …, 鹿児島県, 沖縄県} といった47種類の文字列のどれかが、どこかの都道府県を示しているとします。人間が都道府県を扱うと、「埼玉県」だけでなく、「埼玉」や場合によっては「さいたま」など、同一の都道府県名を示すと判断されそうなさまざまな表現が可能ですが(「さいたま市」があるのも知ってますよ、そこに住んでいるので)、ある世界(つまりはシステムの中の世界)では、都道府県の集合の要素でないものは、都道府県ではないと見做すとします。このとき、「北海道」や「埼玉県」を、素なデータと考えます。つまり、「埼」「玉」のように分割したものは存在せず、「埼玉県なる都道府県」のような余分な文字が増えたものも存在しない、あるいは「埼玉」のようになんとなくわかるとは言え、想定したデータと異なるものはないと考えます。

つまり、とり得るデータの集合を想定して、その中の1つの要素と同一のデータが素なデータと考えます。そして、表のフィールドには、素なデータが、1つだけあるか、場合によっては何もない(null=ナルと呼ぶ)かのどちらかになります。nullについては別の機会に説明しますが、要するに、「都道府県」というフィールドには、「埼玉県」というデータがあるなど、47都道府県の正式名があるということを期待し、「埼玉」や「滋賀」といった文字列はないことを期待します。この考え方はデータベースの世界では「ドメイン」と呼ばれたりしますが、つまりは、実用上のルールがフィールドに入るべきデータに宿っているのです。そういう意味で、データベースの理論はきちんと現実に起こることということを数学の理屈に組み込んでいます。ただ、数式を追うのではなく、要求をもとにしたシステムのあるべき姿を数学で記述をしているということになります。

都道府県はまだ有限なので考えやすいかもしれませんが、人間の名前はどうでしょう。姓でも名でも、都道府県の47要素の集合のような「全要素」を書き出すのはかなり難しいでしょう。また、それが全ての素な名前のデータを持った集合であるということの証明も難しいでしょう。都道府県名の場合は政府の資料等で定義はされているので、厳密に証明は可能だと思います。ただ、名前のように全要素が書き出すことは難しいものであっても、そのような全要素が仮想的にどこかに定義があって、フィールドに登場する値はそのどれかの要素であるような考え方をします。

もちろん、要素がわかっている場合、あるいは合理的に判定が可能な場合、その定義をもとにしてフィールドの値が正しいかどうかの判定もできます。よくある入力値の検査(バリデーション)はこの事実に基づいて行われているということです。

都道府県のフィールドについて、「埼玉県」はOKとして、そこに全要素からの2つ以上の要素がなんらかの方法で入力されていたとします。文字列として合成したということでもいいでしょう。例えば、「埼玉県<改行>千葉県」です。これらは全都道府県の集合から、2つの要素を持ち込んで、改行でつなげたものとなります。これを「都道府県」のフィールドに存在することは、おそらくどんなデータベースでも仕組み上は可能でしょう。文字列を記録できないデータベースはないからです。例えば、「会社」という表があって、都道府県フィールドに拠点のある都道府県を記録するとすれば、ある会社は本社は埼玉県、支社が千葉県ということもあります。

このような複数の都道府県が入っている場合、「都道府県」がドメインとすれば、重複があることで第一正規形を満たしていないとみなします。リレーショナルデータベースにおいては、第一正規形を満たす必要があるというのが根本的な考え方ですが、「記録さえできていればよく、このままでも問題ない」と判断されるのであれば、それはそれで正解な設計であるとも言えます。実は、第一正規形を満たしていないと何が悪いのかということが一般的に言えるかというと、それがかなり難しいです。よく言われている理由は「そうしないとデータベースに格納できない」ということがありますが、文字列処理を強引にやればなんでもできますし、また、FileMakerの繰り返しフィールドのような、第一正規形破りっぽく見える(実はそうだとも言えるし、そうでないとも言える)機能も、ある一定の範囲内では便利に使えます。

いずれにしても、ドメインの要素が複合的に存在する場合、少なくとも、その修正のためのアルゴリズムが複雑化しそうです。つまり、「埼玉県、千葉県」を「埼玉県、茨城県」に変えるという場合の処理が複雑になります。単一の要素だけなら「千葉を茨城に変える」というのは「茨城県」と上書きするだけです。ですが、複合的に存在する場合、現状のデータを読み出し、その中の消したい千葉県の範囲を識別して削除し、一方で、どこかに茨城県というデータを追加するという作業になります。また、「埼玉県」に拠点がある会社だけに絞るというのは、検索ロジックも複雑になります。都道府県では問題にならないかもしれませんが、ある要素は別の要素の一部分と同じというような場合(市区町村名で言えば、山陽小野田市と野田市のような関係)に一方だけを検索させるための「工夫」や「配慮」が必要になります。つまり、「野田市を含む」で検索すると、山陽小野田市も検索されそうです。市区町村が重複のないフィールドに入っていると「野田市であるデータ」と検索する、つまり完全一致で検索できるので、山陽小野田市は自ずと排除されるということになります。このように複合フィールドがあると、何かとデータ処理が面倒になりそうというのが大まかな言い方となるでしょうか。ただ、これらの問題も、頑張ってちゃんとプログラムを書けば大丈夫とも言えます。それに対して、複雑さは品質の低下を招く可能性がある!とこの辺りでほぼ喧嘩状態になりますね。

第一正規形はダメなのかどうかとういうと、その状態で、要求を満たしているのであれば、ダメとは言い難いと言っていいかと思います。むしろ、重要なのは第二、第三正規形への変換が、繰り返しのあるフィールドのデータについてはかなりやりにくくなるという点が第一正規形の意義なのではないかと思います。つまり、第一正規形は、素なデータに分解することで、テーブルの分離をスムーズに進めるというルールなのではないかと考えるのが妥当でしょう。

ちなみに、要求を満たせばなんでもいいのかというと、そこは微妙で、後々に変化する要求への対応が容易になるという点では、第一正規形への変換は必須と考えます。例えば、都道府県ごとの集計はしないと思っていても、後々したくなるかもしれません。大量のデータが集まった後に、フィールドに「埼玉県、千葉県」のような合成データがあったらどうでしょう? しばらく頭抱えるかと思います。もちろん、頑張れば対応できるのでしょうけど、同じような議論の繰り返しになります。フィールドに素なデータだけがある場合に比べて、そうした場合の対処が大きく違ってきます。リレーショナルデータベースの設計として正しいものは、その後のメンテナンス性に大きく影響するのです。このことをシステム開発の中で経験した方も多いでしょう。

今日は図がありませんでした。このままFacebookの近況に書き込むと、広告の画像が取り込まれるので、前回の図の1つを貼っておきます。

[DBデザイン#24] 関係の概念:多対多の発生を考える

前回は多対多の概念の学習ということで、元々IDを割り振った、つまりは第二、第三正規形を満たしているような状況で説明をしました。残るは第一正規形を満たすようにするということを考えれば、「中間テーブル」を持つことがあたかも自然に発生するように説明しましたが、ちょっとレールに乗り過ぎたようです。改めて、前回のような、学生の管理、履修科目の管理という視点から、「表にする」ことによって分解を進めるということがどういうふうに進むのかを考えてみましょう。

まず、「学生」という表があるとします。学校としては、入学した学生の一覧は必ず作るでしょうから、学生一覧がExcelで作られていると想定しましょう。以下の図では、氏名だけがフィールドとして存在していますが、実際には読み仮名や学科、入学年度、住所など多数のフィールドがあると思われます。今回はそれを書きませんが、氏名以外にもフィールドがあるという前提で考えてください。そして、学生課では、入学した2人の学生が(そんなに少ないわけではないのですが)、科目履修をすることになりました。申込用紙を作るなどして希望を集めた結果、「履修科目」フィールドに履修科目をカンマ区切りで描きました。これで、誰が、どの科目を、履修しているのか、という点では記録できたわけです。使いやすいかどうかは別にしてともかく記録はできていると言えるでしょう。一方、教務課では若干興味が違います。履修科目はおそらく入学前から決まっていてこの場合は3科目あり、「履修科目」表に記載しています。この表も同様に名前以外に年度、教室、担当教員などいろいろありそうですが、それがともかくぶら下がっていると考えてください。そして、学生課から教務課へ、学生が書いた履修希望の記入用紙が「そちらも必要でしょう」ということで回ってきました。こちらは科目が興味の中心なので、「履修者」というフィールドを設けて、履修する学生をカンマで区切って記載しました。ここで、2つの部署で、「履修登録用紙」を元にしたデータをそれぞれ作ってしまっています。ということで、まずは表を増やさないで、つまりは気持ちの上での「データの一元化」を果たすため、フィールドを追加してやや強引にデータを追加したとしましょう。現実の学校では、学生数や科目数を考えれば、即座に破綻しそうですが、ここでは架空の状況としてそのように考えましょう。

ここで、教務課ではふと思いつきました。「担当の先生に履修者名簿を渡さないと行けない」ので、さてどうしましょう。履修者フィールドの句点を改行に変えるとか、それをRPAでやればいいではないか!DX〜!!!とか言い出しそうですが、もう少し伝統的なExcelライクな方法もあります。以下のように、履修科目ごとにシートを作って、そこに履修者名簿を作るのです。これだと、最近までやっていましたという学校もあるかもしれません。

ここで、中央に3つある履修科目ごとの履修登録の表は、どうみても、同じフィールド構成になります。つまり、縦に繋げられるじゃないかと。だけど、それじゃあ解析学の履修登録名簿はどうやって得られるのだと突っ込まれます。ここで、ありがちな鋭い登場人物が出てきて、「それは検索すればいいのです」ということで、以下のような表にまとめることができるということに気づきます。これは、実は単独の履修科目で発生していることを、どの履修科目でも発生しているということに気づき、前の図を得るには、履修登録表で、指定した科目名のレコードだけを残せば良い、つまりは検索すれば前の表の情報が得られるように、表の構成を工夫したということです。3つある表を単に合体しただけでは元の3つの表は得られませんが、その時、各レコードに、どの表からそのレコードを持ってきたのかを記録すればいいわけです。そのことを単純に実現するためには、科目名フィールドを追加するということで、それを検索の手がかりにすれば、元の表が得られるということになります。これを素で思いつくのは本当に優秀な人なんでしょうけど、少なくとも普通の私たちは、事例を通じてこうした抽象化の1つの流れを学習し、学生と科目以外の他の関係にも適用できるようになっておきましょう。そして、その知見を元にすれば、その時々の状況に応じてデータベースの設計ができるようになります。

再掲になりますが、前回に出てきた以下の図は、上記の図を、学生、履修科目に対して、ID番号に置き換えて、それぞれIDで参照するという前提で書き直したものです。IDがあること以外は、表現している内容は同一のはずです。

改めて、学生課、教務課に立ち戻って考えてみたいのですが、このまま学年が進み、最初の2人の学生が卒業する時になって、「あ!成績表を作らないといけない」となったとします(普通はそんなことはない!)。その場合、今日の2つ目の表のように、「成績表-学生A」「成績表-学生B」というシートを作ればいいじゃないかということになりますが、3つ目の図のように、学生に関わらず構成が同じということを発見すれば、結果的に「成績表」という1つにまとめられます。そして、その「成績表」は細かいフィールド構成を考えなければ、つまり、学生と科目を結びつける存在というより抽象度の高い存在意義を考えれば、実は「履修登録」と同じになります。よって、履修登録に成績を記述することは合理的と言えるでしょう。

履修名簿も、成績表も、どちらも同じ表から得られます。それらは何を基準にして検索するかの違いしかありません。もちろん、実際にレイアウトを作る時にはいろいろ違いがあるでしょうが、データの在り方、すなわちデータベース設計においては、どちらも同じここでの「履修登録」表からの得られるということで、この表はデータベース内において存在すべき表である、そして必要十分な表と言えるのです。厳密には、要求を満たしているかを常に検討はします。元はと言えば、学生課と教務課で同じデータを作ったため、どちらから考えても、論理的には同じ「履修登録」表ができているとも言えます。

前回は、今回の最初の図のような状況において、学生の表にある履修科目フィールドを見ると、あっさり第一正規形を満たしていないからテーブル分けましょうと言いましたが、ここで第一正規形について、改めて詳しく説明しましょう。ただ、本来は集合論の話から入らないといけないのですが、そこはうまく飛ばして説明を試みます。

以下の図の左上は、最初の図にも出てきていましたが、これをもう少し拡張高く(笑)書くと、要素に集合があるという左下の書き方や、表の要素が表であるという右側のような書き方になります。いずれも履修科目フィールドには、合成あるいは繰り返したデータがあるという見方をします。第一正規形はこうした合成や繰り返しが存在しない状態になっているということです。ちなみに、集合と表は、データベースの世界ではほぼ同じと考えてよく、表を抽象的に捉えると集合であるということに他なりません。この時、何を持って合成や繰り返しとみなすのか?つまり、何回も出てきている「素なデータ」、つまりこれ以上分割できないデータの解釈が問題になります。「素」は意味的には数学の素数のことになります。これは次回からのテーマにしたいのですが、ここでは定義として「単一の科目」が素なデータであるとみなしているとします。集合も表も、素なデータではなく、少なくとも「複数の素なデータ」です。もう少し詳細に言えば、「素ではないデータがどこかにあるかもしれない、あるいは素でないデータが存在する可能性がある」ということで、たまたま現状の表で素なデータばかりだとしても今後素ではなくなる可能性があるのなら、そこは第一正規形を満たしていないポイントとして見る必要があります。

これらの表を第一正規形を満たすように変換した結果は、事実上、今回の3つ目の図にある「履修登録」表になります。図では、科目名が先にありますが、学生フィールドが先にある方が考えやすいかもしれません。ここで、上の図だと、同一の「履修科目」が複数登場しますが、学生は1回しか登場しません。これに対して、「履修登録」表は、学生も履修科目も複数登場します。同じデータがあちこちに登場してしまって、効率が悪く変更も大変ということになるのですが、それを解決するのが第二、第三正規形の考え方です。要するに、フィールド内で繰り返すのをやめよう、レコードとして繰り返しているものはなんとかなります、というのがリレーショナルデータベースの動作の基本ということなのです。

ちなみに軽く宣伝ですが、表の中に表があるというと、そうですね、INTER-MediatorではそういうUIを構築できます。さらに自慢をしておくと、FileMakerはポータルの中にポータルは配置できませんが、INTER-Mediatorは、表の中の表を何階層にも定義できます。実際には3つ程度でやめておくのがパフォーマンス的には有利ですが、機能は持っています。それじゃあINTER-Mediatorは第一正規形を満たしていないのかというと、それとこれとは意味が違います。INTER-Mediatorは第一正規形を満たしているデータベースから、表の中に表があるようなUIを構築できるのです。データの整合性は設計の上で取れているものから、人間が見てわかりやすい形式に表示できるUIの構築機能があるということで、第一正規形を崩しているわけではありません。データベースのレイヤーで、第一正規形を満たしておき、保存されているデータに整合性が確保されているということが重要なポイントになります。

ちなみに、第一正規形は繰り返しデータだけでなく、合成データにも当てはまります。合成データの代表と言えば、住所ですね。住所として1つに記録するか、都道府県や市区町村を分離するのかなど、議論の的になるのですが、実はこの話は第一正規形に至る部分でデータベースの理論の世界では整理されています。

ということで、次回からは、データベース設計における「1つの塊」をどのように考えるのかということのシリーズに入りましょう。

[DBデザイン#23] 関係の概念:多対多を理解する

1対多、1対1と話が進んできました。関係としては、他には多対多があります。ちなみに、多対1もあるという話もあるのですが、ほとんどの場合、双方向で検討が可能なので、1対多と多対1は、単に説明の時に適切に参照するためのものになります。例えば、「納品書と販売明細は1対多の関係にある」と「販売明細と納品書は多対1の関係にある」と説明していることは基本的には同一と考えられるからです。

多対多をこれまでの実例の中で探そうとしても、実は存在していないので、全く異なる事例を出します。この多対多が発生する代表的な例題は、学校の中のデータ管理です。学生1人1人を1レコードとする「学生」表と、履修科目1つ1つを1レコードとする「履修科目」表があったとします。それぞれ、主キーフィールドの〜IDをあらかじめ設けてあります。

これがどう多対多なのかというと、オブジェクト図と表へのデータ追加をしてみて検討することができます。例えば、学生Aは、解析学、線形代数、集合論を履修するということで、学生から見た履修科目は1対多の関係にあります。一方、線形代数学を履修する学生は学生Aと学生Bで、これもやはり1対多の関係にあります。双方とも、ボックスから複数の線が出ているオブジェクトが1つ以上は存在します。実際の大学のデータだと、このボックスが大量に発生して手で描きにくいですが、2人3科目くらいならまあ大丈夫かなと。このように、どちらから見ても多の関係が発生する可能性がある場合、多対多であるとみなします。ここで、それをどうデータ化するという話が実はちょっと込み入るのですが、以下の図の下半分の表のように表現したとしましょう。すでにIDが振られているので、それを利用しています。学生、履修科目、いずれも対応する表との複数の関連性があるので、複数のID値を覚えておかないといけませんが、表の上で見やすい便宜上の記録として、ID値をカンマで区切るということをおこなっています。

学生Aの履修科目は、101 → {601, 602,603} となっています。つまり、右の表の3つのレコードいずれも参照しているということになります。集合論の履修学生は、603 → {101} であり、一人だけが履修していることも表現されています。ちょっと見づらいとは言え、ともかく、学生と履修科目の関連はなんとなくうまく表現できていると思えます。ただし、問題はあって、ある学生がある科目を履修する場合、2つの表の〜IDをそれぞれ矛盾なく更新しないといけないなど、データの処理は面倒になります。これも、CRUDをそれぞれ考えれば、ちょっと大変そうですが、ともかくロジックが加わります。

ただ、この表を見ていて、何が問題かというと、データベース設計を少しでもかじった方は明白なように、〜IDのフィールドに複数のデータがあるため、第一正規形を満たしていないということです。第一正規形は、フィールドの値が1つの素な値、言い換えれば表の中に表があるような状態を認めないという考え方です。「素な値」というのはちょっと唐突で、これもいずれ説明したいとは思っていますが、ここでは表の中に、また表があるという状況になっているとも言えます。この内在する表をうまく外部に出すというのが第一正規形の1つの変換方法になります。一般には、その結果、表のレコードが増えることになるのですが、そこから表の分割は第二、第三正規形の適用になります。ちょっとここでは詭弁っぽいかもしれませんが、すでにIDを振っているあたり、第三正規形まで満たしている状態でもあるので、その状態を有効に活用したいと思います。

改めて学生の方を見ると、学生IDと履修科目IDの1つのデータは、101 → {601, 602,603} となっています。これは、101 → 601、101 → 602、101 → 603 といった3つの関係に分離できます。この関係だけを「履修登録」表にまとめてみると次のようになります。学生の「履修科目ID」からこの表は作ることができますが、一方、履修科目の「学生ID」からも実質的に同一の表を作ることができます。つまり、双方にIDのカンマ区切りリストを持っているということは、データが重複していたのです。重複していたので、更新時には両方を変更しないといけなかったということもここでわかります。また、同一の表を2つ持っている必要は全くありません。なので、一方の表を残すと、結果的に1つ表が増えると同時に、元からあった2つの表から双方を参照する〜IDフィールドを削除することができます。

学生の履修科目IDや、履修科目の学生IDを消しても問題はありません。この3つの表をあらためてよく見てください。学生Aは、学生IDが101です。履修登録で、学生IDが101のレコードは3つあります。その3つのレコードの履修科目IDを順番に見ると、601、602、603です。つまり、履修登録レコードに3つのレコードがある学生は、3科目を履修しているということが、データとして表現されています。そして、どの科目かということも、履修科目IDの値から求めることができます。これもやはり表と表との結合で求めることができるので、「まとめた表」を生成することが可能です。さらに、履修登録にレコードが存在することが「履修登録されている」という事実を表しています。学生Bが集合論を履修していないことは、{201, 603}というレコードが存在しないことから決定づけられます。

ここで、この3つの表の関係をあらためて見てみると、学生からみた履修登録は、1対多になります。逆に履修登録の1レコードは、1つの学生だけを記録するので、1対1です。1対多と1対1なので、学生と履修登録の関係は、1対多とするという話は2回前に説明した通りです。同様に、履修登録と履修科目も同様に多対1の関係になります。ER図的に表すとこのようになります。

このように、「関係を構築する」ための表を用いる必要が出る場合も、設計をしていれば登場します。1対多や1対1に比べて複雑になるのですが、この関係における表の抽出が設計段階にできていないと、実装が大変複雑になるか、あるいは破綻するかのどちらかです。ただ、要求だけを検討してもなかなかわかりにくいです。その場合は、ともかく表にしてみる、あるいはオブジェクト図を書いて実データとしてうまく記録できているのかなどを確認します。

ちなみに、こうした関係を「作文できる」という見方もできます。つまり、ここでは「学生は、履修科目を、履修登録する」という感じです。日本語だと最後に述語が来ますが、英語だとA student resisters a subject. つまり、主語、述語、目的語の順番になって、ちょうどER図の並びの通りになったりします。そして、両側が名詞、中央の関係を記述した表に対する表現は動詞で可能です。作文が意味があれば、おそらくはこの設計は何か正しいものを表現していることになります。ただ、作文による考え方はあまり包括的ではありません。納品書と販売明細のような場合はどうなるでしょう。「納品書は、販売明細を、持つ」とかになって、動詞というか、関係性を表現する単語がなんとでも意味を考えられるようなものになってしまうかもしれません。それでも関係が明白なら、いいのですが、動詞部分が「管理する」とか「記録する」のような、表にする限りは当たり前だろう的な動詞を割り当ててもあまり意味はないのかもしれません。なので、作文可能性は参考程度のものです。ちなみに、たくさんの表が関連しているような場合、直接関連していないけど、線を辿ると関連しているような2つの表についても、多くの場合作文は可能だったりします。

ここで、学生の科目履修だから、やはり得点の記録は必要だとなりました。さて、どこに記録しますか? 科目の得点だから「履修科目」と思った方はアウトです。もちろん、解析学が80点などと記録されますが、得点は学生ごとに異なります。履修科目に記録するには、また、最初のような第一正規形を満たさないフィールドを作るしかなくなります。同様な理由で「学生」の表に追加するのもだめです。もう結論は見えていますが、もう少し得点というデータの性質を考えてみましょう。得点は、学生ごと、科目ごとに割り当てられます。となると、ここまでの検討した結果で言えば、「学生ごと、科目ごと」に履修登録がされるという状況が作られているので、履修登録の表に得点があるというのが1つのアイデアです。つまり、表で書くとこうなります。

得点という情報は、履修が前提であるという考え方とも一致します。つまり、履修登録と得点は1対1であるともみることが出来て、つまりはフィールドでいいというのは前回に説明した通りです。

この多対多の関係は2つの1対多の関係にするという手法は「中間テーブル」などと呼ばれて、SQLでの設計手法では必ず登場するテクニックです。ですが、この中間にあるテーブルは単に2つの表の関連付けだけを行うだけでなく、ここに示した「得点」のように何らかの実データを持たせる必要が出る場合もあります。したがって、単なる中間にある存在ではなく、意味があって存在しているのです。この意味をうまく汲み取って表の名前を付ける必要があります。また、いろんな開発手法がありますが、この中間テーブル手法は、名前や適用方法、適用範囲を変えて、設計手法には必ず存在していると言っていいでしょう。

ということで、まずは多対多の中間テーブルによる展開を説明しましたが、何だかスムーズすぎませんか? 途中にも言いましたが、最初からID番号が振られているのは、ちょっと恣意的ではあります。ですが、まずは理解するためにそういう状況から初めてみました。中間テーブルというテクニックがあるものの、実は、第一から第三までの正規形を適用するということで、自動的に中間テーブルが登場するというのが本来の説明になるのでしょうけど、次回はそういう意味での説明をあらためて試みます。

[DBデザイン#22] 関係の概念:1対1を理解する

昨日は1対多について説明をしました。1対多の関係を見つけることが非常に重要で、リレーショナルデータベースはそうした複数の表に分割してデータを記録することで、多彩なデータ構造を実現しているのがポイントです。では、1対1の関係というのはどういうものでしょうか?

ここで、次のように、「商品」と、「商品在庫数」というエンティティを考えてみますが、ここで1つの商品について、1つの数値で在庫数が管理されているとします。倉庫や拠点が複数あるような会社ではこれも1対多になりそうですが、ここではこの関係が1対1であるとします。そうすると、2つ目の図にあるように、ある商品に対して、その在庫数が1本の線で結ばれます。線は必ず1本であって、商品あるいは商品在庫数の側から複数の線が引かれることはないと考えます。1対多は、「多になる可能性があれば、関係は多とみなす」と説明しましたが、1対1の場合は「常に1つのものと関係する」という意味で使われます。1つ目の図はER図とも言えますが、2つ目の図はレコードを1つのボックスとして模して考えたオブジェクト図的なものです。この時、やはり具体的に表で考えれば、それぞれに商品IDフィールドを備えた2つの表にすることが考えられます。同一の商品IDを持つレコードが対応関係にあります。

表を作るには、まず、商品があって、商品にそれぞれ一意な商品IDの番号を振ります。そして、商品在庫数は、すでに存在する商品IDの値を商品IDに入れて、在庫数をさらに別のフィールドで管理します。商品の側の商品IDに重複がないのは当然ですが、商品在庫数の側でも商品IDは複数存在することは問題が発生します。仮に、101に対するレコードが商品在庫数に複数あれば、「どちらが正しいのか?」という問題が発生するので、なんらかの方法で、商品IDの重複がないようにしなければなりません。システム上ではもちろんバグがないように作ると同時に、重複があればエラーになるような仕組みを使うのかもしれません。

ちなみに、商品IDを重複させていいのかということも気になるかもしません。一方の表の商品IDフィールドがないものとして考えるとどうでしょう?そちらの表の各行のレコードは、その値がどの商品のものなのか特定できなくなります。なので、商品IDが必要になるのです。

しかし、この2つの表をみていて思うことは、次のような表にまとめてしまえるのではないかということです。ちょうど、商品IDが同じような並びをしているので、2つの表を左右に並べてくっつけたような感じになります。前の2つの表でも管理はできそうですが、次のような表でもデータは問題なく管理できそうです。つまり、機能的にはほぼ同一と言えませんでしょうか?

多くの場合は1対1の関係は、同一の表にまとめてしまえることが一般的であったりします。つまり、商品と在庫数の関係は、1対1ですが、この場合、商品という抽象概念、つまり表の1行として構成されている存在に対して、在庫数はその商品の属性になります。ここでは、「商品」「商品ID」「商品名」「在庫数」が、商品に関して登場する概念ですが、「商品」以外はみんな「値を持つ」ことがまず挙げられます。「商品」はこれら値を持つものの集合のように思えないでしょうか?つまり、IDや名前、在庫数をひっくるめて商品なのです。そして、これら、「商品」「商品ID」「商品名」「在庫数」については、2つの要素の組み合わせ全てについて1対1の関係になっています。であれば、値のある「商品ID」「商品名」「在庫数」がフィールドになり、それらを総称的に表現している「商品」が表の名前になるという考え方ができるのです。

そうなると、設計の上で、1対1の関係は、全部同一の表、つまりER図的には1つのボックスにまとめてしまうのかというと、必ずしもそうではありません。設計の段階に応じていろいろな考え方が適用されます。例えば、要求段階で作成されるようなビジネスモデルのクラス図などでは、商品に対して在庫の管理が必要であることを明示するために、あえて別のボックスで示す場合もあります。おそらく、在庫が何に対しての在庫なのかが明白でないような場合、「近々、関係性を正しく定義して、どこかに収める」ということを意図したメモのような感じで1対1の存在を記載する場合もあります。実際、それは属性なのか、それとも表なのかが明白でないような場合も時々発生します。その場合、とりあえず1対1の関係が図に出てきます。ただ、完全に仕様が把握した上でのデータベース設計では1対1は不要とも考えられますが、システムの構成やあるいはロジックの構成によっては1対1の表、つまり、1つの表を分割することもあり得ます。例えば、それぞれが異なるサーバで発生するデータである場合は、別々の表で管理する方が、後から統合する手間がおそらく減るのではないでしょうか? こうした実装上の工夫という点では1対1の表の存在も無視できないでしょう。また、対応する1対1のテーブルの一方の存在そのものが、何か処理をしたフラグ的な使い方もあるかもしれません。ワークフローが進んで行く時に、順次フィールドを更新するよりも、それぞれ別々の表で管理して、1対1のレコードが増えていくような作り方をする方が整理される感じがする場合もあると思われます。

なお、フィールドが多数になると1対1の表に分割というのはあるかもしれませんが、分割基準が雑だったりするとかえってどっちにあるのかわからんということになります。あまり多数だから減らすためというのはモチベーションとしは弱いと思われます。ちなみに、フィールドが数万個にもなるというのは、設計そのものをまずは疑います。通常、横に展開、つまりフィールドの定義が必要と思っても、あまりに多くなるのなら、それは縦に展開、つまりレコードを増やして記録する方法を考え、その手法で設計を考えることになるでしょう。ただ、縦に展開するのが必ずしも良いとは限らず、最終的には要求との擦り合わせが必要ですが、第一正規形を満たすという意味でも、多数のフィールドは「同質のものの繰り返し」があるのかもしれません。ここで何が言いたいかというと、病院の検査結果のデータベースを例に出すとわかりやすいのですが、これはまた別の機会にしましょう。

ということで、残るは多対多ですが、これは次回に説明します。

[DBデザイン#21] 関係の概念:1対多を理解する

今回から数回に分けて、改めて関係の概念を整理しましょう。理解すべきことは、1対多、1対1、そして多対多がどんな状況なのかということです。すでに何度も説明している通り、リレーショナルデータベースは表形式のデータを多数扱うことが得意であり、複雑なデータは複数の表にうまく分割することで扱うことができるのです。そこで、システム化しようとしている世界の中で、特に1対多の関係にある表を見つけ出すことが非常に重要になります。ただ、そのためには、1レコードが何なのかということを決定しなければなりません。1レコードの基準が決まれば、それらが1対多なのかどうかということが決まりますが、実際の設計では、基準が揺らぎ、その都度検討は必要になります。つまり、決めたことが間違っていることもあるわけで、行きつ戻りつ設計は進むことになります。

今までのところで、いくつも1対多が出てきていますので、その概念を再掲しましょう。1つの商品が複数の販売機会において販売されるという事実を考えれば、商品と販売明細が、1対多の関係になります。それをER図的に示すとこう書きます。設計図自体は必要なことだけを記述してシンプルに記述する方が何かと便利なのですが、設計者はもちろん開発に関わる人たちはこれを逆に読み解けるようになっておかないといけません。線を引いてあるもの同士は原則として直接関係があり、この場合は、線で引かれることにより販売においては商品が指定されることを意味しているとも言えるでしょう。線の一方が烏の足跡のようになっていますが、それが「多」の側を示しています。「1」の側は単なる線です。この書き方は改めてまた説明しましょう。

ここで、それぞれの表で行が追加される状態、つまり、レコードが追加されていく様子を図にしてみました。前の図はER図なので、箱はエンティティ、つまり表の存在そのものを記述します。一方、以下の図はオブジェクト図的なもので、箱は1レコードと考えてください。ここでダイアグラムの解釈を切り替える必要がありますが、設計時にはそういう頭の切り替えは頻発します。これは慣れるしかなく、言い換えれば、それぞれの図の前提を把握した上で読解しないといけないということでもあります。このダイアグラムでのレコードとレコードの関連は、原則「1対1」を意味する単なる直線のみになりますが、1つのレコードから複数の線が引かれる点が「1対多」の意味になるのです。以下、レコードが何もないところからレコードが順次増えるということを想定して、表の中にあるレコードの関連を図にしています。

全ての商品は、何度も販売されるので、最後の商品Aのように、1つの商品が複数の販売明細に登場します。この事実が1対多の根拠です。商品Bを見れば、1つの商品に対して1つの販売明細2にだけ関係があるので、この商品だけを見れば1対1です。ですが、商品全体にわたって、1対多の関係を持つものがあれば、この関係は1対多と呼びます。いわば、1対多の可能性があれば、1対多であると認識するのです。実際どうなっているのかということよりも可能性で考えます。極端に言えば、レコードが全く存在しなくても1対多であると表現します。

実際にこうしたレコード間の関連をどうすれば構成できるのかについては、すでに説明していますが、この場合では以下のようになります。まず、1側の商品の表に、特定の商品を参照可能な「商品ID」のフィールドを確保します。商品の特定が可能になるには、商品の表の商品IDに重複があってはいけません。また、レコードとして同じ商品が複数の行に登場してはいけません。このような商品IDフィールドを主キーフィールドと呼びますが、キーフィールドの概念については改めて説明します。数値にするのは検索が安定しているからです。文字列だと全角だの半角だのということがあって間違いやすいから、一般には数値を使います。一方、多の側の販売明細でも、ここでは同じ名前の商品IDフィールドを用意しました。そして、商品名などを記載するのではなく、商品IDの値を書き込みます。ここでは商品Aは2つの販売で登場するので、商品IDフィールドには同じ値の101が2つのレコードに登場しますが、これがまさに「多」の意味でもあります。こうした主キーフィールドを参照するフィールドのことを外部キーフィールドとなどと呼びます。

ここでは商品の表が、主キー以外は商品名しか存在しないので、販売明細側の商品IDの代わりに商品名に置き換えれば、利用者が参照したい表の形式に近付くとも言えますが、正しくは以前に説明したように、販売明細の列が増えるのが表の結合の処理です。つまり、商品IDによって参照される商品の表の1レコードが、販売明細の右側に追加されるのです。この方法により、たくさんの販売明細から特定の商品が常に参照されることになります。こうした結合した表は、原則として一時的に作られて、例えば、明細一覧として画面に出ますが、通常はそれはその時に見せて終わりで、また次に同じ画面を呼び出すと、現状のデータに応じて結合した表を作り直し、画面に表示します。

ということで、1対多についてはほとんど復習のような感じですね。次回は1対1、そして引き続いて多対多についての説明を行いましょう。

[DBデザイン#20] 実例から考える: 概念が増えた2

販売とは別に出荷管理をしているという前提では、データの保持だけはなく、出荷残数を求める方法も確立しておかないといけないということで、前回は銀行口座方式を紹介しました。もちろん、それはそれでうまく行くでしょうけど、データ編集にロジックが絡む点が、複雑さを発生させることも紹介しました。今回は、この応用で、一定期間ごとに残数のキャッシュを作る方法を紹介しましょう。前回の手法の応用なのですが、いくつか利点が増えます。

早速スキーマと、表を示します。スキーマの基本的な形は前回と同じです。顧客と商品に紐づいた受注出荷集計という新たな表が加わりました。初期状態はレコードなしです。ここでは1つの期を1ヶ月とします。1ヶ月に納品書2枚ということはないでしょうけど、考えやすくするために小規模にデータ発生を考えます。2枚の納品書、3回の出荷予定があった場合、その気の受注数、出荷数、そしてここでは出荷残数もフィールドに求めています。

この状態で次の月になって、新たに納品書と出荷が作られ、月末になって集計するとします。とりあえず、受注出荷集計には、月末にレコードを追加する、つまり、期ごと、商品ごと、顧客ごと、という3つの分類軸で受注数、出荷数、出荷残を求めることにします。

こうなると、例えば、12月の途中では、11月までの集計結果を受注出荷集計から取り出し、12月1日以降の納品書、出荷予定の明細分の増減を行うことで、ある時点での出荷残が得られます。当初は出荷残を求めるために、明細をデータの蓄積を始めた最初の段階からチェックすることになってしまいましたが、期を決めて、期末以降をチェックすれば良いという方法であれば、データが増えてもその月のデータを舐めるだけで済むのので、年々線形的にパフォーマンスが悪くなることもまずはないと思われます。ただ、SQLのビュー等で残数を得られるようにすることになるでしょうけど、ビューの定義はちょっと込み入りそうです。

そして、銀行口座の残高のように、明細が発生するたびに残高を更新する方法だと、競合の問題や、ロジックを実施しないとデータが正しくなくなるなどの問題がありましたが、この方法だと、販売明細や出荷明細を作る上での追加のロジックはなく、いずれもレコードを作るだけで処理対象に加わります。もちろん、帳票を作成途中の場合はどうするというワークフローに関わる問題はありますが、編集に関わるロジックはかなりシンプルです。お客さんが勝手にレイアウトを作っても、きちんと出荷残の計算には明細が絡んでくるでしょう。

受注出荷集計のレコード追加は、期末あるいは期首に、バッチ処理を動かすことになります。もちろん、バッチ処理が正しく動くこと、そして、バッチ処理が失敗した時の対処など、バッチ処理の開発は大変ですが、ともかく、利用者が簡単にさわれないところにロジックがあるのはある意味、開発側からすると管理しやすいとも言えます。

受注出荷集計が多数のレコードになって重くないかという心配もあるかもしれませんが、過去に渡ってデータを舐めることはないので、あまり心配はいりません。どうしても心配なら、最新の期のデータ以外は削除するという方針でもいいでしょう。現状の出荷残を求めるということだけのためなら、過去の期のデータは不要になります。

棚卸し調整は、バッチ処理を動かした直後に、フィールド値を手作業で直すというのが1つの方法です。棚卸しについては、前回と手法的には同じになります。なお、期は一定である方がいいかもしれませんが、仕組み上は受注出荷集計のレコードは、気が向いた時に作ってもいいくらいのものです。要するに、明細の全部を舐めることを避けるための手法ということになります。

前回の「受注出荷数」は、出荷残のフィールドがなく、計算で求めることにしました。しかしながら、今回の「受注出荷集計」には出荷残フィールドがあります。これも、それぞれのロジックを作るときに「おそらくこう考えるだろうな」という意図が込められています。前回の銀行口座方式の場合、直接に出荷残のフィールドを増減する方法も考えられるのですが、競合する確率が上がることなどがあるので、増と減をそれぞれ別々のフィールドにしておきました。一方、今回の場合はバッチ処理で求めているので、残数はバッチ処理内で簡単に求められます。結果、残数が必要なので、それをフィールドとして置いておき、むしろ受注数や出荷数は不要になります。ですが、説明上ややこしくなるので、受注数と出荷数のフィールドは置いておきました。変に細かく考えているとも言えるかもしれませんが、残数の出所の違いでこうした変化が発生しうるということでもあります。

この方法は、期の概念の導入と、バッチ処理などのロジックの発生をどう評価するかです。ロジックは発生しますが、銀行口座方式のように、単一のフィールドを複数のユーザで更新するような仕組みよりも、バラバラにレコードを作っておいて必要な時に集計する方が、システムの動作上は安定することが期待できます。どちらが良いのかという問題は、実際の要求に照らし合わせないと結論は出ませんが、少なくとも、いろいろな実現方法があることは考慮すべきでしょう。

ということで、一旦、あるロボットおもちゃメーカーを題材にしたシリーズはここまでとします。しばらくは、データベースを理解するのに必要な概念を深掘りしていこうと思います。更新頻度は落ちるかな〜

MySQLのインポート制限解除をmysqlimportコマンドで行う

MySQLでCSVファイルのインポート作業をしようとすると、次のようなエラーが出てしまった。

mysqlimport: Error: 3948, Loading local data is disabled; this must be enabled on both the client and server sides, when using table: pcode

ということで、このまま検索すると、対処方法が書いてあるサイトが見つかる。

だけど、mysqlimportコマンドを使ってインポートしている場合のクライアントのローカルファイル許可の方法が分からない。MySQLのマニュアルにちゃんと書いてあった。–localというオプションをmysqlimportコマンドに追加すれば上手くいった。