❄️

SQL における NULL との比較

2021/03/15に公開

こんにちは、Snowflake でサポートエンジニアをやっている @indigo13love です。

この記事では、すべてのサンプルクエリが Snowflake の文法で書かれ Snowflake で実行されていますが、基本的に ANSI SQL 標準に書かれている話をしているので、だいたいどの SQL 実装でも(ANSI SQL 準拠であれば)同じような動きになると思います。


突然ですが、下記のようなテーブルがあったとします。

select c1 from t1;
1
2
NULL

NULL でない行だけを取ってこようと考え、下記のように書くと…

select c1 from t1
where c1 != NULL;

何も結果が返ってきません。

おそらく SQL の経験がある方は、この場合、=!= ではなく IS (NOT) NULL を使わなければならないことを知っているでしょう。

select c1 from t1
where c1 is not NULL;
1
2

しかし、なぜ =!= は NULL との比較に使えないかを考えたことはあるでしょうか。

この記事では、どういうロジックでそうなっているかを確認していきます。

NULL との等価比較では一体何が起きているのか

まずは、!= NULL を条件にしたときに何が返ってくるかを確認しましょう。

!= を使った条件は、単純に真偽値を返す式なので、実は SELECT の式リスト側に書くことができます。

select c1 != NULL from t1;

これの結果を見れば、それぞれの行で c1 != NULL 式がどう評価され、結果として「どの行も返さない」という結論になったかがわかります。

で、結果がどうかというと…

NULL
NULL
NULL

すべての行で NULL が返ってきています。!= は TRUE でも FALSE でもなく NULL を返すことがあることがわかりました。

NULL との比較の特性

等価比較における NULL の特性をもう少し掘り下げていきます。今度は NULL の格納された行を抽出しようとしてみます。

select c1 = NULL from t1;

すると、こちらも全行 NULL が返ってきました。

NULL
NULL
NULL

では、今度は右辺を NULL ではない値で実験してみます。c1 = 1 で 1 と一致する行を確認してみます。

select c1 = 1 from t1;

この場合は 1 の行、 2 の行は正しく(直感的に)それぞれ True と False が返っており、 NULL の行だけ NULL が返ってきています。

TRUE
FALSE
NULL

c1 != 1 でも同様の結果です。

select c1 != 1 from t1;
FALSE
TRUE
NULL

また、大小比較においても同様の結果になります。

select c1 > 1 from t1;
FALSE
TRUE
NULL

つまり、左辺であれ右辺であれ NULL を含む比較はすべて NULL を返すということがわかりました。

3 値論理

実はこの動作は SQL における Boolean 型の正しい動作として ANSI SQL 標準にも定義されており、一般的に「3 値論理 (Three-Valued Logic)」と呼ばれます。

https://docs.snowflake.com/ja/sql-reference/ternary-logic.html

ANSI SQL では、 Boolean 型の返す真理値と、 NULL との比較が返す値を下記のように定義しています。

  1. Boolean 型は True, False, Unknown の 3 種類の真理値を持つ
  2. NULL との比較、 Unknown 真理値との比較は Unknown 真理値を返す
  3. Boolean 型では NULL も Unknown 真理値の表現の一つとしてサポートする

つまり、「NULL を含む比較はすべて NULL を返す」という動作は…

  • NULL を含む比較はすべて Unknown 真理値を返す (定義 2)
  • Unknown 真理値は NULL として表現される (定義 3)
  • 結果として NULL を含む比較はすべて NULL を返す

という意味を持っていたことがわかります。

すなわち、「テーブルに格納されている NULL」 は 「空値としての NULL」 であり、「比較式から返ってきた NULL」 は 「Unknown 真理値としての NULL」 であって、違う意味を持つ値だということがわかりました。

さらに WHERE 句の定義が「検索条件が True に評価される行を結果とする」ことになっているため、常に Unknown (NULL) に評価される NULL との比較は、常に結果行に含まれない形となります。

3 値論理における IS (NOT) NULL の扱い

では、IS NULLIS NOT NULL はなぜ問題なく動作するのでしょうか。

ANSI SQL 標準では比較演算子 (=, !=, <, >) と IS はまったく異なる演算子として定義されています。IS については Boolean 型の定義において AND や OR とともに真理値表として以下のように定義されています。

IS TRUE FALSE UNKNOWN
TRUE TRUE FALSE FALSE
FALSE FALSE TRUE FALSE
UNKNOWN FALSE FALSE TRUE

ここまででいちばん直感的な結果が出てきました。

NULL は前述の通り Unknown 真理値でもあるので、IS NULL はすなわち IS Unknown であることになり、左辺が NULL (Unknown) のときのみ True を、それ以外の場合は False を返す形となります。

NULL を含む列での適切な比較方法

ここまでで NULL の比較がどう動作するのか、そして IS NULL は正しく動作するということがわかりました。 NULL の行(もしくは NULL でない行)を抽出したい場合には、IS (NOT) NULL を使用すればいいでしょう。

しかし、最初の問題はまだ完全には解決していません。「NULL を含む 1 でない行」を抽出したい場合にはどうすればいいでしょうか。

(c1 is NULL) or (c1 != 1) ... これはあまり綺麗でないので避けたいところです。

SQL にはもちろんこれを実現するための方法が用意されています。以下でいくつかの例を紹介したいと思います。

IS (NOT) DISTINCT FROM / EQUAL_NULL()

1 つ目は IS (NOT) DISTINCT FROM です。

https://docs.snowflake.com/ja/sql-reference/functions/is-distinct-from.html

"distinct" とは「まったく別の」「まったく異なる」と言ったような意味の単語で、つまり left IS DISTINCT FROM right は「leftright が異なる値である」、すなわち != に近い意味を持つ句です。

left IS NOT DISTINCT FROM right はその逆で、「leftright が同値である」ことを意味しています。

!=IS DISTINCT FROM の違いは、後者が NULL-safe である、すなわち NULL との比較で Unknown (NULL) を返さず、NULL かどうかをちゃんと判定してくれるという点です。

実際にクエリで確認してみると、確かに 1 と NULL が違う値である (False) と判定できています。

select c1 is distinct from 1 from t1;
FALSE
TRUE
TRUE

したがって、NULL を含む可能性がある (Nullable な) 列との比較で、かつ NULL の行も値の 1 つとして扱いたい場合には、!= ではなく IS DISTINCT FROM を使用することで、適切に処理することができるようになります。

ちなみに、この IS DISTINCT FROM もちゃんと ANSI SQL 標準で "distinct predicate" として定義されている句なのですが、その定義はわりと泥臭く

  • 左辺値も右辺値も NULL だったら False
  • 左辺値が NULL で右辺値が NULL でなかったら True
  • 右辺値が NULL で左辺値が NULL でなかったら True

と 1 つ 1 つパターンを列挙して定義されています。[1]

また、Snowflake では ANSI SQL 標準に存在しない EQUAL_NULL() という関数も用意しており、これも IS NOT DISTINCT FROM と同様に動作します。

https://docs.snowflake.com/ja/sql-reference/functions/equal_null.html

select not equal_null(c1, 1) from t1;
FALSE
TRUE
TRUE

IFNULL() / COALESCE()

もう 1 つは IFNULL()COALESCE() といった NULL を別の NULL ではない値に変換する関数を使う方法です。

https://docs.snowflake.com/ja/sql-reference/functions/ifnull.html

https://docs.snowflake.com/ja/sql-reference/functions/coalesce.html

IFNULL() は引数に 2 つの式を取る関数で、1 つ目の引数が NULL だったら 2 つ目の引数を、そうでなければ 1 つ目の引数を返します。

下記の例では IFNULL(C1, 0) で c1 が NULL のときのみ 0 に変換することで、1 との比較が != を使っていても適切に処理できています。

select ifnull(c1, 0) != 1 from t1;
FALSE
TRUE
TRUE

COALESCE() も同じような動きをする関数ですが、複数の(可変長の)引数を取ることができ、最初に NULL でなかった値を返します。

select coalesce(c1, 0) != 1 from t1;
FALSE
TRUE
TRUE

今回の例では IFNULL() との違いがわからないかと思いますが、例えば「A 列が NULL だったら B 列、B 列も NULL だったら文字列リテラル 'foo' を使う」といったような処理を COALESCE(A, B, 'foo') のようにシンプルに記述することができます。

IS DISTINCT FROM と比較しての利点としては、比較以外の操作、例えば文字列操作・集約・結合などに NULL を含み得る列を使用する場合に、考慮点が少なく済み、期待通りの動作を実現するのが簡単になります。

逆に、値を変換するにあたっての処理コストはどうしてもかかってしまうため、シチュエーションに応じて使い分けられるといいでしょう。

脚注
  1. 実際はそれ以降も、普通の値だったら、ユーザ定義型だったら…といろいろな細かいケースの動作が 1 つ 1 つ定義されています。 ↩︎

Discussion