[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コマンドに追加すれば上手くいった。

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

ここまでのところで、納品日が増えたという要求を紐解くと、結果的に出荷という仕組みが必要ということになり、出荷伝票を作ろうという感じでスキーマを変更してきました。納品書(受注受書)と対応を取らないで出荷をするとなると、現状での出荷残数を求めたくなります。それを、販売明細と出荷明細から処理するというのは、データが大量になった場合を考えると、あまり効率良くはありませんし、FileMakerのような一桁以上は遅い感触のプラットフォームでは、さらにパフォーマンス低下の体感が数年後に来そうでやばい(普通の意味で、若者言葉ではなく)です。

そこで、まずは、現状の出荷残をデータベースに記録する方法を考えます。前回に出てきた「銀行口座方式」です。受注すれば増やし、出荷すれば減らすという意味で、イメージはそんなに難しくないと思います。実は実装上はいろんな問題がありますが、後ほど説明します。まず、そうした出荷残データを残す場合、「何に対する出荷残」なのかをしっかり考えます。モデルにしている会社の場合、とりあえず登場しているエンティティは、商品、顧客です。言い換えれば、商品あるいは顧客が違うと、それは「別口座」つまり別のレコードを用意する必要があります。商品として「ロボットいか1号」があるので、それに対する受注残を管理すればいいということではありません。「トイザラシ」の「ロボットいか1号」と、「Amezon」の「ロボットいか1号」は、同じ商品でも、カウントする対象が別なので、異なるレコードで出荷残を管理しないといけなくなります。まずは、スキーマを見てみましょう。ERと表で示します。

新しいエンティティとして「受注出荷数」が追加されました。この表で、受注数と出荷数をカウントして、残数は引き算で求めることにします。この「引き算する」というのが設計に明示されていませんが、これは実はERなりクラス図を作るときには色々と工夫が必要な箇所ですが、ここでは説明でとどめます。実際、受注数は受注明細の個数と関係があるので、受注明細と受注出荷数は関係あるという見方もできます。正確には、受注明細の集約(Aggrigation)を利用してそれらのボックスを線で結び、コメント等で計算方法を示すことも可能ですが、作図テクニック的に寄りそうなので、ここではまず、主要な関係を示すにとどめたいと思います。

いずれにしても、この受注出荷数は、「顧客と関連している」「商品と関連している」というのがカウント対象を検討するときに出てきたことを設計上に落とし込んだ言い方になります。よって、顧客や商品と線を引きますし、表には関連付けできるように、商品IDと顧客IDのフィールドを持たせる必要があります。ここで、1顧客に対して複数の商品があるので、顧客と受注出荷数は1対多の関係になります。同様に、商品と受注出荷数も1対多の関係になります。そのためには、関連づけるためのフィールド、商品IDと顧客IDはいずれも、受注出荷数の表に確保します。

そして、「初期状態」を見てください。ここでは簡単のために、商品は3、顧客は2ですが、「受注出荷数」の表には、3 x 2=6個のレコードが必要です。つまり、「口座は6つ用意する」ということが必要になります。この初期状態を仮定した上で、納品書や出荷予定が作られた後の作業が可能になります。ここで、受注出荷数のCRUDを考慮しておく必要があります。商品が増えた、あるいは顧客が増えた場合を考えてください。商品が増えた場合に、それが全ての顧客に販売する可能性があれば、受注出荷数の表には、顧客の数だけレコードが増えます。つまり、「口座」を作っておかないとカウントできません。もっともここで、うまく設計すれば、商品の出荷可否的な処理も組み込めそうですが、ややこしいので、一旦忘れましょう。同様に、顧客が増えれば、その1顧客に対する全商品の記録を残すので、商品の数だけレコードが増えます。商品や顧客に行を増やすと同時に、受注出荷数の表の行を増やす作業を組み込む必要があります。商品や顧客を削除するときはどうしましょう。本当に削除するのか、記録としては残しつつ受注や出荷がなくなるだけなのかなど細かい動作の検討は必要ですが、一番雑な方法は「放置」であり、まじめな方法は「対応するレコードの削除」となります。これも、単に商品や顧客の削除や無効化だけでなく、他の仕事もしなければなりません。このように、生成と削除時に処理をかませないと全体的な整合性が取れないという状況であり、このような状況を「ロジックが含まれる」というような言い方をします。

受注出荷数のUPDATEはどうでしょうか。図の残りを見てください。販売明細が増えると対応する顧客及び商品に対する受注数が増え、出荷明細が作られると対応する出荷数が増えます。何度も言いますが、理解できない場合は、このように、小さなデータの表を実際に書いてみます。このようにもう書いてみてあるものを眺めてもいいのですが、自分で行を増やしてみたときに、正しく他の表も更新できるかは是非とも試してみてください。設計が正しくできる人は、それができるのです。最終的に出荷残があるのは、商品ID=102、顧客=201(つまり、ロボットいか2号でトイザラシ)に対して2であって、他の商品/顧客に対しては残数は0になっています。

ここで考えることは、販売明細や出荷明細の数値が確定したときに、受注出荷数のレコードを更新します。更新と言いながらも、現実的には現状の値を取り出し、明細の数値を加えて、元のフィールドに書き戻すということを行う必要があります。これが基本ロジックです。そして、この処理は、複数のユーザが同時にこのレコードを修正するということを考慮しなければなりません。ロジックの作り方によっては競合が発生するので、ロックや同期などを実装しなければなりません。実はこれがなかなか難しい場合もあります。ただ、絶対にロックが必要かというと、例えば1人しか事務員がいないならおそらくは大丈夫など、どこまで実装するかは要求と実態から詳細に検討することになります。

ここで難しいのは、ユーザエクスペリエンスを考慮したときの設計です。テキストフィールドで数量を受け付けるとして、それが更新される度に受注出荷数のレコードを更新するのでしょうか?もちろんそれは可能ですが、単に「3個受注した」という処理だけでなく、「5個を2個にした」という処理も実装する必要があり、またまたシステムは複雑化しUIも込み入ってくるかもしれません。出荷予定についても同様ですが、3個出荷可能なのでそのような明細を記載したら、同時に誰かが同じ顧客と商品に対する出荷予定を作ってしまっていた場合どうするか? この辺りはまずはどうするかを決める要求が定まらないということも多く、要望に対する実装が結構難しくなることもあります。いろいろな複雑さを解消する方法としてよくあるのは「納品書」「出荷予定」の「確定処理」です。ボタンを押してもらいます。そのときに受注出荷数を調整しますが、別の人が出荷処理していた場合の対処などのさまざまなロジックを確定処理として実装します。このステップがワークフロー内に存在すれば、おそらくは前述の「5個を2個にした」といった対処は不要になると思われます。確定したら、編集できないくらいの対処もいいかもしれません。確定した日時をタイムスタンプで記録して、そのフィールドが空欄なら未確定というnull利用もスムーズにいきそうです。確定処理というと新しいワークフローを入れろというように受け取られるかもしれませんが、実はこの確定処理は「承認処理」にうまく組み入れることができるかもしれません。このように、実装上、複雑化しないような仕組みをうまく取り入れるということも設計の重要な要素であることは言うまでもありませんが、前回に説明した通り、ここまでのワークフローとの擦り合わせをした結果がスキーマでなければならないのです。

ちなみに、個数を変更したら受注出荷数の更新をするという処理をFileMakerで実装する場合、トリガー使えばできるじゃんと思うかもしれませんが、前述のように、単なる入力だけでなく変更に対処するということも忘れないようにしなければなりません。ですが、もっと重要なことはトリガーの設定がなされていないと受注数や出庫数の更新ができないことです。自分はFileMakerに自信があり、そういうレイアウト開発のマネジメントはきちんとやっている、あるいは、チームマネジメントはバッチリなので大丈夫!などと思っていませんか? このような複雑さシステムに持ち込むと思いがけないことが起こります。FileMakerのソリューションはお客さんが自分で変えてしまうこともあり得ます。全てのレイアウトにトリガーを完全に仕込んだとしても、お客さんが自分で新たに作ってしまったレイアウトにトリガーが仕込まれているとは限りません。かくして、「ときどき出荷数がおかしくなる」というこれまた曖昧なバグレポートが上がってくることになります。こうしたリスクまではなかなか管理しきれません。お客さんにはレイアウトを追加してもいいけども、運用前に必ず見せてもらうなどの確約を取るなど、コミュニケーションマネジメントが必要になるということです。複雑な仕組みの実装は必要であり、もちろん、それができるのが素晴らしいことなのですが、単に実装に時間がかかるだけでなく、その後のメンテナンスのことも考えておく必要があります。

さらに、棚卸しも考えないといけませんが、もっともシンプルな棚卸しは、受注出荷数の数値のフィールドをいきなり変えてしまうことです。変更した記録などは取っておきたいところですが、本質的にはその数値を変更すれば現状に戻ります。ただ、顧客ごとというのはかなり細かいですね。倉庫だと、単に商品の数を数えるくらいの棚卸ししかでないかもしれません。ちなみに、倉庫の残は、倉庫への入庫数が関わり、現状のシステムではそのための仕組みが組み込まれていませんのでご注意ください。もう少し丁寧な方法は、出庫予定に残数の誤差を調整するレコードを作ることです。特別な顧客(例えば「棚卸さん」みたいな)を用意しておくことで、通常の営業活動と区別できるかと思われますが、特定の顧客の特定の商品の場合、どのようにリアルな出荷と区別するのかは難しい問題です。このような運用でカバーはなるべく避けたいところかもしれません。いずれにしても、残数との誤差の把握が結構大変そうです。顧客から「まだ3個届いていない」というクレームがあったとして、残数が0であったような場合の調整くらいになるというところでしょうか。現実に発生する棚卸しの必要性をもとに考えを巡らせましょう。

残数を求めるだけなのに結構複雑になります。残数を求めるためのスキーマも単純そうに見えて、それを成り立たせるための戦術を考えると結構複雑になります。その戦術を実現可能とするスキーマでないといけないわけで、結果的に設計全般を考えることになります。次回は、銀行口座方式とは異なる方法を紹介しましょう。

[DBデザイン#18] 実例から考える: 要求をさらに拡大する

納品書に出荷日を入れるという簡単そうな要求でも、細かくチェックすると色々と複雑な問題が出てきました。そして、「出荷」という概念というか、それはむしろ新たな活動というべき事象が社内にはあって、以前には顕在化していなかったものが、ちょっとしたきっかけでどうやら見えてきました。出荷は、受注(ここでは「納品書」として扱っている)に関係するのは明白ですが、販売明細を出荷に紐づけるか、納品書に紐づけるかという選択肢があって、それぞれでできること、できないことがあると示しました。なお、途中には、販売明細に出荷日のフィールドを設け、納品書と販売明細の関係はそのままで、いわば、販売明細と1対1の出荷のエンティティを設けるようなことも検討しました。それらの分析途中でも指摘しましたが、納品書の明細1行がまとまって出荷するとは限らないかもしれないということもあり、今回から数回に分けて、その場合にどのようなスキーマになるのかということを検討してみましょう。

ちなみに、要求探索の意味での問題点がここで露わになってきており、なぜそんなことが発生するのかをここで考えてみましょう。出荷のことがどうやら要求を検討する段階ではおざなりになっていたというのは明白です。なぜでしょう? それは最初の方で、「いろいろな部署の意見を聞いた」とありますが、出荷部門の意見は聞いていなかったからと言えるでしょう。この会社、もしかしたら、倉庫部門が営業の中の一部で、営業の担当者は気にも止めてなかったのかもしれません。そういうことを防ぐには、ワークフローをしっかりと「記述する」ことでかなりの解決になります。記述方法は色々ありますが、ダイアグラムを記述するのが理想的です。そうすれば、出荷することは忘れないでしょう。ですが、社内の複数の部署をまたがってのフロー図を書き切るのはかなり大変です。みずほ銀行は度重なるシステムトラブルに見舞われていましたが、本格的なシステム統合に先立って何年もかけて社内業務のデータフロー図を作成したことは有名です(『みずほ銀行システム統合、苦闘の19年史』日経BP社)。これが要求を洗い出すための最善の方法になります。しかしながら、現実にそこまでの労力はかけられないとおっしゃるお客さんが多分ほとんどでしょう。そうなると、設計する側でどこまで見えない要求の糸口に気付くかに依存してしまいます。ただ、少なくとも、顧客側の経営者は、業務の要素を把握しておくべきであって、実際にフローに登場しながらも要求に組み込まれていないことを把握するのは管理者の責任であるというのが一般的な考え方だと思います。ともかく、いろいろな立場の人と対話を行うしかありません。現場の人の声を聞くのも大切だけど、それと同様に管理的な立場の人との対話も重要です。そして、少なくとも設計者はわかる範囲でワークフローを何らかの方法で記述をすべきです。書けば、繋がりの不自然なところなどが見えてきて、何か足りないのか、あるいは余計なことをやっているのかなどが顕在化してくるものです。

出荷について考慮が足りなかったのは、社内ではその部署や業務が軽視されていた可能性も高いのですが、他にも要因は考えられます。出荷業務は別途独立してシステムが確立していた可能性があり、社内的にはもう別に考えなくてもいいんだ的な空気があったのかもしれません。そのような場合、「考えなくてOK、以上」で済ませがちなのですが、そこでシステム間連携が雑だと、結局のところ最低限の業務効率化も果たせない可能性もありますし、より作業が複雑化してシステム化のメリットは全くなかったということにもなりかねません。その場合は、既存システムも調査して適合性を高めるのはもちろん、そちらの改変やリプレースを提案できるくらいの設計力は欲しいところです。

まだまだ別の可能性があります。流通を完全に外注している場合もあるでしょう。このモデルになっている会社はどうやら製造部門はあるようですが、そこから倉庫会社にひたすら製品を送り、営業は伝票を送り続けると、自動的に顧客に指定した個数が届いているのかもしれません。この場合も、根本的には別システムです。別システムでかつ別会社なので、さらに柔軟性は損なわれますが、前述の通り、適合性を高めるための工夫を設計に織り込む必要は出てくるでしょう。

ということで、「納品書と出荷の明細は対応していない」前提の設計に入りますが、まずは基本的な考え方をまとめます。以下のER図と、サンプルデータの表のような構成になります。納品書と販売明細、出荷予定と出荷明細、これらが1対多の関係にあります。納品書はもちろん伝票をもとに検討した結果ですが、出荷予定をもとにすれば、正しい意味での「納品書」つまり出荷に対応した商品明細が記載された帳票が作成できそうです。なお、納品書と出荷予定の対応は、これだと記録されていませんが、ここではそれ良しとします。ちなみに、ここでの納品書と出荷予定のそれぞれの明細が対応を取る必要がある案件をこなしたことがありますが、もう少しエンティティが増えて複雑にはなるもののちゃんと動いているのでそういう設計も可能です。

ここで検討したいのは、「ある顧客のある商品に対して、まだ出荷していない個数はいくつか」を知りたいということです。つまり、出荷残数の把握です。出荷予定の生成を考えれば、出荷残があるから行うわけで、出荷残を参照しながら出荷明細を作っていくというシナリオが自然かと思われます。ここでも表のCRUDをワークフローに乗せて考えれば具体的な方針が出てきます。発注数は納品書とその販売明細が元データとして集計すれば得られます。一方、出荷数は、出荷に対する出荷明細を元データとして集計すれば得られます。その差が、まだ出荷していない数になります。この出荷残というデータの存在は、上記のERや表ではまだ顕在化していません。

出荷残を求めることは、データベースの設計的には単純と思われるかもしれませんが、この方法には大きく2つの問題が発生する可能性があります。1つはパフォーマスの問題、もう1つは誤差の問題です。パフォーマンスの問題は、こうしたデータを何年も積み上げていることを想定すれば自ずと見えてきます。最初はそれほどのデータ量でもないものが、数年も使っていると線形的に増えます。ナイーブにな考え方では受注数と出荷数を得るために、何年も前からのデータを全部合計する必要があります。また、線形的にデータが増えるため、「年々システムが遅くなる」可能性は大です。特に、FileMakerで扱い量が多い会社では、まあ2,3年でやばくなるという感じでしょうか? SQLだとかなり多くなってもそれなりには動くと思いますが、いずれにしても効率良い方法ではありません。これに対する対応策は、何らかのキャッシュを自分で構築する方法があります。そのキャッシュをデータベースとして設計しなければなりません。別の対応策は、「現在の出荷残を記録する」方法です。販売明細や出荷明細は、在庫の出入りです。販売明細に「3個」と入力されれば3だけ増加し、2個出荷されれば2を引くということで出荷残数が1になっているようなデータを持つことです。これは、営業と出荷をそれぞれ入と出と見なせば、ちょうど銀行口座のような動作をさせるようなものと考えられます。お金ではなく、商品の個数の入と出があって、残高は出庫残となるものです。いずれにしても、こうした工夫は最初からやっておいてパフォーマンスの問題が出ないようにすべきです。

もう1つの問題は「誤差」です。在庫数や現金残高など、本来は正しく記録していれば現状と帳簿の数値は一致するのですが、これがいろんな理由で一致しません。店舗のようなところだと、一致しない理由は多岐に渡るのですが(万引き、欠品、破損、事務処理を忘れる、など)、一致しないことがいわば常識であって、定期的に「棚卸し」をするのが一般的です。つまり「現状の在庫数」を把握して、帳簿に増減をかけます。この仕組みをなしに、こうした在庫数などの処理を進めるのはおそらくかなり大変なことになります。データが正しければ棚卸しは必要はないというのはその通りですが、棚卸しが不要な現場は実のところ見たことはありません。実際の個数と帳簿上の個数が違うとき、「何で1だけ在庫が違うのか」を血眼になって検証する労力やコストを考えれば、単に調整をしてしまった方が効率的なことが普通なのです。過去の情報をどの程度振り返るのかということも関係がありますが、多くの業務は「その時点での情報」の管理で概ね方がつくという感じではないかと思います。万引きや欠品等での損失は別途考えるとして、通常の業務を回すシステムは誤差の解消を棚卸しという手法で処理をします。企業会計上、数値の調整による損失計上は一定の範囲では認められています。ただ、棚卸しによる調整をどうするのかは、その会社での業務にかなり依存します。今後に紹介するスキーマ変更では、棚卸しについては方針を示すのに止めることにします。

続きは次回に回すとして、実はこのパフォーマンスの問題を解決するのは、スキーマだけでなく、ロジックが発生します。実はスキーマは単なるデータベースの設計ではなく、ロジックのベースにもなり結果的にロジックの設計も行った結果がスキーマだったりして、システム全体に渡る方針を記述したものになるという側面が見えてきます。