各个方面叨叨叨

PostgreSQL 的 NOT IN 查不到数据,先看看名单里有没有 NULL

排除名单里混入 NULL,NOT IN 可能把本该留下的记录也过滤掉。用一组可复现查询看清原因,再按业务规则选择 NOT EXISTS 或过滤空值。

文章目录

查“没有付款的客户”时,明明有未付款记录,结果却一行也没有。先别怀疑索引:如果右侧名单里出现了 NULL,NOT IN 的结果可能是未知值,WHERE 就不会保留这行。

用三条记录把问题缩小

下面的查询不需要建表,可以直接放进 PostgreSQL 查询窗口。客户编号和付款名单是演示数据。

WITH customers(id) AS (VALUES (1), (2), (3)),
     paid(customer_id) AS (VALUES (1), (NULL::integer))
SELECT id
FROM customers
WHERE id NOT IN (SELECT customer_id FROM paid);

这条查询没有返回客户 2、3。对编号 2 来说,判断大致相当于“2 不等于 1,而且 2 不等于 NULL”。第二项无法确定,整个条件就不能被当作 true。NULL 表示缺少值,不能拿它当作一个普通的特殊编号。

可以单独看这个差别:

SELECT
  2 NOT IN (1, NULL) AS unknown_result,
  2 NOT IN (1, 3) AS true_result;

第一列是 NULL,第二列是 true。客户端可能把 NULL 显示成空白;空白不是 false。WHERE 只保留条件为 true 的行。

如果意思是“没有匹配付款记录”

用 NOT EXISTS 把业务关系写出来:

WITH customers(id) AS (VALUES (1), (2), (3)),
     paid(customer_id) AS (VALUES (1), (NULL::integer))
SELECT c.id
FROM customers AS c
WHERE NOT EXISTS (
  SELECT 1
  FROM paid AS p
  WHERE p.customer_id = c.id
);

这次会保留 2、3。付款名单中的 NULL 没有匹配任何非空客户编号,因此不会阻止它们出现。这个写法强调“找不到匹配行”,也适合按客户编号、租户编号等多个条件一起判断。

如果继续用 NOT IN,可以在子查询里加 WHERE customer_id IS NOT NULL。两种写法都应先确认左侧字段的业务规则:左侧编号本身为 NULL 时,NOT IN 与普通等值关联的 NOT EXISTS 可能给出不同结果。不能在没看数据的情况下全库替换。

空值到底代表什么

付款记录的客户编号为空,可能是导入错误,也可能是允许保留的未关联流水。前者应进入异常处理,后者不应自动被解释成“所有客户都付过款”。若客户编号是必填关系,检查约束和导入流程,通常比在每条报表 SQL 后面补条件更可靠。

上线前用已付款、未付款、付款名单含 NULL、名单为空四种输入核对结果。若左侧允许空值,再加一组空编号样本,并写清它是否应该被统计。先证明筛选结果符合业务,再用执行计划比较性能。

PostgreSQL 子查询文档说明了 NOT IN 和 EXISTS 的空值行为。需要进一步分析计划时,可接着看本站的 EXPLAIN 阅读方法。

继续阅读 · 数据库 返回顶部