このブログの更新は Twitterアカウント @m_hiyama で通知されます。
Follow @m_hiyama

メールでのご連絡は hiyama{at}chimaira{dot}org まで。

はじめてのメールはスパムと判定されることがあります。最初は、信頼されているドメインから差し障りのない文面を送っていただけると、スパムと判定されにくいと思います。

参照用 記事

データベースへの論理的アプローチ: NULLについてチャンと考えよう

奥野幹也『理論から学ぶデータベース実践入門』はどこがダメなのか」のなかで、ピンクで「(詳細は別途記述予定。)」と書いてあるところが6箇所あります。これらの“ピンクの宿題”を順不同で片付けていきます(全部、片付くかは不明)。

ピンクの宿題 その1

単なるベキ集合でも(ある意味)重ね合わせです。[...今回の話題に関係ないので省略...]この枠組内でNULLの意味も(ヨタ話じゃなくて)分析できます。(詳細は別途記述予定。)

NULLに関する基本的な事項についてゼロから考察してみます*1。予備知識として、ラーメン屋さんに行った経験を仮定しますが、その経験がなくても読めるように心がけました。この記事を読めば、NULLの使用法とダメさ加減がハッキリと分かるでしょう。

内容:

NULLはダメなの? なんで??

「データベースにおけるNULLはダメだ」というのがおおかたの意見だと思います。僕もダメだと思います。しかし、天下りにダメだと禁止するのは、子供の教育上好ましくないですよね。大人の教育でも好ましくないでしょう。

どうしてダメなのかを納得がいくように説明して、もしそれが必要悪だとしたら、どんなときなら使うことが許されるのか、あるいは代替案にどんなものがあるのかも説明すべきです。

この記事では、NULLそのものより、NULLを使いたくなる状況の背後にあるデータ型を分析することにします。それにより、NULLに関する明快な理解を得られると思います。「ダメなモノだから曖昧な理解でもよい」と考えるのは危険です。ダメなモノのダメさをハッキリと知っておきましょう。

型と型構成子

「型とは何か?」については色々な立場・考え方がありますが、ここでは一番単純な「型とは集合である」(Sets-as-Types)の立場を採用します。例えば、Z = {..., -2, -1, 0, 1, 2, ...} を整数の集合として、「nの型は整数である」は、「n∈Z」とまったく同じ意味です。

要素(項目、メンバー)が整数であるリストを、ブラケットとカンマを使って、[2, 1, 3] とか [-1, 0] とか書くことにします。[0] や [] もリストです。Intは整数型を表すとして、整数のリストの型を List<Int> と書きます。[2, 1, 3], [-1, 0], [0], [] の型はすべて List<Int> です。

伝統と習慣により、“プログラミング言語の型”と“数学の集合”では違う記法を使います。

意味 型の記法 集合の記法
nは整数の値を取る n : Int n∈Z
xは整数リストの値を取る x : List<Int> x∈Z*

Z* の右肩の星はクリーネスターと呼ばれ、形式言語理論で常用される記法です。[2, 1, 3], [-1, 0], [0], [] ∈Z* ですね。

僕はどっちの記法も使うので、n : Z とか、[2, 1, 3]∈List<Int> とか、ごちゃ混ぜ記法を使ってしまうかも知れません。どうせ意味は変わらないので、分野の垣根や方言を気にするのはバカバカしいと思います(と、ごちゃ混ぜに対する言い訳をしておく)。

Listは、型Intに対して新しい型 List<Int> を作り出すので型構成子と呼びます。山形括弧 < > 内に入る型を、型パラメータとか型引数と呼びます。型パラメータを囲むのに山形括弧を使うのはプログラミング言語型理論の習慣ですが、Scala言語やEiffel言語では List[Int] だし、圏論では List(Z) です。括弧の種類がバラバラなんです。繰り返し言いますが、分野の垣根や方言を気にするのはバカバカしいと思います(つまり、ごちゃ混ぜになっても許してね)。

ベキ集合の型

Aが集合だとして、Aの部分集合の全体からなる集合をAのベキ集合*2と呼びます。例えば、A = {1, 2, 3} のとき、Aのベキ集合は次の集合です。

  • {{}, {1}, {2}, {3}, {1, 2}, {1, 3}, {2, 3}, {1, 2, 3}}

ベキ集合は power set なので、Aのベキ集合を Pow(A) と書くことにします*3

もとの集合Aが演算を持っていると、Pow(A) にも演算を定義することが出来ます; B = {True, False} として、Bは論理演算(AND, ORなど)を持つとします。Pow(B) = {{}, {True}, {False}, {True, False}} に論理演算を定義することが出来て4値論理になります。興味がある方は次の記事をどうぞ。

Pow(A) は数学的記法ですが、対応する型構成子記法は Subset<A> としましょう。例えば、集合Bの型名をBoolとして、Subset<Bool> は4値論理の真偽値の型になります。Aが無限集合、例えば A = Z のとき、Pow(Z) には、Z自身や偶数の全体などの無限集合も入ります。コンピュータで無限集合は扱いにくいので、Z(型Int)の有限部分集合だけを考えることがあります。そのときは、(ちょっと長ったらしいけど)FiniteSubset<Int> と書くことにします。Aが有限集合のときは、Subset<A> = FiniteSubset<A> です。記法に慣れるために幾つかの例を挙げておきます。

  • {}∈Pow(B), {True}∈Pow(B)
  • {} : Subset<Bool>, {True} : Subset<Bool>
  • {}∈Pow(Z), {1, 2, 3}∈Pow(Z), {k∈Z| k≧0}∈Pow(Z)
  • {} : Subset<Int>, {1, 2, 3} : Subset<Int>, {k∈Z| k≧0} : Subset<Int>
  • {} : FiniteSubset<Int>, {1, 2, 3} : FiniteSubset<Int>

ラーメン屋さんの食券販売機

前節までで、理論的準備はすっかり整いました。説明に使う事例を紹介します。

とあるラーメン屋さんがあり、事前に食券を自動販売機で買う方式だとします。ラーメンが3種類、トッピングが2種類です。タッチパネル食券販売機は次のように操作します。

  1. 最初に、醤油ラーメン、塩ラーメン、味噌ラーメンのなかから、一種類を選ぶ。
  2. 次にトッピング選択画面が出るので、チャーシューか味玉を選ぶ。どっちか片方でも両方でも、ひとつも選ばなくてもよい(トッピングはオプション)。
  3. 決定ボタンを押すと、食券が発券される。

この食券販売機は、内部に記録(ログ)を持っていて、次のようなデータが蓄積されるとします。

注文 ラーメン トッピング
1 醤油
2 味玉
3 醤油 チャーシュー
4 醤油 チャーシュー, 味玉
5 味噌

この記録データをデータベースのテーブルのように考えましょう。テーブルは3つのカラムを持ちます。それぞれのカラムのデータ型を考えましょう。データベース用語ではカラムのデータ型をドメインと呼ぶので、それぞれのデータ型(=ドメイン)を、D_注文, D_ラーメン, D_トッピング とします。

  • D_注文 = {i∈Z | i ≧ 1}
  • D_ラーメン = {醤油, 塩, 味噌}
  • D_トッピング = Pow({チャーシュー, 味玉}) = {{}, {チャーシュー}, {味玉}, {チャーシュー, 味玉}}

プログラミング言語っぽい書き方だと(enumは列挙型として):

  • D_注文 = Int (カラムのドメイン制約: 注文 ≧ 1)
  • D_ラーメン = enum {醤油, 塩, 味噌}
  • D_トッピング = Subset<enum {チャーシュー, 味玉}>

くどいけど、書き方なんてどうでもいいんです、意味・内容を理解してください。

この食券販売機の記録は、RDB(リレーショナル・データベース)に保存して管理することにします。

繰り返し型と不明型

いよいよRDB登場 … ちょっと待ってください。我々の目的はNULLの分析なので、ベキ集合の型(Subset型、FiniteSubset型)をもう少し詳しく見ておきます。

食券販売機の記録におけるトッピングの型は、Subset<enum {チャーシュー, 味玉}> でした。トッピング・カラムの値は集合になっています。集合である値を、RDB用語では繰り返しグループ(Repeating Group)と呼びます。この用語法に合わせて、Subset<A> を Repeat<A> とも書くことにします -- Aの繰り返し型と呼ぶことにしましょう。

さて、食券販売機の故障かバグで、記録が次のようになってしまった状況を考えます。

注文 ラーメン トッピング
1 醤油
2 味玉
3 醤油 チャーシュー
4 醤油 チャーシュー, 味玉
5 ?

5番の注文が不明です。不明ではありますが、ラーメン・カラムの可能性としては 醤油, 塩, 味噌 のどれかです。つまり、'?'の意味は集合 {醤油, 塩, 味噌} と言えるでしょう。となると、次のように書き換えてもいいでしょうか?

注文 ラーメン トッピング
1 醤油
2 味玉
3 醤油 チャーシュー
4 醤油 チャーシュー, 味玉
5 醤油, 塩, 味噌

これだと、繰り返しグループと見分けが付きません。将来、複数のラーメンを一度に注文できるように変わったとき困ることになります。そこで、「よくわからん」を表すために、先頭に疑問符を付けておきましょう。

注文 ラーメン トッピング
1 醤油
2 味玉
3 醤油 チャーシュー
4 醤油 チャーシュー, 味玉
5 ?醤油, ?塩, ?味噌

この表を見ると、チャーシュー, 味玉 のような繰り返しグループと、?醤油, ?塩, ?味噌 のような不明さを表す集合が現れます。数学的にはどちらもベキ集合の型(の値)ですが、意味・使用状況まで考えると違う型です。次のように定義しましょう。

  • 繰り返し型 Repeat<A> : Aの部分集合の型だが、値は繰り返しグループを表す。
  • 不明型 Unknown<A> : Aの部分集合の型だが、値は可能な候補を表す。実際の値は、集合Aの要素のどれか。

不明型(Unknown型)の値である集合を ?{醤油, 塩, 味噌} と書くことにします。疑問符を分配すれば {?醤油, ?塩, ?味噌} だから、辻褄は合ってます :-)

ところで、?{醤油, 塩} という不明な値は意味があるでしょうか。これは意味あります。ある日、味噌ラーメンが売り切れている状態で、次の記録が発生したとします。

注文 ラーメン トッピング
1 醤油
2 味玉
3 醤油 チャーシュー
4 醤油 チャーシュー, 味玉
5 ?

不明な箇所の可能性は ?{醤油, 塩, 味噌} ではなくて、?{醤油, 塩} です。他からの情報で、不明さを絞り込めることもあるのです。

?{醤油} という不明な値はどう解釈すべきでしょうか。不明ではあるが、可能な値は'醤油'だけ、となります。つまりこれは、確定した単一の値'醤油'と同じです。塩ラーメンと味噌ラーメンが売り切れている日に'?'が現れた状況を考えれば明らかですね。

繰り返し型を禁止する

食券販売機の記録をRDBのテーブルに入れましょう。生の記録だと、「第1正規形でない」としかられるので、2つのテーブルに分けて、トッピング・テーブルを次のようにします。

注文 トッピング
2 味玉
3 チャーシュー
4 チャーシュー
4 味玉

このテーブルのトッピング・カラムの型(集合)は D_トッピング = {チャーシュー, 味玉} で、生データのような繰り返し型は現れてません。

NULLの使用場面のひとつに、繰り返し型の空{}をNULLで表すものがあります。上記の方法で繰り返し型を追い出せば、ついでに空集合を意味するNULLもなくなってしまいます。

繰り返し型の空{}を表すNULLは、もっとも排除しやすいものです。しかし、排除した後で、もとの空集合{}はどう表現されているかには注意してください。今のラーメンの例で、注文1のトッピングが空集合であることは、トッピング・テーブル(注文とトッピングのテーブル)を検索したときの検索結果が空であることに反映されます。このケースの検索は次の問い合わせで実行できます。

  • select トッピング from トッピング・テーブル where 注文=1

「正規化」という言葉を使って述べれば、次の2つの事実が同値です。

  • 正規化前に、特定注文のトッピング・カラムの値が空
  • 正規化後に、特定注文のトッピングの検索(問い合わ)結果が空

不明型の値があるとき

だんだん話が厄介になりますよ。次の生データをRDBに格納することにします。

注文 ラーメン トッピング
1 醤油
2 味玉
3 醤油 チャーシュー
4 醤油 チャーシュー, 味玉
5 ?

不明を表す'?'を、Unknown<enum {醤油, 塩, 味噌}> の値で展開すると:

注文 ラーメン トッピング
1 醤油
2 味玉
3 醤油 チャーシュー
4 醤油 チャーシュー, 味玉
5 ?醤油, ?塩, ?味噌

繰り返し型の排除と同じ方法で複数の行(ロー、レコード)に展開すると:

注文 ラーメン
1 醤油
2
3 醤油
4 醤油
5 醤油
5
5 味噌

しかしこれは、既に述べたように、将来、複数ラーメンを一回に注文できるようになったら困ります。確実な値か不明な値かを識別するカラムを設けてはどうでしょう。

注文 ラーメン 確実性
1 醤油
2
3 醤油
4 醤油
5 醤油 ×
5 ×
5 味噌 ×

一見うまくいきそうで、実際多少は使える方法かもしれません。しかし、このテーブルの情報から、真実のテーブルの候補を判断する方法がありません。例えば、次の2つのテーブルを較べてみてください。

注文 ラーメン
1 醤油
2
3 醤油
4 醤油
5 醤油

 

注文 ラーメン
1 醤油
2
3 醤油
4 醤油
5 醤油
5

一番目のテーブルは、食券販売機が正常に動いていればあり得るデータです。しかし、二番目のテーブルは、現状の(一回の注文でラーメンひとつ)食券販売機からは出現しない異常なデータです。

単に不明だ(不確実だ)というだけでなく、真実のテーブルの候補(あり得る正常データ)をキチンと列挙できることが望ましいのです。この例では、「ひとつの注文にはラーメンがひとつだけ」というルールがあればうまくいきます。が、そのようなルールは、テーブルの構造とは別に手続き的に書かなくてはなりません*4

次の節で述べる「繰り返し値が不明」の場合と比べると、より事情がハッキリするでしょう。

繰り返し型の値が不明なとき

今度は、次の生データを考えます。

注文 ラーメン トッピング
1 醤油
2 味玉
3 醤油 チャーシュー
4 醤油 チャーシュー, 味玉
5 味噌 ?

正規化しない生の状態で、注文5のトッピングの値になり得る値は次の4つです。

  1. {}
  2. {チャーシュー}
  3. {味玉}
  4. {チャーシュー, 味玉}

この4つのどれであるか不明なので、その不明値を書き下すと:

  • ?{{}, {チャーシュー}, {味玉}, {チャーシュー, 味玉}}

上のごとき、繰り返しと不明さを含む生データを、正規化したテーブルで表現することができるのでしょうか? なんか絶望的な感じがします。

ですが、意外にも簡単なんです。前節と同様、確実性のカラムを設けてみると:

注文 トッピング 確実性
2 味玉
3 チャーシュー
4 チャーシュー
4 味玉
5 チャーシュー ×
5 味玉 ×

確実性が×の行に関しては、その行が存在する場合と存在しない場合の二通りがあると解釈して、すべての可能性を列挙してみます。

2行ともに存在する

注文 トッピング
2 味玉
3 チャーシュー
4 チャーシュー
4 味玉
5 チャーシュー
5 味玉

チャーシューの行だけ存在する

注文 トッピング
2 味玉
3 チャーシュー
4 チャーシュー
4 味玉
5 チャーシュー

味玉の行だけ存在する

注文 トッピング
2 味玉
3 チャーシュー
4 チャーシュー
4 味玉
5 味玉

2行ともに存在しない

注文 トッピング
2 味玉
3 チャーシュー
4 チャーシュー
4 味玉

これら4つのケースは、あり得る真実のデータを全て尽くしています。不明さを含んだデータから、真実のデータの候補を列挙できました。

この例では、繰り返し型と不明型がうまいこと相互作用して、結果的に話が単純になったのです。一般論を展開するには、繰り返し型構成子Repeatと不明型構成子Unknownが混じった型の計算法則を準備する必要があります。そのことは、またいずれかの機会にしましょう*5

NULLの使われ方

今までの話で、ベキ集合の型が、現実の場面では繰り返し(Repeat)型と不明(Unknown)型として現れることが分かりました。

NULLの使われ方はたくさんあるそうです。どれくらいあるのか僕は知りませんが、繰り返し型と不明型の文脈で考えると、3種類になります。

繰り返し型の空

ラーメンのトッピングなしの例です。このタイプのNULLは簡単に排除できます。そもそも意味が簡単なので、排除しなくてもあんまり悪さはしません。

ただし、繰り返し型の空だけなら弊害が少ないということで、他のタイプのNULLと混在するような状況では悲劇になるでしょう。

不明型の全体集合=まったく不明

ラーメンの種類が'?'の例です。ラーメンの種類の全体集合は {醤油, 塩, 味噌} ですが、この全体集合が値なら、意味としては「まったく不明」ということです。

データベースの場合、まったく不明であっても、カラムのドメイン(データ型)はあるので、ラーメンの種類が'豚骨'は排除されています。つまり、不明のマーカーとしてNULLだけを使っている場合でも、可能な値の集合はドメインごとに違うのです。たくさんの「まったく不明」があります。「ラーメンの種類がまったく不明」「整数値がまったく不明」「二値真偽値がまったく不明」とかです。

不明型の空=禁止

不明な値 ?{醤油} は、単一の値'醤油'と同じだと言いました。では、不明な値 ?{} はどうでしょう。'?{'と'}'のあいだには、「可能性がある値を並べる」というルールを思い出してください。?{} には可能な値がひとつもありません。つまり、値は未定義(undefined)、あるいは値を持つことを禁止(forbidden)されています。

繰り返し型の空 {} と、不明型の空 ?{} は違います。{}は、「トッピングなし」のように正常な値です。それに対して、ある条件下で値があってはいけないことを ?{} で表します。

ラーメン屋さんで具体例を挙げましょう。味噌ラーメンを赤味噌ラーメンと白味噌ラーメンから選べるようにしたとします。今までの記録データを大きく変えたくないので、次の形を採用します。

注文 ラーメン トッピング 味噌
1 醤油 -
2 味玉 -
3 醤油 チャーシュー -
4 醤油 チャーシュー, 味玉 -
5 味噌 赤味噌

醤油ラーメンと塩ラーメンでは、味噌カラムは無意味です。ハイフンの意味は、未定義または禁止で、不明な型の空 ?{} がふさわしいでしょう。

未定義または禁止を表す値は、不明な型を持ち出さなくても、通常の値に特殊値として追加する方法でも導入できます。しかしそれは、繰り返し型の空や、不明型の「まったく不明」とは別物です。

NULLがダメなホントの理由

NULLにいろいろな種類があり、その扱いはけっこう難しいことが分かったでしょう。とはいえ、それぞれの種類のNULLをそのデータ型と共に分析すれば、NULLの正体は明らかになり、利用法・対処法も見えてきます。

データベースにおけるNULLがダメなのは、そのように様々なNULL達を、エヌ・ユー・エル・エルと綴る単一の記号で表そうとしているところです。つまり、性質も振る舞いも異なる様々なNULL達を区別して扱う手段がないのです。

性質も振る舞いも異なるモノ達を、ミソもクソも一緒に単一の呼び名で指し示せば、破綻することは目に見えています。どれかひとつのセマンティクスに限ってNULLを使う、という方法はありますが、そのひとつのセマンティクスの選び方が人により違うので、違った選択が出会ったとき、やはり破綻します。

結局、「NULLは使わない、使うときは外部に影響が及ばない閉じた範囲で使う」というような処方箋になるかと思います。

*1:もとの“ピンクの宿題”に書いてあった凸結合の話とかは今回はしません。これは、さらに先延ばし。

*2:「ベキ」は古い難しい字で「冪」と書くか、あるいは略字の「巾」を使います。どちらも難読なので、僕はカタカナ書きしています。

*3:印刷物では、ドイツ文字(フラクトゥール)のペー(P相当)が使われることが多いです。しかし、プレーンテキストでペーを書くのは難しいので、僕はPowを使っています。

*4:奥野幹也『理論から学ぶデータベース実践入門』はどこがダメなのか」のなかで説明した演繹データベースならば、手続きではなくて論理式として「ひとつの注文にはラーメンがひとつだけ」を書けます。

*5:RepeatとUnknownはどちらベキ集合を作る型構成子です。その点では同じ。しかし、現実的な解釈は違います。「同じだけど違う」をうまく定式化するのはけっこう難しそうです。