Skip to content

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
标准别名everySQL 标准里 bool_and 的名字,两者完全等价
全称量词Universal Quantifier「对所有行成立」,对应 bool_and
存在量词Existential Quantifier「存在某行成立」,对应 bool_or
三值逻辑Three-Valued Logictrue / false / NULL,聚合后仍遵守
真空真Vacuous Truth空集上「全部满足」逻辑上为真,但 PostgreSQL 返回 NULL

两个函数与三值逻辑的完整真值表(按组内行的布尔取值组合):

组内包含bool_andbool_or说明
全部 truetruetrue—
true + falsefalsetrue与 / 或的基本行为
全部 falsefalsefalse—
true + NULLtruetrueNULL 被忽略
false + NULLfalsefalseNULL 被忽略
全部 NULLNULLNULL关键陷阱,非 true 也非 false
空组(无行)NULLNULL只能靠 coalesce 决定默认口径
text
              bool_and                        bool_or
   ┌───────────────────────────┐   ┌───────────────────────────┐
   │ 只看非 NULL 值            │   │ 只看非 NULL 值            │
   │ 全真 → true               │   │ 有真 → true               │
   │ 有一个假 → false          │   │ 全假 → false              │
   │ 没有非 NULL 值 → NULL ⚠️  │   │ 没有非 NULL 值 → NULL ⚠️  │
   └───────────────────────────┘   └───────────────────────────┘

一句话记忆:bool_and / bool_or 只对非 NULL 值做出判断;一旦组内没有任何非 NULL 值,它们无法判断,只能返回 NULL。

语法与用法 ​

sql
-- 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 表达的是「对全部行都成立」。

最小可运行示例:

sql
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;
text
 全部为真 | 存在为真 | 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 行确实存在但被两个布尔聚合忽略了。

准备本节示例数据:

sql
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 的别名,两者在解析后是同一个函数:

sql
-- 用 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';
text
   名称   | 别名指向 | 是否同一函数
----------+----------+--------------
 bool_and | bool_and | t
(1 row)
sql
-- 更直接的等价性验证:两者在任意数据上结果都一致
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;
text
 b | e | 等价
---+---+------
 f | f | t
(1 row)

常见的误用是把 every 读成「每一个都不同」:

想表达的业务含义正确写法错误写法
全部行都满足条件bool_and(cond) / every(cond)—
存在行满足条件bool_or(cond)—
全部行的值互不相同count(*) = count(DISTINCT x)every(x)
每行都非 NULLbool_and(x IS NOT NULL)—
sql
-- 「SKU 是否全局唯一」不能用 every,必须用计数比较
SELECT
  count(*) = count(DISTINCT sku) AS sku唯一,
  every(sku IS NOT NULL)         AS sku全非空
FROM order_items;
text
 sku唯一 | sku全非空
---------+-----------
 t       | t
(1 row)

全 NULL 组返回 NULL,而不是 true ​

这是布尔聚合最危险的语义。按三值逻辑,bool_and 只在存在非 NULL 值时才能给出判断;组内全是 NULL 时它「无从判断」,于是返回 NULL。

sql
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;
text
 裸结果 | 是否为NULL | 默认通过 | 默认不通过
--------+------------+----------+------------
        | t          | t        | f
(1 row)

后果直接体现在报表上:

sql
-- 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;
text
 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 直接漏掉这组。

只要有「通过 / 不通过」的业务含义,就必须显式兜底:

sql
coalesce(bool_and(qc_passed), false)   -- 保守口径:未知视为不通过
coalesce(bool_and(qc_passed), true)    -- 乐观口径:未知视为通过

选哪个是业务决策,不是技术决策——但必须显式写出来。

NULL 被忽略,不等于「NULL 当作 true」 ​

三值逻辑里 bool_and 对 NULL 的处理是「跳过」,不是「视为真」也不是「视为假」。这一点可以用包含 false 与 NULL 的组证明:

sql
WITH t(v) AS (VALUES (true), (false), (NULL), (NULL))
SELECT
  bool_and(v) AS 含假含空_与,
  bool_or(v)  AS 含假含空_或
FROM t;
text
 含假含空_与 | 含假含空_或
-------------+-------------
 f           | t
(1 row)

bool_and 为 f——如果 NULL 被当作 true,结果同样是 f,看不出差别。换个更锋利的例子:

sql
-- 只有 NULL 与 true,没有 false
WITH t(v) AS (VALUES (true), (true), (NULL))
SELECT
  bool_and(v) AS 真与空,
  bool_and(v) IS TRUE AS 严格等于真
FROM t;
text
 真与空 | 严格等于真
--------+------------
 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
sql
-- 三种写法对照:全部已付款
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;
text
 布尔聚合 | 计数组合_严格 | 额外要求非空
----------+---------------+--------------
 t        | t             | t
(1 row)

注意 NOT bool_or(cond) 的坑:当组内全是 NULL 时 bool_or 返回 NULL,NOT NULL 仍是 NULL,于是 WHERE NOT bool_or(...) 不会把该组纳入结果——这恰好是「没有行满足」应当成立的情形:

sql
-- 组内全 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;
text
 看起来应为真 | 实际是否严格为真 | 计数写法
--------------+------------------+----------
              | 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)有一个真就为真
sql
-- 四种写法结果一致;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);
text
 布尔与 | 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(德摩根对偶)
没有任何行满足 condNOT bool_or(cond)bool_and(NOT cond)
每一行都满足 condNOT bool_and(NOT cond)bool_and(cond)
存在一行不满足 condbool_or(NOT cond)NOT bool_and(cond)
sql
-- 德摩根对偶:两侧结果应恒等(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;
text
 not_or | and_not | 对偶成立
--------+---------+----------
 f      | f       | t
(1 row)

但德摩根律在三值逻辑下会被 NULL 破坏。bool_and(NOT cond) 在组内全 NULL 时返回 NULL,而 NOT bool_or(cond) 同样返回 NULL——两者仍一致,可一旦写进 WHERE 或参与 NOT 比较,NULL 就会让「应当为真」变成「筛不出来」:

sql
-- 全 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);
text
 and_not | not_or
---------+--------
(0 rows)

结论:德摩根律可以用来改写、也可以在优化器层面互相推导,但不要把它当作 NULL 安全的手段。凡是要落地成筛选条件,一律显式写 count(*) FILTER (WHERE ...) = 0 并配 coalesce。

布尔分布统计:array_agg(DISTINCT flag) 与计数 ​

做数据质量巡检时,除了「是否全部通过」,往往还需要看布尔列的取值分布——有多少真、多少假、多少 NULL。array_agg(DISTINCT ...) 能一眼看出到底出现了哪几种取值:

sql
-- 一眼看清质检列出现过哪些取值(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;
text
 order_no | 取值分布  | 通过数 | 不通过数 | 未质检数 | 明细总数
----------+-----------+--------+----------+----------+----------
 SO-2001  | {t}       |      2 |        0 |        0 |        2
 SO-2002  | {f,NULL}  |      0 |        1 |        1 |        2
 SO-2003  | {NULL}    |      0 |        0 |        2 |        2

array_agg(DISTINCT ...) 保留 NULL(与 array_agg 一致),所以 {NULL} 这类结果能明确告诉巡检者「这一单根本没有任何质检记录」,而不会像 bool_and 那样只给一个难以解释的空白。「聚合结论 + 取值分布 + 三类计数」三件套一起输出,才是可用于排查的数据质量报表。

布尔聚合与 EXISTS 的分工 ​

两者都能回答「是否存在」,但作用层级不同:

维度bool_or + GROUP BYEXISTS 子查询
返回粒度每个分组一行布尔值每行一个布尔值(相关子查询)
是否继续扫描必须扫完整个分组找到第一行即可短路返回
能否与其他聚合同屏能不能(是独立子查询)
是否受 GROUP BY 限制是否
典型场景「每个订单是否都有异常明细」「这个用户是否有任意订单」
sql
-- 场景 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;
text
 order_no | 存在未付款明细
----------+----------------
 SO-2001  | f
 SO-2002  | t
 SO-2003  | t
(3 rows)
sql
-- 场景 B:大表上只想知道「有没有」,用 EXISTS,命中即停
SELECT EXISTS (
  SELECT 1 FROM order_items i WHERE i.is_paid IS FALSE
) AS 存在未付款明细;
text
 存在未付款明细
----------------
 t
(1 row)

判断规则:需要按组输出 → 布尔聚合;只需一个全局真假 → EXISTS。 在海量数据上,EXISTS 因为能提前终止,通常比 bool_or 快得多。

与 GROUP BY / FILTER / HAVING 组合 ​

场景一:把所有订单的完整性检查汇总成一行。

sql
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;
text
 数量全部合法 | 明细全部已付款 | 存在未付款明细 | 质检全部通过_保守 | 明细总数
--------------+----------------+----------------+-------------------+----------
 t            | f              | t              | f                 |        6
(1 row)

场景二:用 HAVING 挑出「不完整」的订单。

sql
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;
text
 order_no | 明细数 | 已付款数 | 是否全部付款
----------+--------+----------+--------------
 SO-2002  |      2 |        1 | f
 SO-2003  |      2 |        0 | f
(2 rows)

场景三:与 FILTER 组合,让同一个布尔聚合只关心部分行。

sql
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;
text
 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,可用于「累计至今是否全部满足」:

sql
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;
text
 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 步:逐单输出三项检查结果 ​

sql
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;
text
 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 步:汇总成一行日报 ​

sql
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;
text
 订单数 | 明细数 | 全部订单明细已付款 | 存在未付款明细 | 全部质检通过 | 存在未质检明细_or | 存在未质检明细_count
--------+--------+--------------------+----------------+--------------+-------------------+---------------------
      3 |      6 | f                  | t              | f            | t                 | t
(1 row)

注意这里 存在未质检明细_or 与 _count 结果一致,但前者在「表里一行都没有」时会返回 NULL(全空组),后者稳定返回 f。这就是前面强调的写法选择。

第 3 步:用 HAVING 挑出问题订单 ​

sql
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;
text
 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 步:用窗口形式做订单内的渐进校验 ​

sql
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;
text
 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 组合等价 ​

sql
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;
text
 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 步:清理 ​

sql
DROP TABLE IF EXISTS order_items;
DROP TABLE IF EXISTS orders;

注意事项 ​

  1. every 是 bool_and 的别名,与「全部不同」「每行分别」都无关:

    sql
    -- 这两个是同一个函数
    bool_and(cond)
    every(cond)
    -- 「全部互不相同」要用计数比较
    count(*) = count(DISTINCT x)
  2. 全 NULL 组返回 NULL,不是 true。这是布尔聚合最危险的语义:

    sql
    -- 空数据或全 NULL 时得到 NULL
    SELECT bool_and(qc_passed) FROM t;
    -- 只要结果参与业务判断,就必须兜底
    SELECT coalesce(bool_and(qc_passed), false) FROM t;
  3. bool_and / bool_or 忽略 NULL,NULL 既不当作 true 也不当作 false,只是不参与判断。

  4. FILTER 过滤后只剩空集时,同样返回 NULL,这与「组内全 NULL」表现一致,容易在排查时误判成数据缺失。

  5. 不要用 NOT bool_or(cond) 表达「不存在满足条件的行」,全 NULL 时它返回 NULL 而非 true:

    sql
    -- 不安全
    WHERE NOT bool_or(cond)
    -- 安全
    WHERE count(*) FILTER (WHERE cond) = 0
  6. is_paid IS NOT TRUE 与 NOT is_paid 语义不同:前者把 NULL 也算不满足,后者对 NULL 返回 NULL。做异常统计时按需选择,别混用。

  7. 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;
  8. 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;
  9. 与 EXISTS 的分工要看返回粒度:按组输出用布尔聚合,只要一个全局真假用 EXISTS(可短路,通常更快)。

  10. 布尔聚合结果是真正的 boolean 类型,可以参与 WHERE / HAVING / CASE,但不能直接 sum:

    sql
    -- 错误:boolean 不能求和
    SELECT sum(bool_and(x)) FROM t;
    -- 正确:先转换
    SELECT sum((bool_and(x))::int) FROM t;
  11. HAVING 中引用布尔聚合时记得处理 NULL,否则「条件为假」与「条件为 NULL」的行为差异会让筛选结果意外:

    sql
    HAVING coalesce(bool_and(cond), false) = false
  12. 窗口形式不改变 NULL 语义。bool_and(x) OVER (...) 在全 NULL 分区上同样返回 NULL,coalesce 一样不能省。

最佳实践 ​

  1. 凡是「全部 / 存在」的业务判断,都显式写出兜底口径。默认值选 false(保守)还是 true(乐观)必须由业务决定并写在注释里:

    sql
    -- 质检未采集时视为不通过,宁可误报不可漏报
    coalesce(bool_and(qc_passed), false) AS 质检全部通过
  2. 优先用 bool_and / bool_or 表达逻辑意图,而不是用 count 拼不等式。前者语义直接,后者只有在需要同时展示计数时才更合适:

    sql
    -- 意图清晰
    bool_and(is_paid)
    -- 需要数字时
    count(*) FILTER (WHERE is_paid) AS 已付款数,
    count(*)                        AS 总数
  3. 「不存在满足条件的行」一律用 count(*) FILTER (WHERE cond) = 0,避免 NOT bool_or(...) 的 NULL 陷阱。

  4. 统计异常时用 IS NOT TRUE 而不是 NOT,让 NULL 明确归入「异常」这一侧,避免未知数据被静默排除在分母外:

    sql
    count(*) FILTER (WHERE qc_passed IS NOT TRUE) AS 未通过或未质检数
  5. 把布尔聚合与 FILTER 组合,一次扫描产出多口径结论,避免同一张表扫多遍:

    sql
    SELECT
      bool_and(is_paid)                                      AS 全部已付款,
      bool_and(is_paid) FILTER (WHERE qty > 1)               AS 多数量明细全部已付款,
      bool_or(is_paid IS FALSE)                              AS 存在未付款
    FROM order_items;
  6. 按组输出用 bool_and + GROUP BY,全局判断用 EXISTS。前者便于与其他聚合同屏,后者在大表上能提前终止。

  7. 布尔聚合结果直接作为 CASE 条件,让报表可读性更好,不要把布尔值原样吐给前端:

    sql
    CASE WHEN coalesce(bool_and(qc_passed), false)
         THEN '通过' ELSE '存在异常' END AS 质检结论
  8. 对「未知」状态单独留一列。布尔聚合能告诉你「是否全部通过」,但无法区分「全部通过」与「全部未采集」——需要额外用 count(*) FILTER (WHERE x IS NULL) 把「未知量」暴露出来:

    sql
    count(*) FILTER (WHERE qc_passed IS NULL) AS 未质检数,
    bool_and(qc_passed)                       AS 已质检部分是否全通过
  9. 在数据校验脚本中统一用 bool_and(x IS NOT NULL) 检查字段完整性,比逐个 count(x) = count(*) 写法更简洁:

    sql
    SELECT
      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;
  10. 给布尔聚合的结果列起明确的中文别名,如「全部已付款」「存在未付款明细」,让报表消费方不必回读 SQL 才能理解 t / f 的含义。

下一节

继续学习:STDDEV / VARIANCE / REGR_xxx

参考链接 ​