SQLは3値論理、NULLの扱いに気を付けよう

基本的なことだけど、久しぶりにハマって2時間くらい溶かしてしまったので、自分用メモ。大体のことは以下の本に書いてある。

INとNULL

普通のプログラミング言語であれば、真偽値(BOOLEAN)はTRUE or FALSEの二通り。しかし、SQLの世界ではNULLが入ってくるため話がややこしくなる。いくつか例があるが、排中律が成立しないことが挙げられる。具体的なクエリとしては以下のようなもの。

SELECT
  1 IN (1) AS a,
  2 IN (1) AS b,
  2 NOT IN (1) AS c,
  1 IN (1, NULL) AS d,
  2 IN (1, NULL) AS e,
  2 NOT IN (1, NULL) AS f

eとfがやっかいで、同一の条件でINNOT INを書いているので、どちらかがTRUEでどちらかがFALSEを期待してしまうが、結果はそんなことはなくてどちらもNULLになる。INを同値変換すると=ORを使った形(1 = 2 OR 1 = NULL)にできるが、変換された後ではIS NULLを使って比較をしていないため、こういったことが起きる。

f:id:syou6162:20220216175924p:plain
nullになるケースが出てきてしまう

INに与えているリストの中に「一つでも」NULLが入っていると、結果がNULLになってしまう。この例は単純なのですぐに気付けるが、クエリをあれこれ組み合わせていると結構気付けなかったりする。

今回はまったケース

で、はまったケースがこちら(例なので、小さいサンプル)。でかいデータ(data)というものがあって、それぞれのカラムはNULLにならないと私は思っていた。このデータを適当にGROUP BYして、各行に対して文字列結合した結果にINを使った結果、TRUEでもFALSEでもなくNULLが登場してしまう。GROUP BYした結果が数万件単位あって、目視で見たところ異常なさそうと判断したのがよくなかった。

WITH
  data AS (
  SELECT
    "a" AS a,
    "b" AS b,
  UNION ALL
  SELECT
    NULL AS a,
    "bb" AS b,
  UNION ALL
  SELECT
    "aa" AS a,
    NULL AS b,
  UNION ALL
  SELECT
    NULL AS a,
    NULL AS b,
    ),
  tmp AS (
  SELECT
    a,
    b
  FROM
    data
  GROUP BY
    a,
    b )
SELECT
  "a:b" IN (SELECT a || ":" || b FROM tmp) AS x,
  "aa:b" IN (SELECT a || ":" || b FROM tmp) AS y,
  "aa:b" NOT IN (SELECT a || ":" || b FROM tmp) AS z

f:id:syou6162:20220216181059p:plain
再びNULLが登場

敗因としては元データにNULLがないと勝手に断定したことが原因で、これを防ぐためには

  • 元データにNULLが入るケースがあるか確認する
    • dbtのsourceのテストとかでも何でも
  • GROUP BYの条件にIS NOT NULLを忘れずに付けていく
  • coalesce関数などを使って、NULLの場合には他の値を入れていく

とかだろうか。

怖い例

古いテーブルを定期的に削除したいが、削除してはいけないテーブルがあって、それらはホワイトリスト形式で管理されている。削除候補対象に対してNOT IN ホワイトリストのようなことをやっていると、削除が全然されなかったり、(消してはいけないテーブルも含めて)全削除されてしまう、ということもあり得る。特に後者はやばいので、ホワイトリストの中にNULLが入っていないとか、削除候補と実際に削除する件数が一致していないか(全削除になっていないか)など、防衛的に振る舞うなどしたほうがよい*1

所感

こういうのを人間がやるの、まあ間違えないほうが難しいよなって気がするので、型付きSQLみたいなのが出てきて欲しい。この結果はBOOLEANじゃなくてOPTIONAL<BOOLEAN>みたいなやつ。BigQueryだとカラムのSchema(モード)にNon-NULLを定義することはできるけど、SQLレベルでそれが判断したい。SQLはきっと10年後も生き残っているツールなんだろうけど、10年後も人間がこんなところで悩んでいる未来にはいたくないな...。調べればきっとすでにアイディアとしてはあるんだろうけど、まあ流行らなかったんだろうなぁ。以上、愚痴でした。

*1:やらかしたわけではないです