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 from 與 is 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 的那三列。