要するに、データベースにnullが1つでも含まれていれば、クエリから正しくない結果が返される可能性がある。しかも、一般的には、どのクエリから正しくない結果が返されるのかを知る方法はないので、すべての結果があやしく見えてくる。nullが含まれたデータベースから正しい結果が得られることは確信できない。筆者に言わせれば、この状況はまさにお手上げである。
――――C.J.デイト
はじめに
多くのプログラミング言語が、真理値型(BOOL
型、BOOLEAN
型)というデータ型を持っています。もちろん、SQLにも真理値型が存在します。ユーザーが直接扱えるデータ型として定義されたのはSQL-99ですが、WHERE句などの条件の評価時にも真理値の演算が行なわれています。
ところで、普通のプログラミング言語の真理値型とSQLの真理値型の違いをご存知でしょうか? それは、普通の言語の真理値型が、true、falseという2つの値を持つのに対し、SQLはそれに加えて、unknownという第3の値を持つことです。このため、2つの真理値だけを持つ通常の論理体系が2値論理と呼ばれるのに対し、SQLの論理体系は3値論理(three-valued logic)と呼ばれて区別されます。
では、なぜSQLでは3値論理が採用されているのでしょう。コンピュータが基礎とするブール代数は2値論理ですし、私たちが高校までに習う数学や論理学も2値論理に基づいています。関係モデルの基礎の一つである述語論理も2値論理です。それなのに、なぜリレーショナル・データベースの世界でのみ、この風変わりな体系が用いられているのでしょうか?
その答えは、NULLにあります。リレーショナル・データベースは、NULLを持ち込んだことによって、同時に第3の真理値も持ち込まざるをえなくなりました。3値論理は、たびたびトリッキーで直観に反する振舞いを見せてDBエンジニアを悩ませます。
本稿では、この3値論理について解説すると共に、どのような局面で注意を要するか、具体的なソースコードを例に説明します。テーマの性質上、前半は理論寄りの話が多くなりますが、お付き合いいただきたく思います。理論については既に知っている、または具体例を見ながらの方が理解しやすいという方は、後半の「実践編」から読んで、適宜、理論編を参照するという読み方をしてもらってもかまいません。
対象読者
すべてのDBエンジニア
動作環境
すべてのリレーショナル・データベース
理論編
2つのNULL、3値論理か、それとも4値論理か
3値論理についての話を始めるにあたり、まずNULLについての話から始めたいと思います。なぜなら、NULLこそが3値論理のすべての元凶だからです。
2つのNULLという言葉は、奇異に聞こえるかもしれません。SQLには、NULLは1種類しか存在しないからです。しかしNULLについての議論では、一般的にNULLを2種類に分けて考えます。そのため、最初に基礎知識としてこの区別を説明します。
2種類のNULLとは、「未知(Unknown)」と、「適用不能(Not Applicable, Inapplicable)」です。例えば、サングラスをかけた人の眼の色は分かりません。その人が眼の色を持つことは確かですが、サングラスを外して調べるまでは、何色かは未知です。一方、冷蔵庫の眼の色も分かりません。しかしこちらの場合、色が分からないのはそもそも冷蔵庫に「眼の色」という属性を適用不能だからです。
「冷蔵庫の眼の色」という概念は、「円の体積」や「男性の出産回数」と同じように無意味です。普段、私たちは一口に「分からない」と言いますが、分からなさにもいろいろな種類があるということです。適用不能のNULLは、「不明」というよりは、どちらかと言うと「無意味」に近い概念です。いわば、「未知」という言葉に含まれるニュアンスが「今は分からないけど、条件がそろえばいずれ分かる」というものだとすれば、「適用不能」の方は「どう頑張っても分からない」というものです。
最初にこの分類を行なったのは、関係モデルの創始者E.F.コッドです。彼による「失われた情報」の分類を以下に示します。
コッドはかつて、この2種類のNULLを厳密に区別するべきだと考えて4値論理を提唱したことがあります。幸か不幸か(私は間違いなく「幸」だと思いますが)、このアイデアは一般的な支持を得ず、現在のすべてのDBMSはNULLをひとまとめにして3値論理を採用しています。しかし、この分類自体はとてもうまいものだったので、その後も多くの論者が踏襲しています。
なぜ「= NULL」ではなく「IS NULL」と書かなくてはならないのか?
これは、けっこう疑問に思っている人も多いはずです。SQLを習いたてのころ、ある列がNULLの行を選択しようとして、次のようなクエリを書いて失敗した、という経験は、ほとんどの人が持っているでしょう。
-- NULLの検出に失敗するSQL SELECT * FROM tbl_A WHERE col_1 = NULL;
ご存知のように、このSQLは失敗します。正しくは、「col_1 IS NULL」と書かねばなりません。ちょうどC言語を習い始めのころに「if (hoge = 0)」と書いてしまう間違いとよく似ています。それにしてもなぜ「=」による比較は失敗するのでしょう? 同一性は「=」で表すというのが、私たちが小学校から習ってきた常識のはずです。
それには、もちろん理由があります。それは、NULLに比較述語を適用した結果が、常にunknownになってしまうからです。クエリの結果として選択されるのは、WHERE句の条件評価がtrueになる行のみです。falseやunknownの行は選択されません。等号に限らず、すべての比較述語が同じように動作します。col_1が値である場合もNULLである場合も、おかまいなしに結果はunknownです。
--以下の式は全部 unknown に評価される。 1 = NULL 2 > NULL 3 < NULL 4 <> NULL NULL = NULL
では、なぜNULLに比較述語を適用した結果が絶対に真にならないのでしょう? それは、NULLが値でも変数でもないからです。NULLというのは「そこに値がない」ことを示すただの視覚的マーク、目印に過ぎません。一方、比較述語を適用できるのは値だけです。従って、値ではないNULLに比較述語を適用することは、そもそもナンセンスなのです。
それゆえ、「列の値がNULLである」とか「NULL値」、「ナル値」といった表現も、まったくの誤りです。値ではないので、NULLは定義域(domain)にも含まれません。逆に、NULLを値だと思っている人に訊ねたいのですが、もしNULLが値ならば、その型は一体何でしょう? リレーショナル・データベースで扱われる値はすべて、文字型や数値型など何らかの型を持ちます。仮にNULLが値なら、やはり何らかの型を持たねばなりません。
おそらく、NULLを値と勘違いしやすい理由は2つあります。一つは、C言語などにおいてNULLが1つの定数(多くの処理系では整数0)として定義されているため、それと混同しがちなことです。SQLにおけるNULLと他のプログラミング言語のNULLは、まったくの別物です(参考資料の「初級C言語Q&A」を参照)。
第二の理由は、「IS NULL」という述語が2つの単語から構成されているので、「IS」が述語で「NULL」が値のように見えることです。特にSQLは、「IS TRUE」や「IS FLASE」といった述語も持っているので、それと類比的に考えると、こういう印象を抱くのも無理はありません。しかし『標準SQLガイド 改訂第4版』でも注意が促されているように、「IS NULL」はこれで一つの述語とみなすべきで、従ってむしろ「IS_NULL」と一語で書いた方がふさわしいぐらいです。
unknown、第三の真理値
前段で、いよいよ真理値unknownが登場しました。冒頭でも述べたように、これは、リレーショナル・データベースがNULLを採用したことによって持ち込まれた「第三の真理値」です。ここで1つ、注意してほしいことがあります。それは、真理値のunknownとNULLの一種であるUNKNOWN(未知)は異なるものだということです。前者は真理値型のれっきとした値ですが、後者は値でも変数でもありません。区別しやすいように、前者を太字の小文字でunknown、後者を普通の大文字でUNKNOWNと表記します。両者の違いを理解するには、「x = x」という単純な等式を例に取るのが分かりやすいでしょう。xが真理値のunknownである場合、「x = x」はtrueに評価されます。一方、xがUNKNOWNの場合はunknownに評価されます。
--こっちはれっきとした真理値の比較 unknown = unknown → true --こっちは要するに「NULL = NULL」 UNKNOWN = UNKNOWN → unknown
それでは、SQLが従う3値論理の真理表を見てみましょう。
x | NOT x |
t | f |
u | u |
f | t |
AND | t | u | f |
t | t | u | f |
u | u | u | f |
f | f | f | f |
OR | t | u | f |
t | t | t | t |
u | t | u | u |
f | t | u | f |
網掛けの箇所が、2値論理にはない3値論理特有の演算です。他のSQLの述語はすべて、この3つの論理演算を組み合わせることで作れます。その意味で、このマトリックスは文字通りSQLの母体(matrix)です。
しかし、NOTの場合は単純だからいいとして、ANDとORの組み合わせを全部覚えるのはなかなか大変です。そこで、3つの真理値の間に次のような優先順位があると考えてください。
- ANDの場合: false > unknown > true
- ORの場合 : true > unknown > false
強い方が弱い方を呑み込みます。例えば、「true AND unknown」なら、unknownの方が強いので、結果もunknownになります。ところが、「true OR unknown」の場合、今度はtrueの方が強くなるので、結果はtrueになります。この順位を覚えておけば、3値論理演算も見通しがよくなります。特に、ANDの演算にunknownが含まれた場合、結果が絶対にtrueにならないという特徴をよく覚えておいてください。後でこれが重要なキーになります。
さて、理論の話はこのぐらいにしましょう。次からは、具体的なソースコードを例に、3値論理がどのようにトリッキーな振舞いを見せるかを調べていきます。2値論理に慣れた私たちの直観に反する動作をするため、最初は分かりづらく感じるかもしれません。そのときは、この真理表へ立ち戻って、実際に手を動かして演算を追ってみてください。
それでは、最後に練習問題をどうぞ。
問題:a = 2, b = 5, c = NULLとする。このとき、次の式の真理値はどうなるか。
- a < b AND b > c
- a > b OR b < c
- a < b OR b < c
- NOT ( b <> c)
1.unknown、 2.unknown、 3.true、 4.unknown