BOOL_AND / BOOL_OR / EVERY
「这一批数据是不是全部合格」「有没有任何一条异常」——这类判断在报表与数据校验里无处不在。PostgreSQL 提供两个布尔聚合 bool_and / bool_or,以及一个标准 SQL 别名 every。它们语法极简,但有两个必须记住的语义:every 就是 bool_and 的别名(不是「全部不同」,也不是「每个」),以及全 NULL 的组返回 NULL 而不是 true——后者会让「校验通过」的报表在空数据上显示空白,甚至被误当成通过。本节把这两个函数以及它们与 count(*) / EXISTS 的分工讲清楚。
前置知识
阅读本节前,建议先了解:STRING_AGG / ARRAY_AGG / JSONB_AGG
基础概念
| 概念 | 术语 | 说明 |
|---|---|---|
| 布尔聚合 | Boolean Aggregate | 把一列布尔值归约为单个布尔值 |
| 逻辑与聚合 | bool_and | 全部为真才返回真,等价于许多行的 AND |
| 逻辑或聚合 | bool_or | 有一个为真就返回真,等价于许多行的 OR |
| 标准别名 | every | SQL 标准里 bool_and 的名字,两者完全等价 |
| 全称量词 | Universal Quantifier | 「对所有行成立」,对应 bool_and |
| 存在量词 | Existential Quantifier | 「存在某行成立」,对应 bool_or |
| 三值逻辑 | Three-Valued Logic | true / false / NULL,聚合后仍遵守 |
| 真空真 | Vacuous Truth | 空集上「全部满足」逻辑上为真,但 PostgreSQL 返回 NULL |
两个函数与三值逻辑的完整真值表(按组内行的布尔取值组合):
| 组内包含 | bool_and | bool_or | 说明 |
|---|---|---|---|
全部 true | true | true | — |
true + false | false | true | 与 / 或的基本行为 |
全部 false | false | false | — |
true + NULL | true | true | NULL 被忽略 |
false + NULL | false | false | NULL 被忽略 |
全部 NULL | NULL | NULL | 关键陷阱,非 true 也非 false |
| 空组(无行) | NULL | NULL | 只能靠 coalesce 决定默认口径 |
bool_and bool_or
┌───────────────────────────┐ ┌───────────────────────────┐
│ 只看非 NULL 值 │ │ 只看非 NULL 值 │
│ 全真 → true │ │ 有真 → true │
│ 有一个假 → false │ │ 全假 → false │
│ 没有非 NULL 值 → NULL ⚠️ │ │ 没有非 NULL 值 → NULL ⚠️ │
└───────────────────────────┘ └───────────────────────────┘一句话记忆:bool_and / bool_or 只对非 NULL 值做出判断;一旦组内没有任何非 NULL 值,它们无法判断,只能返回 NULL。
语法与用法
-- 1) 两个基本函数
bool_and(expression boolean) → boolean
bool_or(expression boolean) → boolean
-- 2) SQL 标准别名(完全等价,没有额外行为)
every(expression boolean) → boolean -- ≡ bool_and
-- 3) 与 FILTER 组合(PostgreSQL 9.4+)
bool_and(expr) FILTER (WHERE 条件)
-- 4) 参数是布尔表达式时可直接传
bool_and(amount > 0)
bool_or(status = 'failed')
every是bool_and的别名,不是「每个都不同」。这个误解相当常见:every(x)与count(DISTINCT x) = count(x)毫无关系,后者才表达「全部互不相同」。every表达的是「对全部行都成立」。
最小可运行示例:
WITH t(v) AS (
VALUES (true), (true), (false), (NULL)
)
SELECT
bool_and(v) AS 全部为真,
bool_or(v) AS 存在为真,
every(v) AS every结果,
count(*) AS 总行数,
count(v) AS 非NULL行数
FROM t; 全部为真 | 存在为真 | every结果 | 总行数 | 非NULL行数
----------+----------+-----------+--------+------------
f | t | f | 4 | 3
(1 row)bool_and 为 f 是因为组内有一个 false;bool_or 为 t 是因为有 true;every 的结果与 bool_and 逐字相同——这就是别名的证据。注意 count(v) = 3 而 count(*) = 4,NULL 行确实存在但被两个布尔聚合忽略了。
准备本节示例数据:
DROP TABLE IF EXISTS order_items;
DROP TABLE IF EXISTS orders;
CREATE TABLE orders (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
order_no TEXT NOT NULL UNIQUE,
status TEXT NOT NULL CHECK (status IN ('pending', 'paid', 'shipped', 'done', 'canceled')),
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE TABLE order_items (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
order_id BIGINT NOT NULL REFERENCES orders(id) ON DELETE CASCADE,
sku TEXT NOT NULL,
qty INTEGER NOT NULL CHECK (qty > 0),
is_paid BOOLEAN, -- 允许 NULL:尚未对账
qc_passed BOOLEAN -- 允许 NULL:尚未质检
);
INSERT INTO orders (order_no, status) VALUES
('SO-2001', 'done'),
('SO-2002', 'paid'),
('SO-2003', 'pending');
INSERT INTO order_items (order_id, sku, qty, is_paid, qc_passed) VALUES
-- SO-2001:全部已付款、全部质检通过
(1, 'A-0001', 1, true, true),
(1, 'A-0002', 2, true, true),
-- SO-2002:一条未付款,质检有 NULL
(2, 'B-0001', 1, true, true),
(2, 'B-0002', 3, false, NULL),
-- SO-2003:全部未付款,质检全 NULL
(3, 'C-0001', 1, false, NULL),
(3, 'C-0002', 1, false, NULL);详细说明
every 是 bool_and 的别名
这是最需要先钉死的一点。every 来自 SQL 标准,PostgreSQL 把它实现为 bool_and 的别名,两者在解析后是同一个函数:
-- 用 pg_proc 验证两者指向同一个实现
SELECT
p1.proname AS 名称,
p2.proname AS 别名指向,
p1.oid = p2.oid AS 是否同一函数
FROM pg_proc p1
JOIN pg_proc p2 ON p2.oid = p1.oid
WHERE p1.proname = 'bool_and'; 名称 | 别名指向 | 是否同一函数
----------+----------+--------------
bool_and | bool_and | t
(1 row)-- 更直接的等价性验证:两者在任意数据上结果都一致
WITH t(v) AS (VALUES (true), (false), (true))
SELECT bool_and(v) AS b, every(v) AS e, bool_and(v) IS NOT DISTINCT FROM every(v) AS 等价
FROM t; b | e | 等价
---+---+------
f | f | t
(1 row)常见的误用是把 every 读成「每一个都不同」:
| 想表达的业务含义 | 正确写法 | 错误写法 |
|---|---|---|
| 全部行都满足条件 | bool_and(cond) / every(cond) | — |
| 存在行满足条件 | bool_or(cond) | — |
| 全部行的值互不相同 | count(*) = count(DISTINCT x) | every(x) |
| 每行都非 NULL | bool_and(x IS NOT NULL) | — |
-- 「SKU 是否全局唯一」不能用 every,必须用计数比较
SELECT
count(*) = count(DISTINCT sku) AS sku唯一,
every(sku IS NOT NULL) AS sku全非空
FROM order_items; sku唯一 | sku全非空
---------+-----------
t | t
(1 row)全 NULL 组返回 NULL,而不是 true
这是布尔聚合最危险的语义。按三值逻辑,bool_and 只在存在非 NULL 值时才能给出判断;组内全是 NULL 时它「无从判断」,于是返回 NULL。
WITH t(v) AS (VALUES (NULL::BOOLEAN), (NULL::BOOLEAN))
SELECT
bool_and(v) AS 裸结果,
bool_and(v) IS NULL AS 是否为NULL,
coalesce(bool_and(v), true) AS 默认通过,
coalesce(bool_and(v), false) AS 默认不通过
FROM t; 裸结果 | 是否为NULL | 默认通过 | 默认不通过
--------+------------+----------+------------
| t | t | f
(1 row)后果直接体现在报表上:
-- SO-2003 的 qc_passed 全是 NULL,bool_and 返回 NULL
SELECT
o.order_no,
bool_and(i.qc_passed) AS 质检全通过_裸,
coalesce(bool_and(i.qc_passed), false) AS 质检全通过_兜底
FROM orders o
JOIN order_items i ON i.order_id = o.id
GROUP BY o.order_no
ORDER BY o.order_no; order_no | 质检全通过_裸 | 质检全通过_兜底
----------+---------------+-----------------
SO-2001 | t | t
SO-2002 | f | f
SO-2003 | | f
(3 rows)空数据上的「全部通过」会静默变成 NULL
质量校验、对账、风控这类场景里,「全部通过」往往被下游当作「无异常,放行」。若该组恰好全是 NULL(数据未采集、字段未回填、上游延迟),bool_and 返回 NULL,前端可能渲染成空白、IF 判断走进 else 分支,甚至 WHERE bool_and(...) = false 直接漏掉这组。
只要有「通过 / 不通过」的业务含义,就必须显式兜底:
coalesce(bool_and(qc_passed), false) -- 保守口径:未知视为不通过
coalesce(bool_and(qc_passed), true) -- 乐观口径:未知视为通过选哪个是业务决策,不是技术决策——但必须显式写出来。
NULL 被忽略,不等于「NULL 当作 true」
三值逻辑里 bool_and 对 NULL 的处理是「跳过」,不是「视为真」也不是「视为假」。这一点可以用包含 false 与 NULL 的组证明:
WITH t(v) AS (VALUES (true), (false), (NULL), (NULL))
SELECT
bool_and(v) AS 含假含空_与,
bool_or(v) AS 含假含空_或
FROM t; 含假含空_与 | 含假含空_或
-------------+-------------
f | t
(1 row)bool_and 为 f——如果 NULL 被当作 true,结果同样是 f,看不出差别。换个更锋利的例子:
-- 只有 NULL 与 true,没有 false
WITH t(v) AS (VALUES (true), (true), (NULL))
SELECT
bool_and(v) AS 真与空,
bool_and(v) IS TRUE AS 严格等于真
FROM t; 真与空 | 严格等于真
--------+------------
t | t
(1 row)所以结论是:NULL 完全不影响 bool_and / bool_or 的判断结果,唯一的影响是「当全部非 NULL 值都不存在时,结果退化为 NULL」。
用 count 组合替代 bool_and
在 PostgreSQL 9.4 支持 FILTER 之前,以及在某些需要同时看到「总数 / 通过数 / 不通过数」的场景,用 count 组合表达更直观:
| 目标 | 布尔聚合写法 | count 组合写法 |
|---|---|---|
| 全部满足 | bool_and(cond) | count(*) FILTER (WHERE NOT cond) = 0 |
| 全部满足(忽略 NULL) | bool_and(cond) | count(cond) FILTER (WHERE NOT cond) = 0 |
| 存在满足 | bool_or(cond) | count(*) FILTER (WHERE cond) > 0 |
| 无任何行满足 | NOT bool_or(cond) ⚠️ NULL 风险 | count(*) FILTER (WHERE cond) = 0 |
-- 三种写法对照:全部已付款
SELECT
bool_and(i.is_paid) AS 布尔聚合,
count(*) FILTER (WHERE i.is_paid IS NOT TRUE) = 0 AS 计数组合_严格,
count(i.is_paid) = count(*) AND bool_and(i.is_paid) AS 额外要求非空
FROM order_items i
WHERE i.order_id = 1; 布尔聚合 | 计数组合_严格 | 额外要求非空
----------+---------------+--------------
t | t | t
(1 row)注意 NOT bool_or(cond) 的坑:当组内全是 NULL 时 bool_or 返回 NULL,NOT NULL 仍是 NULL,于是 WHERE NOT bool_or(...) 不会把该组纳入结果——这恰好是「没有行满足」应当成立的情形:
-- 组内全 NULL:bool_or 为 NULL,NOT 后仍是 NULL
WITH t(v) AS (VALUES (NULL::BOOLEAN))
SELECT
NOT bool_or(v) AS 看起来应为真,
NOT bool_or(v) IS TRUE AS 实际是否严格为真,
count(*) FILTER (WHERE v) = 0 AS 计数写法
FROM t; 看起来应为真 | 实际是否严格为真 | 计数写法
--------------+------------------+----------
| f | t
(1 row)所以「不存在满足条件的行」这类判断,一律用 count(*) FILTER (WHERE cond) = 0,不要用 NOT bool_or(cond)。
min / max 与布尔聚合的等价关系
PostgreSQL 里 false 的排序位置在 true 之前(false < true),因此对布尔列求 min 恰好是「有没有假」(即 bool_and),求 max 恰好是「有没有真」(即 bool_or)。pg_aggregate 内部就是按这个等价关系把布尔聚合接入 min/max 索引优化的:
| 布尔聚合 | 等价的 min / max 写法 | 含义 |
|---|---|---|
bool_and(flag) | min(flag) | 全为真才为真 |
bool_or(flag) | max(flag) | 有一个真就为真 |
-- 四种写法结果一致;NULL 均被忽略
SELECT
bool_and(flag) AS 布尔与,
min(flag) AS min等价,
bool_or(flag) AS 布尔或,
max(flag) AS max等价,
bool_and(flag) IS NOT DISTINCT FROM min(flag) AS 与min一致,
bool_or(flag) IS NOT DISTINCT FROM max(flag) AS 与max一致
FROM (VALUES (true), (false), (true), (NULL)) AS t(flag); 布尔与 | min等价 | 布尔或 | max等价 | 与min一致 | 与max一致
--------+---------+--------+---------+-----------+-----------
f | f | t | t | t | t
(1 row)知道这层等价关系有两个实际好处:一是当布尔列上已有 B-tree 索引时,min/max 可能走索引末端扫描(与数值列同理);二是排查代码时看到 min(flag) 能立刻反应过来它等价于 bool_and。但可读性上仍应优先写 bool_and / bool_or,业务语义远比 min/max 直白。
德摩根律:NOT 与两个聚合的互换
德摩根律(De Morgan's Law)在布尔聚合上表现为:「全都不满足」等价于「不存在满足」取反,反之亦然。
| 目标语义 | 写法 A | 写法 B(德摩根对偶) |
|---|---|---|
没有任何行满足 cond | NOT bool_or(cond) | bool_and(NOT cond) |
每一行都满足 cond | NOT bool_and(NOT cond) | bool_and(cond) |
存在一行不满足 cond | bool_or(NOT cond) | NOT bool_and(cond) |
-- 德摩根对偶:两侧结果应恒等(NULL 行需一并考虑)
WITH t(cond) AS (VALUES (true), (false), (true))
SELECT
NOT bool_or(cond) AS not_or,
bool_and(NOT cond) AS and_not,
NOT bool_or(cond) IS NOT DISTINCT FROM bool_and(NOT cond) AS 对偶成立
FROM t; not_or | and_not | 对偶成立
--------+---------+----------
f | f | t
(1 row)但德摩根律在三值逻辑下会被 NULL 破坏。bool_and(NOT cond) 在组内全 NULL 时返回 NULL,而 NOT bool_or(cond) 同样返回 NULL——两者仍一致,可一旦写进 WHERE 或参与 NOT 比较,NULL 就会让「应当为真」变成「筛不出来」:
-- 全 NULL 组:德摩根两侧都退化为 NULL,WHERE 都筛不出来
WITH t(cond) AS (VALUES (NULL::BOOLEAN))
SELECT
bool_and(NOT cond) AS and_not,
NOT bool_or(cond) AS not_or
FROM t
WHERE bool_and(NOT cond); and_not | not_or
---------+--------
(0 rows)结论:德摩根律可以用来改写、也可以在优化器层面互相推导,但不要把它当作 NULL 安全的手段。凡是要落地成筛选条件,一律显式写 count(*) FILTER (WHERE ...) = 0 并配 coalesce。
布尔分布统计:array_agg(DISTINCT flag) 与计数
做数据质量巡检时,除了「是否全部通过」,往往还需要看布尔列的取值分布——有多少真、多少假、多少 NULL。array_agg(DISTINCT ...) 能一眼看出到底出现了哪几种取值:
-- 一眼看清质检列出现过哪些取值(true / false / NULL)
SELECT
o.order_no,
array_agg(DISTINCT i.qc_passed) AS 取值分布,
count(*) FILTER (WHERE i.qc_passed IS TRUE) AS 通过数,
count(*) FILTER (WHERE i.qc_passed IS FALSE) AS 不通过数,
count(*) FILTER (WHERE i.qc_passed IS NULL) AS 未质检数,
count(*) AS 明细总数
FROM orders o
JOIN order_items i ON i.order_id = o.id
GROUP BY o.id, o.order_no
ORDER BY o.order_no; order_no | 取值分布 | 通过数 | 不通过数 | 未质检数 | 明细总数
----------+-----------+--------+----------+----------+----------
SO-2001 | {t} | 2 | 0 | 0 | 2
SO-2002 | {f,NULL} | 0 | 1 | 1 | 2
SO-2003 | {NULL} | 0 | 0 | 2 | 2array_agg(DISTINCT ...) 保留 NULL(与 array_agg 一致),所以 {NULL} 这类结果能明确告诉巡检者「这一单根本没有任何质检记录」,而不会像 bool_and 那样只给一个难以解释的空白。「聚合结论 + 取值分布 + 三类计数」三件套一起输出,才是可用于排查的数据质量报表。
布尔聚合与 EXISTS 的分工
两者都能回答「是否存在」,但作用层级不同:
| 维度 | bool_or + GROUP BY | EXISTS 子查询 |
|---|---|---|
| 返回粒度 | 每个分组一行布尔值 | 每行一个布尔值(相关子查询) |
| 是否继续扫描 | 必须扫完整个分组 | 找到第一行即可短路返回 |
| 能否与其他聚合同屏 | 能 | 不能(是独立子查询) |
是否受 GROUP BY 限制 | 是 | 否 |
| 典型场景 | 「每个订单是否都有异常明细」 | 「这个用户是否有任意订单」 |
-- 场景 A:需要「每个订单 + 该订单是否存在异常」,用 bool_or
SELECT
o.order_no,
bool_or(i.is_paid IS FALSE) AS 存在未付款明细
FROM orders o
JOIN order_items i ON i.order_id = o.id
GROUP BY o.order_no
ORDER BY o.order_no; order_no | 存在未付款明细
----------+----------------
SO-2001 | f
SO-2002 | t
SO-2003 | t
(3 rows)-- 场景 B:大表上只想知道「有没有」,用 EXISTS,命中即停
SELECT EXISTS (
SELECT 1 FROM order_items i WHERE i.is_paid IS FALSE
) AS 存在未付款明细; 存在未付款明细
----------------
t
(1 row)判断规则:需要按组输出 → 布尔聚合;只需一个全局真假 →
EXISTS。 在海量数据上,EXISTS因为能提前终止,通常比bool_or快得多。
与 GROUP BY / FILTER / HAVING 组合
场景一:把所有订单的完整性检查汇总成一行。
SELECT
bool_and(i.qty > 0) AS 数量全部合法,
bool_and(i.is_paid) AS 明细全部已付款,
bool_or(i.is_paid IS FALSE) AS 存在未付款明细,
coalesce(bool_and(i.qc_passed), false) AS 质检全部通过_保守,
count(*) AS 明细总数
FROM order_items i; 数量全部合法 | 明细全部已付款 | 存在未付款明细 | 质检全部通过_保守 | 明细总数
--------------+----------------+----------------+-------------------+----------
t | f | t | f | 6
(1 row)场景二:用 HAVING 挑出「不完整」的订单。
SELECT
o.order_no,
count(*) AS 明细数,
count(*) FILTER (WHERE i.is_paid) AS 已付款数,
bool_and(i.is_paid) AS 是否全部付款
FROM orders o
JOIN order_items i ON i.order_id = o.id
GROUP BY o.order_no
HAVING NOT coalesce(bool_and(i.is_paid), false) -- 挑出「并非全部已付款」的
ORDER BY o.order_no; order_no | 明细数 | 已付款数 | 是否全部付款
----------+--------+----------+--------------
SO-2002 | 2 | 1 | f
SO-2003 | 2 | 0 | f
(2 rows)场景三:与 FILTER 组合,让同一个布尔聚合只关心部分行。
SELECT
o.order_no,
bool_and(i.qc_passed) FILTER (WHERE i.is_paid) AS 已付款明细的质检全通过,
count(*) FILTER (WHERE i.qc_passed IS NOT NULL) AS 已质检数,
count(*) FILTER (WHERE i.is_paid AND i.qc_passed IS NULL) AS 待质检数
FROM orders o
JOIN order_items i ON i.order_id = o.id
GROUP BY o.order_no
ORDER BY o.order_no; order_no | 已付款明细的质检全通过 | 已质检数 | 待质检数
----------+------------------------+----------+----------
SO-2001 | t | 2 | 0
SO-2002 | t | 1 | 1
SO-2003 | | 0 | 0
(3 rows)看 SO-2003:FILTER (WHERE i.is_paid) 把两条未付款明细全部过滤掉,聚合输入为空组,于是 bool_and 返回 NULL(表格里的空白)。这正是「全 NULL / 空组返回 NULL」的另一种触发路径——FILTER 过滤后只剩空集,与「组内全 NULL」等价。
bool_and 与窗口形式
布尔聚合同样支持 OVER,可用于「累计至今是否全部满足」:
SELECT
o.order_no,
i.sku,
i.is_paid,
bool_and(i.is_paid) OVER (PARTITION BY o.order_no ORDER BY i.sku) AS 截至本行是否全部付款
FROM orders o
JOIN order_items i ON i.order_id = o.id
WHERE o.order_no = 'SO-2002'
ORDER BY i.sku; order_no | sku | is_paid | 截至本行是否全部付款
----------+---------+---------+----------------------
SO-2002 | B-0001 | t | t
SO-2002 | B-0002 | f | f
(2 rows)窗口形式默认帧是「起点到当前行」,所以第二行一旦遇到
false,累计结果就锁定为f。想要「整组」的结果需显式ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING。
实战示例
场景:一个订单履约平台需要一份「数据完整性日报」,逐单检查三类规则——明细是否全部已付款、数量是否全部为正、质检是否全部通过(未质检不算通过),并汇总出问题订单清单。
第 1 步:逐单输出三项检查结果
SELECT
o.order_no,
o.status,
count(*) AS 明细数,
bool_and(i.is_paid) AS 全部已付款,
bool_and(i.qty > 0) AS 数量全部合法,
bool_and(i.qc_passed) AS 质检全部通过_裸,
coalesce(bool_and(i.qc_passed), false) AS 质检全部通过_兜底,
count(*) FILTER (WHERE i.qc_passed IS NULL) AS 未质检数
FROM orders o
JOIN order_items i ON i.order_id = o.id
GROUP BY o.id, o.order_no, o.status
ORDER BY o.order_no; order_no | status | 明细数 | 全部已付款 | 数量全部合法 | 质检全部通过_裸 | 质检全部通过_兜底 | 未质检数
----------+---------+--------+------------+--------------+-----------------+-------------------+----------
SO-2001 | done | 2 | t | t | t | t | 0
SO-2002 | paid | 2 | f | t | f | f | 1
SO-2003 | pending | 2 | f | t | | f | 2
(3 rows)三行的差异正是本节全部要点的汇总:
- SO-2001:健康订单,三项检查全
t。 - SO-2002:有一条
false(未付款)与一条 NULL(未质检),bool_and因false返回f,NULL 不改变结论。 - SO-2003:
qc_passed全 NULL,裸结果为 NULL(空白),兜底后为f。
第 2 步:汇总成一行日报
SELECT
count(DISTINCT o.id) AS 订单数,
count(*) AS 明细数,
bool_and(i.is_paid) AS 全部订单明细已付款,
bool_or(i.is_paid IS FALSE) AS 存在未付款明细,
coalesce(bool_and(i.qc_passed), false) AS 全部质检通过,
bool_or(i.qc_passed IS NULL) AS 存在未质检明细_or,
count(*) FILTER (WHERE i.qc_passed IS NULL) > 0 AS 存在未质检明细_count
FROM orders o
JOIN order_items i ON i.order_id = o.id; 订单数 | 明细数 | 全部订单明细已付款 | 存在未付款明细 | 全部质检通过 | 存在未质检明细_or | 存在未质检明细_count
--------+--------+--------------------+----------------+--------------+-------------------+---------------------
3 | 6 | f | t | f | t | t
(1 row)注意这里
存在未质检明细_or与_count结果一致,但前者在「表里一行都没有」时会返回 NULL(全空组),后者稳定返回f。这就是前面强调的写法选择。
第 3 步:用 HAVING 挑出问题订单
SELECT
o.order_no,
o.status,
count(*) AS 明细数,
count(*) FILTER (WHERE i.is_paid IS NOT TRUE) AS 未付款或未对账数,
count(*) FILTER (WHERE i.qc_passed IS NOT TRUE) AS 未通过或未质检数
FROM orders o
JOIN order_items i ON i.order_id = o.id
GROUP BY o.id, o.order_no, o.status
HAVING NOT coalesce(bool_and(i.is_paid), false)
OR NOT coalesce(bool_and(i.qc_passed), false)
ORDER BY o.order_no; order_no | status | 明细数 | 未付款或未对账数 | 未通过或未质检数
----------+---------+--------+------------------+------------------
SO-2002 | paid | 2 | 1 | 2
SO-2003 | pending | 2 | 2 | 2
(3 rows)
is_paid IS NOT TRUE与NOT is_paid不等价:前者会把 NULL 也算作「不满足」,后者对 NULL 返回 NULL 从而被FILTER丢弃。做「异常统计」时应统一用IS NOT TRUE,才能把「未知」纳入分母。
第 4 步:用窗口形式做订单内的渐进校验
SELECT
o.order_no,
i.sku,
i.is_paid,
i.qc_passed,
bool_and(i.is_paid) OVER (
PARTITION BY o.order_no ORDER BY i.sku
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
) AS 累计_是否全部付款,
bool_and(coalesce(i.qc_passed, false)) OVER (
PARTITION BY o.order_no ORDER BY i.sku
ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
) AS 整单_质检全部通过
FROM orders o
JOIN order_items i ON i.order_id = o.id
ORDER BY o.order_no, i.sku; order_no | sku | is_paid | qc_passed | 累计_是否全部付款 | 整单_质检全部通过
----------+---------+---------+-----------+-------------------+------------------
SO-2001 | A-0001 | t | t | t | t
SO-2001 | A-0002 | t | t | t | t
SO-2002 | B-0001 | t | t | t | f
SO-2002 | B-0002 | f | | f | f
SO-2003 | C-0001 | f | | f | f
SO-2003 | C-0002 | f | | f | f
(6 rows)两种帧的差别一目了然:累计 列随行推进而变化,整单 列在同一订单内恒定(因为用了 UNBOUNDED FOLLOWING)。窗口形式里同样包了 coalesce——窗口聚合对 NULL 的语义与分组聚合完全一致,不是窗口就不会返回 NULL。
第 5 步:验证布尔聚合与 count 组合等价
SELECT
o.order_no,
bool_and(i.is_paid) AS bool_and写法,
count(*) FILTER (WHERE i.is_paid IS NOT TRUE) = 0 AS count写法,
(bool_and(i.is_paid)) IS NOT DISTINCT FROM
(count(*) FILTER (WHERE i.is_paid IS NOT TRUE) = 0) AS 两者一致
FROM orders o
JOIN order_items i ON i.order_id = o.id
GROUP BY o.order_no
ORDER BY o.order_no; order_no | bool_and写法 | count写法 | 两者一致
----------+--------------+-----------+----------
SO-2001 | t | t | t
SO-2002 | f | f | t
SO-2003 | f | f | t
(3 rows)在非全 NULL 组上两者永远一致;差异只出现在全 NULL / 空组上——那时
bool_and给 NULL,count(...) = 0给t。用IS NOT DISTINCT FROM而非=比较,正是为了正确处理 NULL 情形。
第 6 步:清理
DROP TABLE IF EXISTS order_items;
DROP TABLE IF EXISTS orders;注意事项
every是bool_and的别名,与「全部不同」「每行分别」都无关:sql-- 这两个是同一个函数 bool_and(cond) every(cond) -- 「全部互不相同」要用计数比较 count(*) = count(DISTINCT x)全 NULL 组返回 NULL,不是
true。这是布尔聚合最危险的语义:sql-- 空数据或全 NULL 时得到 NULL SELECT bool_and(qc_passed) FROM t; -- 只要结果参与业务判断,就必须兜底 SELECT coalesce(bool_and(qc_passed), false) FROM t;bool_and/bool_or忽略 NULL,NULL 既不当作true也不当作false,只是不参与判断。FILTER过滤后只剩空集时,同样返回 NULL,这与「组内全 NULL」表现一致,容易在排查时误判成数据缺失。不要用
NOT bool_or(cond)表达「不存在满足条件的行」,全 NULL 时它返回 NULL 而非true:sql-- 不安全 WHERE NOT bool_or(cond) -- 安全 WHERE count(*) FILTER (WHERE cond) = 0is_paid IS NOT TRUE与NOT is_paid语义不同:前者把 NULL 也算不满足,后者对 NULL 返回 NULL。做异常统计时按需选择,别混用。bool_and的参数必须是布尔类型或布尔表达式,传数字会报错:sql-- 错误 SELECT bool_and(qty) FROM order_items; -- ERROR: function bool_and(integer) does not exist -- 正确 SELECT bool_and(qty > 0) FROM order_items;bool_and/bool_or不能嵌套(所有聚合都不能),需要「先按组判断再整体判断」时必须分层:sql-- 错误 SELECT bool_and(bool_or(x)) FROM t GROUP BY g; -- 正确 WITH per_group AS (SELECT g, bool_or(x) AS any_x FROM t GROUP BY g) SELECT bool_and(any_x) FROM per_group;与
EXISTS的分工要看返回粒度:按组输出用布尔聚合,只要一个全局真假用EXISTS(可短路,通常更快)。布尔聚合结果是真正的
boolean类型,可以参与WHERE/HAVING/CASE,但不能直接sum:sql-- 错误:boolean 不能求和 SELECT sum(bool_and(x)) FROM t; -- 正确:先转换 SELECT sum((bool_and(x))::int) FROM t;HAVING中引用布尔聚合时记得处理 NULL,否则「条件为假」与「条件为 NULL」的行为差异会让筛选结果意外:sqlHAVING coalesce(bool_and(cond), false) = false窗口形式不改变 NULL 语义。
bool_and(x) OVER (...)在全 NULL 分区上同样返回 NULL,coalesce一样不能省。
最佳实践
凡是「全部 / 存在」的业务判断,都显式写出兜底口径。默认值选
false(保守)还是true(乐观)必须由业务决定并写在注释里:sql-- 质检未采集时视为不通过,宁可误报不可漏报 coalesce(bool_and(qc_passed), false) AS 质检全部通过优先用
bool_and/bool_or表达逻辑意图,而不是用count拼不等式。前者语义直接,后者只有在需要同时展示计数时才更合适:sql-- 意图清晰 bool_and(is_paid) -- 需要数字时 count(*) FILTER (WHERE is_paid) AS 已付款数, count(*) AS 总数「不存在满足条件的行」一律用
count(*) FILTER (WHERE cond) = 0,避免NOT bool_or(...)的 NULL 陷阱。统计异常时用
IS NOT TRUE而不是NOT,让 NULL 明确归入「异常」这一侧,避免未知数据被静默排除在分母外:sqlcount(*) FILTER (WHERE qc_passed IS NOT TRUE) AS 未通过或未质检数把布尔聚合与
FILTER组合,一次扫描产出多口径结论,避免同一张表扫多遍:sqlSELECT bool_and(is_paid) AS 全部已付款, bool_and(is_paid) FILTER (WHERE qty > 1) AS 多数量明细全部已付款, bool_or(is_paid IS FALSE) AS 存在未付款 FROM order_items;按组输出用
bool_and+GROUP BY,全局判断用EXISTS。前者便于与其他聚合同屏,后者在大表上能提前终止。布尔聚合结果直接作为
CASE条件,让报表可读性更好,不要把布尔值原样吐给前端:sqlCASE WHEN coalesce(bool_and(qc_passed), false) THEN '通过' ELSE '存在异常' END AS 质检结论对「未知」状态单独留一列。布尔聚合能告诉你「是否全部通过」,但无法区分「全部通过」与「全部未采集」——需要额外用
count(*) FILTER (WHERE x IS NULL)把「未知量」暴露出来:sqlcount(*) FILTER (WHERE qc_passed IS NULL) AS 未质检数, bool_and(qc_passed) AS 已质检部分是否全通过在数据校验脚本中统一用
bool_and(x IS NOT NULL)检查字段完整性,比逐个count(x) = count(*)写法更简洁:sqlSELECT bool_and(order_no IS NOT NULL) AS 单号完整, bool_and(sku IS NOT NULL) AS SKU完整, bool_and(qty IS NOT NULL) AS 数量完整 FROM order_items;给布尔聚合的结果列起明确的中文别名,如「全部已付款」「存在未付款明细」,让报表消费方不必回读 SQL 才能理解
t/f的含义。
下一节