ホームリレーショナル・データベースの世界

怪文書



 2005年6月11日、パスカルは仲間のデイトとダーウェンに「えらいもの見つけてしもうた」と、興奮気味にメールを送りました[1]。彼が見つけた「えらいもの」とは、とある Web サイトで見つけたこんな書き込みでした。

  • しかし、モデルおよびデータベースにとって必要不可欠の基礎的な値が一つ存在する ―― それが NULL である ―― NULL はモデルとシステムのレベルでは、不在としてその意味論を定義されている ・・・・・・ NULL は空集合と同一である(それは欠如であり、欠落である)。私は、他の値を持たないモデルは想像できる。しかし NULL を持たないモデルは想像できない。言い換えれば、NULL は全てのモデルの始点であり、NULL の後にはじめて他の非-始原的な要素を追加することができる。そうした他の要素は、モデルを汚染しており、不必要だと言っても差し支えない。だが、NULL は違う。NULL がこの役割を果たすために、他の値は全く真の「値」ではないのだ(その意味で、5, 10, "テキスト"のような値を「普通の」値と呼ぶ)。

 「わくわくするだろ」とパスカル。確かにわくわくするけどさ・・・・・・。

 結論から言っておくと、この引用した文章は徹頭徹尾まちがっているので、真に受けないでください。パスカルは「取り合うだけ時間の無駄だ」と一蹴していますが、私も、まあ同感です。

 でもそれはそれとして、空集合と NULL の違いというのは面白いテーマです。ダーウェンもパスカルへの返信で要点をついたコメントをしています。ここは一つ、この目の眩みそうな勘違いを奇貨として、生産的な議論へ発展させてみようではありませんか。

NULL と空集合は別物



 NULL と空集合の最も根本的な相違は、NULL は存在しないのに対し、空集合は存在する、ということです。ここでの「存在する」という言葉は、「代数演算の対象として整合的に扱える」という意味です。普段の生活の中で物がある/ないと言うときの「存在」とは違いますし、哲学的な含意もありません。ごくプラグマティックな用法です。

 とはいえ、これだけだと分かりづらいので、説明しましょう。まず、NULL も空集合も、何かが無い状態を表す点では同じです。NULL の場合は、ある実体の属性が、空集合は、そもそもその実体が無いことを表す概念です。実装レベルでは、その「実体」とは行のことです。

 NOT NULL制約のついていない列では、NULL の箇所がポッカリ穴を空けたように見えますが、その穴自体は、何の値でもありません。NULL はよく「値ではない」、「変数ではない」という否定的な特徴づけがなされますが、敢えて積極的に定義するなら、「ここには値がない」という文に付けられた名前です。

 一方、空集合の実装レベルの表現は、空テーブルです。NULL と違って、こちらはれっきとしたスキーマ内に存在するオブジェクトです。そして何より、空集合は、集合代数において単位元(identity)の役割を果たす重要な存在です。単位元というのは、数学の群論などで使われる概念で、非形式的に言えば、

 二項演算の中に含まれたとき、もう一方の元に一切影響を及ぼさない元のこと

です。言葉で説明するとわかりにくいのですが、具体的に見れば難しいものではありません。例えば、加算(+)と整数の集合についての単位元は 0 です。全ての整数 X について、0 + X = X + 0 = X が成り立つからです。加算において、0 は X に一切影響を及ぼしません。
 集合代数の和は、SQL において UNION として実装されていますが、任意のテーブル S に対して、空集合(Ø)は、

     Ø UNION S = S UNION Ø = S

という性質を満たしますから、これは立派な単位元です。また、0 が減算(-)に対して右単位元になるように、空集合も差(EXCEPT)に対して右単位元になります。

 このように、空集合はゼロと共通の性質を多く持っており、ともに代数系の中で基礎的な位置を占めています。

 翻って、NULL はどうかというと・・・・・・これがもう破壊的きわまりない。よく知られているように、NULL が演算の中に含まれると「NULL の伝播」というブラックホールのごとき現象を起こします。

  1 + NULL = NULL
  2 - NULL = NULL
  3 * NULL = NULL
  4 / NULL = NULL
  NULL / 0 = NULL

 もはや単位元どころではありません。演算そのものを壊しています。しかも、NULL には集合演算は適用できないうえ(だって集合じゃないから)、最も基本的な論理法則である同一律(x = x)すら成立しません。空集合の場合は、ちゃんと「Ø = Ø」が成立します。

 このように、体系内での振る舞いという観点から見れば、NULL と空集合は月とスッポンほど違います。空集合が体系をうまく機能させるために、むしろ積極的に要請されたのに対し、NULL は体系に噛み付き、食い破ってしまいます[2]

SQLの欠陥 その1:空集合と集約関数



 NULL と空集合を、数学的対象として扱えるか否か、という観点から見れば、 およそ同一視しようという気は起きません。しかしそれにも関わらず、冒頭でパスカルが面白半分に引用するような勘違いが生じるのはなぜでしょう? もちろん、「無い」という状態を表す記号という共通点に目を奪われてしまう、というのが一つ。もう一つは、SQL の仕様にも問題があることです。というのも、SQL の NULL と空集合の扱いには、かなりの混乱があるのです。
 一例を挙げると、空テーブルに対するクエリの動作です。EmptyTblという、0行のテーブルを考えましょう。すると、次のクエリは、一行も返しません。

  --サンプル1:一行も返さない(でも空集合を返す)
  SELECT col
   FROM EmptyTbl;

 これは、ごくまっとうな動作です。このクエリは一行も返しませんが、より正確に補足すると空集合を返しています。目に見えないから実感湧かないでしょうが[3]、このことは、次段でサブクエリ式の戻り値を考えるときにはっきりします。
 さて、問題は、次のクエリです。

  --サンプル2:一行返す
  SELECT SUM(col)
   FROM EmptyTbl;

 驚いたことに、このクエリは一行の結果を返します。0行のテーブルから1行の結果が生まれたのです。何でしょう、この無から有を生むがごとき所業は。そしてそこに何が含まれているかというと・・・・・・

 NULLなんですよね。

 ちなみに、SUM の代わりに AVG や極値関数を使っても結果は同じ。COUNT を使ったときだけ 0 が返ります。いずれにせよ、空集合に集約関数を適用すると、要素数1の集合が生まれるということです。そしてさらに不思議なことに、GROUP BY を使うと、結果は再び、一行も返りません。

  --サンプル3:一行も返さない(でも空集合を返す)
  SELECT SUM(col)
   FROM EmptyTbl;
  GROUP BY col

 実は、このクエリの動作は、見た目ほど不思議なものではありません。結果が空集合になる理由は、GROUP BY が元の集合からパーティションを作る働きをするからです[4]。定義上、空集合は一つのパーティション ―― すなわち自分自身 ―― しか持たないため、このケースで空集合が返るのは、それほど意味不明なことではありません。

 こうした空集合と集約に関する SQL の仕様は、昔から議論の的になってきました。デイトは、総和を加算の拡張と考えて、空集合に SUM を適用した場合はゼロを返すべきだと主張しました[5]。ゼロは加算の単位元なので、総和を求める手続き型のアルゴリズムで初期値として使われます。SQL もそれと歩調を合わせたらすっきりするじゃないか、というわけです。

 この議論に対して反論したのが、セルコです[6]。反論の論拠は、主に以下のようなものです。
  1. やはり「無から有を生む」気持ち悪さは解消されない。
  2. 手続き型の総和アルゴリズムにおいても、配列が空だった場合の戻り値はゼロ以外を指定することができる。
  3. 空集合に適用した場合と、たまたま総和がゼロになる非-空集合の結果は、区別するべきではないか。
 いずれも、それなりに説得的な理由です。1番は直感的に多くの人が感じるでしょうし、3番は実務上で重要な意味を持ちます。そもそも足しあげることができなかったのか、足しあげたけど偶然ゼロになったのかを区別したいケースはあります。セルコは、空集合に SUM 関数を適用した場合の対処として
  1. 現行どおり NULL を返す
  2. エラー・メッセージを返す
  3. 空集合だったことを示すインディケーターを追加する
のいずれかを選ぶのが良いとしています。1番は現状維持ということなので割愛。2番は、理論的には分からなくもありません。例えば極値関数を使うとき、「この集合の中から最大値を一つ選べ」と言われて、その集合が空集合だった場合、答えとしては「選べません」と言うよりほかにないからです。しかし、空テーブルを集約するたびにエラーを返していては、現実問題としてシステムがまわりません。

 私は個人的に、3番のアイデアを支持します。結果に NULL を返す仕様はいまさら変えられないでしょうから、せめてホスト言語側でインディケーターを取得できるようにしておくのは便利ですし、旧来の仕様とも矛盾しません。現実的な落としどころでしょう。

SQLの欠陥 その2:サブクエリの罪



 SQL-92 で追加された重要な機能に、サブクエリを式として演算の中に組み込めるというものがあります。非常に便利な機能なので、皆さんもよく使っていると思います。この拡張自体は大変素晴らしいことだったのですが、SQL はここで一つの際どい過ちを犯しました。それまで SQL は空集合のための記号を持っていなかったため、NULL で代用することにしたのです。カントールが聞いたら仰天するでしょう。

 そうすると、サブクエリの戻り値が空集合であることの判定も、なぜか IS NULL 述語で(!)やることになります。例えば、差集合を応用した関係除算は次のように書けますが、ここでも IS NULL で空集合の判定をしています[7]

  --差集合を利用した関係除算:IS NULL で空集合チェック
  SELECT DISTINCT shop
   FROM ShopItems SI1
   WHERE (SELECT item FROM Items I
        EXCEPT
        SELECT item FROM ShopItems SI2
        WHERE SI1.shop = SI2.shop ) IS NULL;

 同様に、外部結合がスカラ・サブクエリで代用できるのも、スカラ・サブクエリが空集合の代わりに NULL を返すために「図らずも可能になってしまった」テクニックです[8]。SQL の仕様からして混乱しているのだから、NULL と空集合を混同する人が現れるのも、仕方ない面はあります。でも、ここまで読んだ皆さんは、もう勘違いしないでくださいね。

 NULL と空集合は全くの別物です!



[1] Pascal, F with Darwen, H and Date, C, J, "On NULLs, Empty Sets (and Minds)", 2005.

[2] その証拠に、集合論にはその名もずばり「空集合の存在公理」という公理が存在します。しかし「NULL の存在公理」はどの体系にも存在しません。

[3] なかには MySQL のように、クエリが一行も選択しなかったときは、「Empty set」と端末に表示してくれる実装もあります。理論的な厳密さに配慮した、丁寧な措置です。

[4] ある集合の部分集合の集合 P は、次の三つの条件を満たすときにパーティション(類別)と呼ばれます。
  1. P の全ての要素が空集合ではない。
  2. P の全ての要素の和が元の集合と一致する。
  3. P の互いに異なる任意の二つの要素が共通部分を持たない。
 SQL の GROUP BY は、まさにパーティションを作る演算子です。「PATITION BY」という表記が使われていないのは、パーティション化した後に必ず集約が行われるからでしょう。この名前は、OLAP 関数の PATITION BY句に使われています。

[5] Date, C, J, "Empty Bags and Identity Crises" Relational Database Writings 1991-1994, Addison-Wesley, 1995, pp.49-54.

[6] Celko, J, "Summing With SQL", 1996.
 実は、セルコは第4の根拠として「空集合に対する集約結果がゼロだと選択公理に反する」という理由も挙げているのですが、多分これは間違っています。というのも、選択公理の仮定には「集合族の要素は空集合ではない」という条件が含まれているからです。選択公理は、選択元の集合が空集合だった場合については何も言っていません。

[7] このクエリは IS NULL 述語をスカラ値にしか適用できない実装の場合、サブクエリが複数行を返すとエラーになります。しかし標準 SQL では、IS NULL は IN述語のように、複数のスカラ値をまとめたリストに適用できるとされています。
 また関係除算の説明については、「HAVING句の力」を参照。

[8] この書き換えの具体例については、「外部結合の使い方」を参照。

 

Copyright (C) ミック
作成日:2007/01/26
最終更新日:2017/06/22 b_entry.gif b_entry.gif