保留每份客户:NULL-6查询改写与执行
这份任务把“谓词值正确”“输出份数正确”“空组正确”和“计划确实能执行”放在同一个输入上检查。最后要交出五份结果及其计算过程,而不是只说两个 SQL 看起来相近。
阅读入口与自测
核心四站:三值谓词 → 外连接补行 → 存在与排除 → 保留合同的去相关。
按需补课:不清楚两份同值客户为什么不能合并,先读多重集重数与聚合;不清楚一次返回后要保存什么,读迭代器游标;不知道碰撞为何还要比较原键,读哈希表。计页与有限帧的后续分支接DB-4旧路线,本任务不替换其三个连接算法和页账本。
入口自测:NULL=NULL 是什么?同一左客户两份、每份有三条匹配订单,内连接几份、半连接几份?答案是 UNKNOWN、六份、两份。若第一题答 TRUE,先分清普通等号和身份相等;若半连接答一份,先分清工作参数去重和输出去重。
固定数据与观察合同
Customer(label,k) 按输入顺序为:
| 出现编号,仅供核对 | label | k |
|---|---|---|
| c0 | A | 1 |
| c1 | A | 1 |
| c2 | B | 2 |
| c3 | C | 3 |
| c4 | D | NULL |
| c5 | E | 4 |
Orders(k,amount,status) 为:
| 出现编号,仅供核对 | k | amount | status |
|---|---|---|---|
| o0 | 1 | 10 | ok |
| o1 | 1 | 10 | ok |
| o2 | 1 | NULL | ok |
| o3 | 2 | 20 | hold |
| o4 | 2 | NULL | ok |
| o5 | NULL | 99 | ok |
| o6 | 4 | 0 | ok |
| o7 | 4 | 5 | ok |
Blocked(k) 有两份,分别为 2、NULL。没有声明 label 或 k 是唯一键。出现编号不进入 SQL,不给查询偷偷增加主键约束。
所有输入为固定有限 bag,普通比较采用 SQL 三值规则;表达式纯、确定且无副作用。金额数学上为精确整数;无 LIMIT、窗口、DISTINCT 输出或并发修改。标量子查询的多行错误单独观察,不用 NULL 代替它。成功结果按完整元组的多重集比较,不要求输出顺序。
任务
- 分别列出“按键左连接”“ON 中再要求 ok”“左连接后 WHERE 要求 ok”的每个客户输出份数,指出相差的行来自哪里
- 计算有没有 ok 订单的半连接和反连接;再用 Blocked 比较 NOT EXISTS 与 NOT IN,分别给出完整结果
- 对未被黑名单按普通等号匹配排除的每份客户,求 ok 订单 COUNT(*)、COUNT(amount)、SUM(amount)。保留两份 A,区分 B 的全 NULL 组和 C 的空组
- 把逐客户重复扫描改为一次订单摘要加逐份客户探测。写出状态、不变量、完成值、内存需求和超限行为
- 把聚合子查询改为普通标量 amount,分别测试零行、一行 NULL、两份相同值、两份不同值和无外层行
- 将相关条件改为“同键且 amount>客户阈值”,说明参数域必须增加什么;不能仅换几个数据后宣称原键摘要仍足够
完整答案
先按出现配对,再判断补行
按键左连接:c0、c1 各配 o0、o1、o2;c2 配 o3、o4;c3、c4 各补一行;c5 配 o6、o7,共十二份。o5 不匹配 c4,因为 NULL=NULL 为 UNKNOWN。
ON 同时要求 ok:只删除 c2/o3 这对,其余匹配和 c3、c4 的补行保持,共十一份。左连接后 WHERE status=ok:删除 c2/o3,还删除 c3、c4 的 NULL 占位,共九份。WHERE 删除完不会让连接回头重新补行。
c2/o4 是真实匹配,虽然 amount=NULL。用金额是否 NULL 判断匹配失败会误判它;如需显式标识,应在右输入增加 present=1,补行才把它变成 NULL。
存在、排除与最终五份
有没有 ok 订单:半连接为 A 两份、B、E;反连接为 C、D。两份结果相加还原六份 Customer。内连接后投影会把 A 放大成六份,DISTINCT 又会把它压成一份,都不是半连接。
NOT EXISTS 对 Blocked 只删除 B,因为其键 2 有真匹配;NULL 黑名单条目不为任何普通等号提供真匹配。NOT IN 则把 A、C、D、E 的排除判断变成 UNKNOWN,B 为 FALSE,所以没有一份能通过 WHERE。右侧为空时即使左键 NULL,NOT IN 也为 TRUE,这是另一个必须检查的边界。
原终点的 SQL 形状如下,三个聚合子查询具有相同筛选条件,可在物理实现中融合扫描:
SELECT c.label, c.k,
(SELECT COUNT(*) FROM Orders o
WHERE o.k=c.k AND o.status='ok') AS n,
(SELECT COUNT(o.amount) FROM Orders o
WHERE o.k=c.k AND o.status='ok') AS nonnull_n,
(SELECT SUM(o.amount) FROM Orders o
WHERE o.k=c.k AND o.status='ok') AS total
FROM Customer c
WHERE NOT EXISTS (SELECT 1 FROM Blocked b WHERE b.k=c.k);
| 输出元组 | 重数 |
|---|---|
| (A,1,3,2,20) | 2 |
| (C,3,0,0,NULL) | 1 |
| (D,NULL,0,0,NULL) | 1 |
| (E,4,2,2,5) | 1 |
暂不排除 B 时,它的摘要是 (B,2,1,0,NULL)。B 的真实 NULL 金额行使 COUNT(*)=1,C 的空输入为零;两者 SUM 都是 NULL。E 的零金额是合法非 NULL 输入,计数为二。
摘要计划及出现级证明
扫描 Orders,只接纳 status=ok 且键非 NULL 的出现,按 k 保存 (行数,非 NULL 金额数,数值和)。内部状态依次在键 1 上从 (1,1,10)、(2,2,20) 到 (3,2,20);键 2 得 (1,0,0);键 4 从 (1,1,0) 到 (2,2,5)。共六次状态更新、三个键项。
完成摘要时,非 NULL 金额数为零就输出 SUM=NULL,因此键 2 的完成值是 (1,0,NULL)。客户未命中时返回全局聚合默认值 (0,0,NULL),不是删除客户。每份客户分别探测,c0、c1 各取得一次键 1 的摘要。
对应的 SQL 形状为:
SELECT c.label, c.k,
COALESCE(g.n,0) AS n,
COALESCE(g.nonnull_n,0) AS nonnull_n,
g.total
FROM Customer c
LEFT JOIN (
SELECT k, COUNT(*) AS n, COUNT(amount) AS nonnull_n,
SUM(amount) AS total
FROM Orders
WHERE status='ok' AND k IS NOT NULL
GROUP BY k
) g ON g.k=c.k
WHERE NOT EXISTS (SELECT 1 FROM Blocked b WHERE b.k=c.k);
对已扫描前缀,G[k] 始终等于该键全部合格真实出现的贡献和;逐份加入保持不变量。扫描结束后,对一个客户分别讨论 NULL 键、非 NULL 命中和非 NULL 未命中,三种情况得到与原子查询相同的订单 bag 摘要。原客户没有去重,每份都交付一次,所以最终重数也相同。
不要把 g.total 也 COALESCE 为零;不要以原订单外连接后的 COUNT(*) 代替真实订单计数;不要按 label,k 对原外连接分组而忘记两份 A 已经合并。每种错误分别改变空值、空组或外层重数。
操作与空间账本
不带排除、无索引且三个聚合融合的逐客户扫描需要 6×8=48 次候选对条件求值。按五个不同参数共享计算是 5×8=40 次,再计建域和重连。专用等值摘要则只读八份 Orders,做六次状态更新;这些操作种类不同,不直接换算成性能倍数。
合成计划读 Blocked 两份,精确集合只有键 2;读 Orders 八份,保留三个摘要项;读 Customer 六份,非 NULL 键作五次排除探测。B 被删除,剩余非 NULL 的 A 两份、C、E 做四次摘要探测,D 直接取默认值,输出五份。
同时需要三个摘要项、一个黑名单项,以及桶头、游标、两个黑名单标志和输出缓冲。检查器按逻辑项容量约束,未把 Python 对象大小当成真实页宽。容量参数只接受非负整数,拒绝布尔值和小数。键及计数可用固定机器字容纳、哈希负载受控时具有线性期望记录操作界;任意精度算术、碰撞最坏情况和页 I/O 必须另计。
黑名单表须到 EOF 后才 READY。容量只允许一个非 NULL 键时,读 {2,NULL} 成功;读 {2,NULL,5} 在插入 5 时 FAILED,不能继续探测。摘要容量为二时遇第三个组也返回 CapacityExceeded,不允许遗漏键 4 后把 E 当作空组。读错误不能冒充 EOF。
标量与参数域迁移
无聚合的标量子查询在 C、D 上零行返回 NULL,在 B 上一行 NULL 也返回 NULL,在 E 上两行报错,在 A 上三行报错。两行都为 10 仍须报错。去相关可以为每键保存 0、1、至少2 三档计数和第一值;构建时记录多行标志,到被外层需求的键探测时再报错。空外层或只查询 C 时,不因内表其他键重复而失败。
迁移题另取外输入 (label,k,threshold):(A,1,5) 两份、(B,1,15)、(C,NULL,0)。内表金额为同键 1 的 10、20、NULL 三份。查询同键且 amount>threshold 的全局摘要。A 得 (2,2,30) 两份,B 得 (1,1,20),C 得 (0,0,NULL)。
参数必须是 (k,threshold),不能只用 k;A 与 B 同键却需要不同答案。域去重后有三个参数值,逐参数计算再按两列的 NULL 安全身份回连,原业务 k 相等和金额比较仍使用三值语义。简单参数域与内表配对需 3×3=9 次候选检查,原四份逐行计算为 12 次;若要进一步加速,需新的排序/前缀摘要算法与证明,本页不从“去相关”三个字推出线性界。
复算与错误定位
下载Python 3 标准库检查器,运行(不要加 -O;关闭断言时程序会拒绝执行):
python3 foundations-sql-null-checker.py
它核对 9 个二元三值输入、240 个成员探测(含强制碰撞)、1,600 个出现级外连接实例,以及各 10,360 个聚合和标量成功/错误实例。小实例保留每份出现编号,再按完整输出元组的 Counter 比较,不能只比较 set 或总行数。它另外通过 SQLite 执行上面两条 SQL,核对共同五份结果。
SQLite 的普通标量子查询允许多行时取值,与本页 PostgreSQL/SQL 多行错误合同不同。因此 SQLite 只复核 bag/NULL/聚合查询;标量错误由显式解释器与独立参数状态实现交叉检查,不宣称做过 PostgreSQL 实机测试。有限穷举是实现证据,正文逐份证明才承担所有有限输入上的结论。
若只剩一份 A,先找外层或最终输出的 DISTINCT;若 A 变四份,检查参数域有没有去重;若 C 计数为一,检查是否把占位算作真实订单;若 D 消失,检查参数身份回连是否误用普通等号;若 E 没有报标量错误,检查是否取了第一行或提前去重;若查询 C 也因 A 重复而报错,检查是否把无需求的错误过早提升到全局。
图与程序都使用本文自定数据和合同;未复制数据库实现,也不涵盖完整 SQL 语言、持久化、并发或生产性能调优。
参考资料
- 四个概念页分别链接其直接依据;三值、外连接、子查询和聚合接口以固定版本 PostgreSQL 18 文档为语义参照
- Neumann 与 Kemper,Unnesting Arbitrary Queries,§3.2:只有参数工作域去重,原 bag 仍保留;本文的空组修补、标量错误状态与具体成本为限定模型下的独立推导