PostgreSQL中NULL陷阱的排除過濾指南
一、背景:看似簡單的需求
在一次數(shù)據(jù)集成任務(wù)中,遇到了這樣一個(gè)業(yè)務(wù)過濾需求:
當(dāng)銷售區(qū)域?yàn)?quot;拉美(LA)"時(shí),需要排除 region = 'BR' 并且 order_status = 'CANCELED' 的訂單數(shù)據(jù)。但有一個(gè)特別要求:如果這兩個(gè)字段中任意一個(gè)為 NULL,該行數(shù)據(jù)必須保留。
需求很清晰,但實(shí)際寫 SQL 時(shí),才發(fā)現(xiàn)這里面藏著一個(gè)很經(jīng)典的坑——SQL 的 NULL 三值邏輯。
二、最初的寫法及其隱患
直覺上,"排除某個(gè)組合"會寫成:
AND NOT (region = 'BR' AND order_status = 'CANCELED')
這段 SQL 在大多數(shù)場景下跑起來是"正確"的,但它實(shí)際上依賴了 SQL NULL 三值邏輯的一個(gè)"副作用"來保留 NULL 行——這是隱式的,而非明確表達(dá)的意圖,屬于代碼意圖不清晰的隱患。
三、SQL 的三值邏輯(Three-Valued Logic)
這是理解 NULL 問題的基礎(chǔ)。
和編程語言中的布爾兩值邏輯(true / false)不同,SQL 采用的是三值邏輯:
| 值 | 含義 |
|---|---|
TRUE | 條件成立 |
FALSE | 條件不成立 |
UNKNOWN | 不確定(NULL 參與運(yùn)算的結(jié)果) |
核心規(guī)則:WHERE 子句只保留結(jié)果為 TRUE 的行,UNKNOWN 和 FALSE 都會被過濾。
NULL 參與任何比較運(yùn)算,結(jié)果幾乎都是 UNKNOWN:
NULL = 'CANCELED' → UNKNOWN NULL != 'CANCELED' → UNKNOWN NULL AND TRUE → UNKNOWN NOT NULL → UNKNOWN
四、NOT (A AND B)遇到 NULL 時(shí)的完整分析
還原本文場景,逐行分析:
AND NOT (region = 'BR' AND order_status = 'CANCELED')
| region | order_status | A='BR' | B='CANCELED' | A AND B | NOT(A AND B) | WHERE 結(jié)果 |
|---|---|---|---|---|---|---|
'BR' | 'CANCELED' | TRUE | TRUE | TRUE | FALSE | ? 被排除 |
'BR' | 'OTHER' | TRUE | FALSE | FALSE | TRUE | ? 保留 |
'US' | 'CANCELED' | FALSE | TRUE | FALSE | TRUE | ? 保留 |
NULL | 'CANCELED' | UNKNOWN | TRUE | UNKNOWN | UNKNOWN | ?? 被過濾! |
'BR' | NULL | TRUE | UNKNOWN | UNKNOWN | UNKNOWN | ?? 被過濾! |
NULL | NULL | UNKNOWN | UNKNOWN | UNKNOWN | UNKNOWN | ?? 被過濾! |
結(jié)論:NOT (A AND B) 無法保留 NULL!NULL 行因結(jié)果是 UNKNOWN 而被悄悄過濾。
五、為什么"跑起來沒報(bào)錯"就以為是對的?
這正是最危險(xiǎn)的地方。
如果在數(shù)據(jù)質(zhì)量較好的表中,region 字段實(shí)際上從不出現(xiàn) NULL(比如它來自一個(gè)外鍵關(guān)聯(lián),能關(guān)聯(lián)上的必然有值),那這段 SQL 跑起來結(jié)果看上去完全正確。
但一旦:
- 上游數(shù)據(jù)質(zhì)量下降,出現(xiàn) NULL
- 表結(jié)構(gòu)調(diào)整,字段變?yōu)榭煽?/li>
- 換了一張數(shù)據(jù)較"臟"的表
原本"正確"的 SQL 就會悄無聲息地少數(shù)據(jù),排查起來極其困難。
六、正確寫法:顯式聲明 NULL 保留
AND (region IS NULL OR region != 'BR' OR order_status IS NULL OR order_status != 'CANCELED')
邏輯含義:滿足以下任意一個(gè)條件就保留這行數(shù)據(jù):
region是 NULLregion不等于'BR'order_status是 NULLorder_status不等于'CANCELED'
唯一被排除的,是同時(shí)滿足:region = 'BR' 且 order_status = 'CANCELED'(且兩者都不為 NULL)。
七、三種寫法對比
| 寫法 | region=NULL 時(shí) | order_status=NULL 時(shí) | 意圖清晰度 | 推薦 |
|---|---|---|---|---|
NOT (A AND B) | ?? 隱式過濾 | ?? 隱式過濾 | ? 差 | ? |
A IS NULL OR A!='BR' OR B IS NULL OR B!='CANCELED' | ? 明確保留 | ? 明確保留 | ? 好 | ? |
NOT (A='BR' AND B='CANCELED' AND A IS NOT NULL AND B IS NOT NULL) | ? 明確保留 | ? 明確保留 | 一般 | 可接受 |
八、延伸:其他高頻 NULL 陷阱
陷阱 1:!=不等于不能過濾 NULL
-- ? 錯誤:order_status 是 NULL 的行也會被過濾掉 WHERE order_status != 'CANCELED' -- ? 正確:明確保留 NULL WHERE order_status != 'CANCELED' OR order_status IS NULL
陷阱 2:NOT IN遇到子查詢有 NULL,全部結(jié)果為空
-- ? 危險(xiǎn):子查詢結(jié)果中有一個(gè) NULL,整個(gè)查詢返回空!
WHERE order_id NOT IN (SELECT order_id FROM blacklist_orders)
-- ? 安全寫法:過濾子查詢中的 NULL
WHERE order_id NOT IN (
SELECT order_id FROM blacklist_orders WHERE order_id IS NOT NULL
)
-- ? 更推薦:用 NOT EXISTS,天然不受 NULL 影響
WHERE NOT EXISTS (
SELECT 1 FROM blacklist_orders WHERE blacklist_orders.order_id = t.order_id
)
陷阱 3:聚合函數(shù)中的 NULL
COUNT(*) -- 統(tǒng)計(jì)所有行,NULL 也計(jì)入 COUNT(order_status) -- 忽略 NULL 行,兩者結(jié)果可能不同! SUM(amount) -- NULL 行被忽略,不是當(dāng) 0 處理 AVG(amount) -- 分母只統(tǒng)計(jì)非 NULL 行,結(jié)果可能偏高
九、快速驗(yàn)證字段是否有 NULL
在寫過濾條件之前,先查一下字段的 NULL 情況,是一個(gè)好習(xí)慣:
-- 檢查字段 NULL 數(shù)量
SELECT
COUNT(*) AS total,
COUNT(region) AS region_not_null,
COUNT(*) - COUNT(region) AS region_null_count,
COUNT(order_status) AS status_not_null,
COUNT(*) - COUNT(order_status) AS status_null_count
FROM orders
WHERE geo = 'LA';
十、總結(jié):黃金法則
凡是業(yè)務(wù)上需要"保留 NULL"或"排除 NULL"的場景,必須用 IS NULL / IS NOT NULL 顯式處理,絕不能依賴三值邏輯的副作用。
記住這三句話:
? 顯式優(yōu)于隱式 —— 意圖要寫清楚,不要靠"副作用" ? 先查 NULL 分布 —— 動手寫條件前,先確認(rèn)字段是否可空 ? UNKNOWN ≠ FALSE —— NULL 參與運(yùn)算結(jié)果是 UNKNOWN,WHERE 會過濾它
附:本文最終落地的 SQL 寫法(MyBatis XML)
<!-- 只在 geo = LA 時(shí)追加此過濾條件 -->
<if test="geo == 'LA'">
AND (region IS NULL OR region != 'BR'
OR order_status IS NULL OR order_status != 'CANCELED')
</if>
讀法:明確排除"region 確實(shí)等于 BR 且 order_status 確實(shí)等于 CANCELED"的行,其余所有行(包括任意字段為 NULL 的行)一律保留。意圖清晰,無歧義,無副作用依賴。
番外:用 COALESCE 能解決嗎? 能,比如這樣:
<if test="geo == 'LA'">
and not (COALESCE(region_cd,'') = 'BR' AND COALESCE (order_status,'') = 'CREDIT NOTE')
</if>
為什么本文沒有選擇 COALESCE?
雖然 COALESCE 可行,但在本場景中有幾個(gè)明顯缺點(diǎn):
? 缺點(diǎn) 1:占位符存在歧義風(fēng)險(xiǎn)
? 缺點(diǎn) 2:索引失效,影響查詢性能
? 缺點(diǎn) 3:可讀性可能不太好
反正合適就好吧!
以上就是PostgreSQL中NULL陷阱的排除過濾指南的詳細(xì)內(nèi)容,更多關(guān)于PostgreSQL NULL陷阱排除過濾的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
PostgreSQL流復(fù)制(主從復(fù)制)詳細(xì)教程
本文詳細(xì)介紹了PostgreSQL流復(fù)制技術(shù),流復(fù)制通過WAL日志實(shí)時(shí)同步主從庫數(shù)據(jù),支持異步和同步兩種模式,具有一定的參考價(jià)值,感興趣的可以了解一下2025-11-11
postgresql 中position函數(shù)的性能詳解
這篇文章主要介紹了postgresql 中position函數(shù)的性能詳解,具有很好的參考價(jià)值,希望對大家有所幫助。一起跟隨小編過來看看吧2021-02-02
詳解如何定位postgreSQL數(shù)據(jù)庫中未被使用過的索引
在生產(chǎn)環(huán)境上,由于不規(guī)范的優(yōu)化措施,數(shù)據(jù)庫中可能存在大量的索引,并且相當(dāng)一部分的索引重未被使用過,今天帶大家如何找出這些索引,本文給大家介紹了定位postgreSQL數(shù)據(jù)庫中未被使用過的索引的方法,需要的朋友可以參考下2024-03-03
使用Postgresql 實(shí)現(xiàn)快速插入測試數(shù)據(jù)
這篇文章主要介紹了使用Postgresql 實(shí)現(xiàn)快速插入測試數(shù)據(jù),具有很好的參考價(jià)值,希望對大家有所幫助。一起跟隨小編過來看看吧2021-01-01
postgresql 將逗號分隔的字符串轉(zhuǎn)為多行的實(shí)例
這篇文章主要介紹了postgresql 將逗號分隔的字符串轉(zhuǎn)為多行的實(shí)例,具有很好的參考價(jià)值,希望對大家有所幫助。一起跟隨小編過來看看吧2021-02-02
postgresql關(guān)于like%xxx%的優(yōu)化操作
這篇文章主要介紹了postgresql關(guān)于like%xxx%的優(yōu)化操作,具有很好的參考價(jià)值,希望對大家有所幫助。一起跟隨小編過來看看吧2021-01-01
Postgresql 查看SQL語句執(zhí)行效率的操作
這篇文章主要介紹了Postgresql 查看SQL語句執(zhí)行效率的操作,具有很好的參考價(jià)值,希望對大家有所幫助。一起跟隨小編過來看看吧2021-02-02

