当前位置: 首页 > news >正文

PostgreSQL 中的 NULL 陷阱:从一次排除过滤说起

SQL 中的 NULL 陷阱:从一次排除过滤说起

一、背景:看似简单的需求

在一次数据集成任务中,遇到了这样一个业务过滤需求:

当销售区域为"拉美(LA)"时,需要排除region = 'BR'并且order_status = 'CANCELED'的订单数据。但有一个特别要求:如果这两个字段中任意一个为 NULL,该行数据必须保留。

需求很清晰,但实际写 SQL 时,才发现这里面藏着一个很经典的坑——SQL 的 NULL 三值逻辑


二、最初的写法及其隐患

直觉上,"排除某个组合"会写成:

ANDNOT(region='BR'ANDorder_status='CANCELED')

这段 SQL在大多数场景下跑起来是"正确"的,但它实际上依赖了 SQL NULL 三值逻辑的一个"副作用"来保留 NULL 行——这是隐式的,而非明确表达的意图,属于代码意图不清晰的隐患


三、SQL 的三值逻辑(Three-Valued Logic)

这是理解 NULL 问题的基础。

和编程语言中的布尔两值逻辑(true/false)不同,SQL 采用的是三值逻辑

含义
TRUE条件成立
FALSE条件不成立
UNKNOWN不确定(NULL 参与运算的结果)

核心规则:WHERE 子句只保留结果为TRUE的行,UNKNOWNFALSE都会被过滤。

NULL 参与任何比较运算,结果几乎都是UNKNOWN

NULL='CANCELED'→ UNKNOWNNULL!='CANCELED'→ UNKNOWNNULLANDTRUE→ UNKNOWNNOTNULL→ UNKNOWN

四、NOT (A AND B)遇到 NULL 时的完整分析

还原本文场景,逐行分析:

ANDNOT(region='BR'ANDorder_status='CANCELED')
regionorder_statusA='BR'B='CANCELED'A AND BNOT(A AND B)WHERE 结果
'BR''CANCELED'TRUETRUETRUEFALSE❌ 被排除
'BR''OTHER'TRUEFALSEFALSETRUE✅ 保留
'US''CANCELED'FALSETRUEFALSETRUE✅ 保留
NULL'CANCELED'UNKNOWNTRUEUNKNOWNUNKNOWN⚠️ 被过滤!
'BR'NULLTRUEUNKNOWNUNKNOWNUNKNOWN⚠️ 被过滤!
NULLNULLUNKNOWNUNKNOWNUNKNOWNUNKNOWN⚠️ 被过滤!

⚠️结论:NOT (A AND B)无法保留 NULL!NULL 行因结果是 UNKNOWN 而被悄悄过滤。


五、为什么"跑起来没报错"就以为是对的?

这正是最危险的地方。

如果在数据质量较好的表中,region字段实际上从不出现 NULL(比如它来自一个外键关联,能关联上的必然有值),那这段 SQL 跑起来结果看上去完全正确。

但一旦:

  • 上游数据质量下降,出现 NULL
  • 表结构调整,字段变为可空
  • 换了一张数据较"脏"的表

原本"正确"的 SQL 就会悄无声息地少数据,排查起来极其困难。


六、正确写法:显式声明 NULL 保留

AND(regionISNULLORregion!='BR'ORorder_statusISNULLORorder_status!='CANCELED')

逻辑含义:满足以下任意一个条件就保留这行数据:

  1. region是 NULL
  2. region不等于'BR'
  3. order_status是 NULL
  4. order_status不等于'CANCELED'

唯一被排除的,是同时满足:region = 'BR'order_status = 'CANCELED'(且两者都不为 NULL)。


七、三种写法对比

写法region=NULLorder_status=NULL意图清晰度推荐
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 的行也会被过滤掉WHEREorder_status!='CANCELED'-- ✅ 正确:明确保留 NULLWHEREorder_status!='CANCELED'ORorder_statusISNULL

陷阱 2:NOT IN遇到子查询有 NULL,全部结果为空

-- ❌ 危险:子查询结果中有一个 NULL,整个查询返回空!WHEREorder_idNOTIN(SELECTorder_idFROMblacklist_orders)-- ✅ 安全写法:过滤子查询中的 NULLWHEREorder_idNOTIN(SELECTorder_idFROMblacklist_ordersWHEREorder_idISNOTNULL)-- ✅ 更推荐:用 NOT EXISTS,天然不受 NULL 影响WHERENOTEXISTS(SELECT1FROMblacklist_ordersWHEREblacklist_orders.order_id=t.order_id)

陷阱 3:聚合函数中的 NULL

COUNT(*)-- 统计所有行,NULL 也计入COUNT(order_status)-- 忽略 NULL 行,两者结果可能不同!SUM(amount)-- NULL 行被忽略,不是当 0 处理AVG(amount)-- 分母只统计非 NULL 行,结果可能偏高

九、快速验证字段是否有 NULL

在写过滤条件之前,先查一下字段的 NULL 情况,是一个好习惯:

-- 检查字段 NULL 数量SELECTCOUNT(*)AStotal,COUNT(region)ASregion_not_null,COUNT(*)-COUNT(region)ASregion_null_count,COUNT(order_status)ASstatus_not_null,COUNT(*)-COUNT(order_status)ASstatus_null_countFROMordersWHEREgeo='LA';

十、总结:黄金法则

凡是业务上需要"保留 NULL"或"排除 NULL"的场景,必须用IS NULL/IS NOT NULL显式处理,绝不能依赖三值逻辑的副作用。

记住这三句话:

✅ 显式优于隐式 —— 意图要写清楚,不要靠"副作用" ✅ 先查 NULL 分布 —— 动手写条件前,先确认字段是否可空 ✅ UNKNOWN ≠ FALSE —— NULL 参与运算结果是 UNKNOWN,WHERE 会过滤它

附:本文最终落地的 SQL 写法(MyBatis XML)

<!-- 只在 geo = LA 时追加此过滤条件 --><iftest="geo == 'LA'">AND (region IS NULL OR region != 'BR' OR order_status IS NULL OR order_status != 'CANCELED')</if>

读法:明确排除"region 确实等于 BR 且 order_status 确实等于 CANCELED"的行,其余所有行(包括任意字段为 NULL 的行)一律保留。意图清晰,无歧义,无副作用依赖。


番外:用 COALESCE 能解决吗? 能,比如这样:

<iftest="geo == 'LA'">andnot(COALESCE(region_cd,'')='BR'ANDCOALESCE(order_status,'')='CREDIT NOTE')</if>

为什么本文没有选择 COALESCE?

虽然COALESCE可行,但在本场景中有几个明显缺点:

❌ 缺点 1:占位符存在歧义风险
❌ 缺点 2:索引失效,影响查询性能
❌ 缺点 3:可读性可能不太好

反正合适就好吧!

http://www.jsqmd.com/news/824864/

相关文章:

  • Git 如何检查当前版本是否存在已知安全漏洞 CVE
  • 【NotebookLM物理学研究辅助终极指南】:20年物理计算专家亲授5大高阶用法,90%研究者至今不知
  • BililiveRecorder 直播录制文件修复:3步拯救你的珍贵直播回忆
  • 2026年4月黄金回收技术解析与正规渠道指南:18K金回收/18K金抵押/包包典当/包包回收/包包抵押/奢侈品抵押/选择指南 - 优质品牌商家
  • Taotoken控制台功能详解,从密钥管理到用量分析一站掌握
  • CC2530开发避坑指南:IAR for 8051 10.10.1新建工程到流水灯调试的完整流程
  • 专业实战指南:如何高效应用FUnIE-GAN实现水下图像增强
  • 《UltraEdit 正则表达式实战:从数据清洗到代码重构》
  • Ketcher分子绘图工具完全指南:从零开始掌握化学结构绘制
  • 2026年5月湖北地区知识产权实缴:专业团队如何助力企业优化资本结构? - 2026年企业推荐榜
  • LLM Token用量监控:从成本可视到优化实践
  • STM32H743 FDCAN实战:手把手教你调试CAN节点错误计数器与Bus_Off状态
  • 5大革新点解析:Faze4六轴机械臂从开源设计到工业级应用的实战指南
  • Bebas Neue:为什么这款开源字体让设计师爱不释手?
  • 用Python+Pandas搞定QAR飞行数据清洗:手把手教你从MathorCup赛题数据中提取安全关键项
  • 《企业级 Harness 工程实战:原理与应用》AI Agent领域的“Harness Engineering”(驾驭工程) FDE 前线部署工程师 Forward-Deployed Eng‘r
  • NomNom存档编辑器:解放你的《无人深空》游戏体验终极指南
  • 【STM32+HAL库】---- 模拟SPI实现ST7735s屏幕图形化界面开发
  • 我靠“测试即服务”这个理念,拿下了3个大客户
  • 用STM32F103C8T6驱动Ra-01SC模组:从接线到收发数据的保姆级避坑指南
  • Java-Callgraph2:企业级Java静态调用图分析工具深度解析
  • JavaScript PPT自动化生成终极指南:5分钟从零到专业演示文稿
  • MoocDownloader终极指南:三步轻松下载中国大学MOOC视频课程
  • ML模型监控:构建生产环境模型性能保障体系
  • 保姆级教程:在Qt项目中配置Halcon18.11环境并显示第一张图片
  • 企业费控管理软件系统推荐怎么选?这几个核心问题一定要搞懂 - 资讯速览
  • 终极DeepL Chrome翻译插件完整指南:高效跨语言浏览解决方案
  • Dism++实战指南:Windows系统维护的一站式解决方案
  • 5个专业策略:构建企业级本地漏洞情报分析平台
  • 3d 打印拆分零件