SQL 出身關聯理論,帶著一個在一般程式語言裡找不到對應物的特殊值:null。Python 有 None、PHP 有 null、C 有 nil,幾乎每個語言都有長得像 null 的東西——但 SQL 的 null 不一樣。

三值邏輯#

SQL 的 null 引入了三值邏輯(three-valued logic),與其他語言的 None/Null 差異最明顯的地方在於值的比較。看看 SQL 的 null 真值表:

select a::text, b::text,
       (a=b)::text as "a=b",
       format('%s = %s',
               coalesce(a::text, 'null'),
              coalesce(b::text, 'null')) as op,
       format('is %s',
              coalesce((a=b)::text, 'null')) as result
   from (values(true), (false), (null)) v1(a)
        cross join
       (values(true), (false), (null)) v2(b);

cross join 非常適合產生真值表:它對欄位做笛卡兒積,把 a 的每個值依序配上 b 的每個值。coalesce 函式回傳第一個非 null 的參數(所有參數必須同型別),這裡與 format 搭配讓結果更易讀:

   a   │   b   │  a=b  │      op       │ result
═══════╪═══════╪═══════╪═══════════════╪══════════
 true  │ true  │ true  │ true = true   │ is true
 true  │ false │ false │ true = false  │ is false
 true  │ ¤     │ ¤     │ true = null   │ is null
 false │ true  │ false │ false = true  │ is false
 false │ false │ true  │ false = false │ is true
 false │ ¤     │ ¤     │ false = null  │ is null
 ¤     │ true  │ ¤     │ null = true   │ is null
 ¤     │ false │ ¤     │ null = false  │ is null
 ¤     │ ¤     │ ¤     │ null = null   │ is null
(9 rows)

把 null 理解為「我不知道這是什麼」,而不是「這裡沒有值」。想像左手 A 藏著一個你不知道是什麼的東西,右手 B 也藏著一個你不知道是什麼的東西——問你 A 和 B 是不是同一個東西?你無從知道。所以 SQL 中 null = null 回傳 null:這是對問題的正確回答,卻不一定是你預期、或能讓查詢得到預期結果集的答案。

因此 SQL 提供了處理可能為 null 的資料的其他運算子:is distinct fromis not distinct from。它們名字很長,並且「假裝」null 等於 null——如果你想當作 SQL 沒有三值邏輯,就用這兩個運算子,忘掉布林比較會回傳 null 這回事。

is distinct from 真值表
select a::text as left, b::text as right,
       (a = b)::text as "=",
       (a <> b)::text as "<>",
       (a is distinct from b)::text as "is distinct",
       (a is not distinct from b)::text as "is not distinct from"
  from            (values(true),(false),(null)) t1(a)
       cross join (values(true),(false),(null)) t2(b);
 left  │ right │   =   │  <>   │ is distinct │ is not distinct from
═══════╪═══════╪═══════╪═══════╪═════════════╪══════════════════════
 true  │ true  │ true  │ false │ false       │ true
 true  │ false │ false │ true  │ true        │ false
 true  │ ¤     │ ¤     │ ¤     │ true        │ false
 false │ true  │ false │ true  │ true        │ false
 false │ false │ true  │ false │ false       │ true
 false │ ¤     │ ¤     │ ¤     │ true        │ false
 ¤     │ true  │ ¤     │ ¤     │ true        │ false
 ¤     │ false │ ¤     │ ¤     │ true        │ false
 ¤     │ ¤     │ ¤     │ ¤     │ false       │ true
(9 rows)

最後兩欄完全沒有 null。

Not Null 約束#

有時資料模型需要強力保證某欄位不能是 null——通常是因為應用程式處理「未知」毫無意義,也就是這是個必填值。

任何欄位的預設值(除非另外指定)都是 null。但預設值只是預設值,不是約束,應用程式仍可能對有非 null 預設值的欄位插入 null:

create table test(id serial, f1 text default 'unknown');
insert into test(f1) values(DEFAULT),(NULL),('foo');
table test;
 id │   f1
════╪═════════
  1 │ unknown
  2 │ ¤
  3 │ foo

儘管設定了特定預設值,表裡還是出現了 null。避免的方法是加上 not null 約束

drop table test;
create table test(id serial, f1 text not null default 'unknown');
insert into test(f1) values(DEFAULT),(NULL),('foo');
ERROR:  null value in column "f1" violates not-null constraint
DETAIL:  Failing row contains (2, null).

這次 insert 失敗了:接受這筆資料會違反建表時指定的約束。

Outer Join 帶來的 NULL#

如前所見,outer join 會保留參考關聯的所有列,並在 join 條件成立時補上外側關聯的欄位;條件不成立時,外側關聯的欄位就以 null 填入

典型例子是日曆日期上尚未登錄資料的情況。在賽車資料庫中查詢桿位(pole position)車手及其最終名次——由於比賽會提早登錄,有些還沒開賽,資料庫裡自然沒有賽果:

  select races.date,
         races.name,
         drivers.surname as pole_position,
         results.position
    from races
         /*
          * We want only the pole position from the races
          * know the result of and still list the race when
          * we don't know the results.
          */
         left join results
                on races.raceid = results.raceid
               and results.grid = 1
         left join drivers using(driverid)
   where     date >= '2017-05-01'
         and date < '2017-08-01'
order by races.date;
    date    │         name          │ pole_position │ position
════════════╪═══════════════════════╪═══════════════╪══════════
 2017-05-14 │ Spanish Grand Prix    │ Hamilton      │        1
 2017-05-28 │ Monaco Grand Prix     │ Räikkönen     │        2
 2017-06-11 │ Canadian Grand Prix   │ Hamilton      │        1
 2017-06-25 │ Azerbaijan Grand Prix │ ¤             │        ¤
 2017-07-09 │ Austrian Grand Prix   │ ¤             │        ¤
 2017-07-16 │ British Grand Prix    │ ¤             │        ¤
 2017-07-30 │ Hungarian Grand Prix  │ ¤             │        ¤
(7 rows)

即使 results.grid 在資料模型中有 not null 約束,有時就是根本沒有那筆資料——換句話說,我們不知道查詢的答案,SQL 便在答案中使用 null。

null 值可能由查詢本身產生。基本上你無法逃避處理 null,應用程式必須為它做好準備,並且清楚知道該拿它怎麼辦。

在應用程式中使用 NULL#

多數程式語言都有「未知/尚未初始化」狀態的表示法:Python 的 None、Java/C/PHP 的 null(語義各異),甚至 OCaml 的 option 型別、Haskell 的 maybe 型別。依你選用的工具,SQL 的 null 大多能直接對應到這些概念。要記住兩件事:

  • 結果集裡可能出現 null,程式碼要據此撰寫。
  • 寫 SQL 時謹記三值邏輯的語義:想表達「是 null」就寫 where foo is null,而不是錯誤的 where foo = null——因為 null = null 是 null,該列不會被選進結果集。
select a, b
  from (values(true), (false), (null)) v1(a)
       cross join
       (values(true), (false), (null)) v2(b)
where a = null;

這什麼都查不到(0 rows)——沒有任何列「等於」null。換成正確的問法:

select a, b
  from (values(true), (false), (null)) v1(a)
       cross join
       (values(true), (false), (null)) v2(b)
where a is null;

就能得到 a 為 null 的那三列。