用户问“未取消的订单有多少”,模型生成 status <> 'cancelled',数据库顺利返回两笔,答案却可能漏掉一笔状态未知的订单。问题不一定是模型没找到表,也不一定是关联重复;NULL 会改变比较、筛选和聚合的含义。本文按 PostgreSQL 17 文档展开,并用 SQL Server 文档交叉核对基础逻辑。所有订单与数值均为构造推导,未运行数据库实验。
先区分未知、零和没有这条记录
企业 Text-to-SQL 可靠性要求把业务意图变成可核验的计算约束。空值政策应成为其中一项独立约束:字段可空,不代表任意查询都可以忽略它;业务允许缺失,也不代表缺失可以按零参与经营汇总。这里重点处理选择与统计中的空值,补充已有的查询治理与关联放大方法。
SQL 的 NULL 表达缺失或未知,并不是数值零或普通空字符串;比较可能得到 TRUE、FALSE、UNKNOWN 三种结果。Microsoft 文档明确说明这种三值逻辑,以及用 IS NULL、IS NOT NULL 检查缺失的方法。[1] 本文讨论普通标量比较,不将这些规则不加区分地扩展到所有数据库特殊类型或兼容设置。
构造订单表 orders,每个 order_id 唯一且非空,字段依次为 order_id、customer_id、amount、status。四行是:O1、C1、100、paid;O2、C2、NULL、NULL;O3、NULL、0、open;O4、C3、50、cancelled。amount 采用同币种十进制金额;此处 NULL 不是字符串“NULL”。
还应区分三种来源:原始字段未知、外连接没有匹配行、聚合没有可用输入。它们最终都可能显示为空,却需要不同解释。建议保留来源状态或辅助计数,不依靠一个空白单元格代表所有情形。业务负责人必须决定未知状态是否进入待处理清单,数据团队再落实查询规则。
WHERE 只保留 TRUE,取反不会把未知变成已知
在上述四行上,status <> 'cancelled' 对 O1、O3 为 TRUE,对 O4 为 FALSE,对 O2 为 UNKNOWN。因此筛选结果是 O1、O3。PostgreSQL 文档规定 WHERE 丢弃条件为 FALSE 或 NULL 的行。[4] 两笔结果回答的是“状态已知且不等于 cancelled”,并不自动等于业务想要的全部未取消订单。
若业务明确要求把未知状态纳入待核查集合,可以使用 status <> 'cancelled' OR status IS NULL,结果为 O1、O2、O3,并将 O2 标为状态待确认。若未知状态不得进入正式数量,仍可以返回两笔,但同时报告一笔被单列。两种政策各有用途,关键是不要让 SQL 默认行为替企业作决定。
写 NOT (status = 'cancelled') 不能解决这个问题:UNKNOWN 取反后仍然未知。检查缺失也不能写 status = NULL。可将业务谓词分为已满足、已不满足、未知三组,在展示层说明每组数量;不要只比较一个肯定条件和它的否定条件,然后假设两组一定覆盖全部记录。
同样的问题会出现在金额阈值、审批标记和结束日期上。例如 amount > 0 不会包含金额未知的 O2,也不会包含真实零金额的 O3,但两者被排除的原因不同。若系统要解释异常,应分别返回金额缺失与已知为零,不能将所有未入选对象都称为“不符合业务条件”。
NOT IN 遇到空值,先判断名单到底表达什么
再构造 excluded_customers,仅有 C2 和 NULL 两行。按 customer_id NOT IN (SELECT customer_id FROM excluded_customers) 筛订单,四笔都不会保留:C2 已命中排除项,其余无法证明与名单中未知值不同。PostgreSQL 对 NOT IN 的空值行为有明确说明。[2] 这不是没有订单,而是这个谓词没有对任何订单产生 TRUE。
如果业务意图是排除已知编号的客户,可以采用 NOT EXISTS (SELECT 1 FROM excluded_customers e WHERE e.customer_id = o.customer_id),其中外层订单别名为 o。按这个相等匹配定义,结果为 O1、O3、O4。NOT EXISTS 检查子查询有无匹配行,不会因为名单另一行为空而否定所有已知客户。
但 O3 的客户编号也未知,它在此写法中没有匹配行,因此被保留。若业务要求只接受客户身份已知的订单,还要加 o.customer_id IS NOT NULL,结果才是 O1、O4。这说明 NOT EXISTS 是表达某一种匹配语义的方法,不是无条件正确的替换口诀。完整性不足的名单也不能因为过滤掉 NULL 就被宣布有效。
实施前应选择政策:名单中的空编号是可隔离的数据错误、仅统计已知匹配的边界,还是必须暂停判断的风险信号。对于关键名单,缺失身份可能意味着无法安全确认对象状态,应先修复来源。另需单测空名单:当右侧没有任何行时,NOT IN 的结果为 TRUE,包括左值为空的情形;不能笼统记成“左值为空永远不通过”。[2]
COUNT 和 SUM 的答案,需要附带输入完整性
对未筛选的四笔订单,COUNT(*) 为 4,COUNT(amount) 为 3,SUM(amount) 为 150,AVG(amount) 为 50。后两个结果只使用非空金额,平均值的分母是 3。PostgreSQL 文档分别定义行计数、非空输入计数及这些聚合规则。[3] 150 只能称为已知金额合计;缺失的 O2 金额未获确认时,不能声称它就是完整订单金额。
若先把每个缺失金额补成零,AVG(COALESCE(amount,0)) 将变成 37.5。计算没有报错,却改变了统计对象:从三个已知金额的平均值,变为四笔订单中把未知当零后的平均值。只有业务证据支持零值政策时,这种变化才有解释;为了让报表出现一个数字而补零,不是数据修复。
再看两个边界:仅选 O2 时,行数为 1,非空金额数为 0,SUM(amount) 为 NULL;完全没有选中订单时,在没有 GROUP BY 的标量聚合查询中,行数为 0,SUM(amount) 同样为 NULL。[3] 因而只看 SUM 不能区分有订单但全缺失与根本无订单。带 GROUP BY 的空输入通常没有分组行,也不能与标量聚合的单行结果混为一谈。
建议同时交付 row_count、known_amount_count、missing_amount_count 和 known_amount_sum;本例依次为 4、3、1、150。缺失数可以在同一行集上用 COUNT(*) - COUNT(amount) 求得。是否允许给出完整合计,由指标契约另行判断,必要时返回暂定状态。辅助计数增加少量结果字段,却让模型能基于实际证据说明限制。
LEFT JOIN 保留的是连接阶段,不保证后续筛选仍保留
构造 customers,只有 C1、C2、C3 三行,客户键唯一非空。需求是列出所有客户并显示已支付订单数。若先按客户编号 LEFT JOIN orders,再在 WHERE 写 o.status = 'paid',最终只剩 C1。C2 的匹配订单状态未知,C3 的匹配订单已取消,都被后续筛选排除。外连接与 WHERE 的这种差异来自查询语义。[4]
可以把已支付条件放到连接条件中:customers c LEFT JOIN orders o ON o.customer_id = c.customer_id AND o.status = 'paid'。此时三个客户都保留,再按客户分组用 COUNT(o.order_id),得到 C1 为 1、C2 为 0、C3 为 0。这里 order_id 必须在真实订单行中非空,才能作为是否匹配的计数依据。
在同一连接结果上使用 COUNT(*),C2 和 C3 也各得到 1,因为外连接为未匹配客户产生了一行补空结果。这个 1 是连接输出行数,不是订单数。若右侧真实行的被计数字段本身允许为空,COUNT(该字段) 又会漏掉已匹配记录,因此应选择可靠的非空事实键。
也不能一律给原 WHERE 追加 OR o.order_id IS NULL 作为补丁:C2 和 C3 原本有订单匹配,分别是未知状态与 cancelled,并不会生成新的补空行来挽救这个条件。更重要的是,“已支付订单数为零”不证明“确定没有应付款”;未知状态 O2 仍应在独立的完整性统计中说明。
把空值语义写进生成约束与反例验收
第一步,为可认证指标登记字段是否可空、空值产生路径、排除政策、分母含义和缺失时的输出状态。模型读取这些规则再生成查询;没有批准的政策时,应提出具体澄清或返回带限制的探索结果。不要仅在系统提示中写一句“注意 NULL”,却不提供业务定义。
第二步,静态检查高风险形态:与 NULL 直接比较、可能可空的 NOT IN 子查询、外连接后对右侧列作排除空值的 WHERE 条件,以及统计口径未说明的 COUNT(可空列) 或 COALESCE。它们是审查信号,不能一概禁止,因为某些查询正需要这样的语义;检查器应结合字段约束与指标定义判断。
第三步,固定小型反例数据及预期集合。本例至少核对:未取消且已知为 O1、O3;包含待确认状态为 O1、O2、O3;排除已知客户且要求客户编号非空为 O1、O4;金额完整性为四行中一行未知;全部客户的已支付订单计数为 1、0、0。验收不仅比较总数,也比较 order_id 或 customer_id 集合,防止一条漏掉、一条多算恰好抵消。
第四步,对每个模板加入空表、全空列、零值、空字符串、右侧名单含空、左侧身份为空、外连接未匹配及已匹配但状态为空等变体。在实际目标数据库及部署版本上运行,并核对查询构建器、驱动和展示层是否继续保留 NULL;本文章的构造推导不能代替这一步集成验收。
最终答案应同时说明已知结果、被单列的未知记录和数据截止范围。发现异常时先定位输入字段、谓词还是聚合政策,不能用补零或换一个 SQL 写法掩盖口径问题。可信智能问数既需要正确数字,也需要知道这个数字覆盖了哪些事实、遗漏了哪些尚未确认的信息。
