SQLite:外连接、条件位置与结果行
用完整输入与三个查询解释 ON、WHERE、NULL 补值和一对多结果。
目标是保留全部客户,并列出他们的已支付记录。支付条件的位置会改变结果:一项条件可以决定配对,也可以决定哪些结果行被保留。本页用同一组输入比较这两种作用。
案例沿用 2026-10-04 原材料的 Python 3.14.6、SQLite 3.53.4 内存数据库记录。本轮阅读了查询、输入和断言,并核对 SQLite 官方的语义说明;未重跑该历史环境,也未把结果认定为 PostgreSQL 等其他实现的运行结果。
输入和需要保留的关系
以下 DDL 与迁移后的脚本一致。customer_id 与 status 为 NOT NULL,使后续 p.customer_id IS NULL 能在这个构造中识别外连接补值行。示例没有声明外键,客户和支付的对应关系由输入构造保证。
CREATE TABLE customers (id INTEGER PRIMARY KEY);
CREATE TABLE payments (
id INTEGER PRIMARY KEY,
customer_id INTEGER NOT NULL,
status TEXT NOT NULL
);
INSERT INTO customers VALUES (1), (2), (3);
INSERT INTO payments VALUES
(10, 1, 'paid'),
(30, 3, 'cancelled');客户 1 有已支付记录,客户 2 完全没有支付记录,客户 3 只有取消记录。保留全部客户意味着后两种情况都需要出现在结果中;“没有支付”与“没有符合状态的支付”是两个不同输入条件。
将支付状态用于配对
SELECT c.id, p.id AS payment_id
FROM customers AS c
LEFT JOIN payments AS p
ON p.customer_id = c.id AND p.status = 'paid'
ORDER BY c.id, p.id;c、p 是表别名;SELECT 输出客户与支付的编号,ORDER BY 让结果便于逐行核对。ON 同时要求客户编号相同与支付状态为 paid。对没有任何匹配右表行的左表行,LEFT JOIN 保留一行,并将右表列补为 NULL。1
原记录结果:
| id | payment_id | 形成原因 |
|---|---|---|
| 1 | 10 | 支付 10 同时满足两个配对条件 |
| 2 | NULL | 没有任何支付配对 |
| 3 | NULL | 取消记录不满足整个 ON 条件,仍需保留客户 |
基础表列为 NOT NULL 不阻止结果列出现 NULL。约束约束的是存储值,外连接补值是查询结果的一部分。
将支付状态用于结果筛选
只按客户编号配对,再在 WHERE 中筛选状态:
SELECT c.id, p.id AS payment_id
FROM customers AS c
LEFT JOIN payments AS p ON p.customer_id = c.id
WHERE p.status = 'paid'
ORDER BY c.id, p.id;原记录只剩 (1,10)。客户 2 的补值行里 p.status = 'paid' 得到 NULL,即未知;WHERE 只保留条件为真的行。客户 3 已配上取消记录,条件为假。两行都被排除。2
理解这项变化可以按“配对、为未匹配客户补值、筛选结果”展开。SQLite 明确说明,文档中的步骤用于解释结果,执行引擎可以采用其他物理过程。扫描顺序、索引选择和条件下推需要另查计划与实现。1
OR NULL 能保留哪些客户
SELECT c.id, p.id AS payment_id
FROM customers AS c
LEFT JOIN payments AS p ON p.customer_id = c.id
WHERE p.status = 'paid' OR p.customer_id IS NULL
ORDER BY c.id, p.id;原记录为 (1,10), (2,NULL)。客户 2 的补值行满足 IS NULL,得以保留。客户 3 已按编号配上取消记录,因此连接时没有为它另补一行 NULL;paid 判断和 IS NULL 判断都不为真,它仍被排除。
这项写法表达的是“有已支付配对,或者完全没有按编号配对”。最初需求表达的是“所有客户,右侧仅展示已支付配对”。确定条件位置要先写清需要保留的左侧集合,以及右侧何时算匹配。
保留客户与每位客户一行
追加第二条已支付记录:
INSERT INTO payments VALUES (11, 1, 'paid');再次执行第一个 ON 查询,原记录为:
(1,10), (1,11), (2,NULL), (3,NULL)客户 1 出现两次,因为有两个符合条件的配对。若下游要求一位客户一行,还需明确行的业务含义:统计已支付金额、只判断是否存在、选取最近一笔,或把多笔记录组织为集合。对应方案分别涉及聚合、EXISTS、带确定排序的选取或集合构造;任意去重可能丢掉有用支付信息。
结果行数同样影响分页和统计。对连接结果 LIMIT 分页,并不等价于按客户分页;COUNT(*) 统计连接行,也不自动等于客户数。应先确定分页对象和计数对象,再选择查询结构。
记录与复算范围
构造案例脚本保存三种查询与追加配对的断言,同时含 Python 名称绑定案例。它只使用标准库与内存 SQLite,输出 Python/SQLite 版本及 JSON 结果,不依赖网站数据库。
历史记录支持以上四组结果。重新运行脚本产生的是所用 Python 内嵌 SQLite 的新记录,需要保留实际版本。脚本没有观察优化器内部步骤,没有测量读者理解,也没有验证其他数据库实现。
Footnotes
-
SQLite,SQL Language Expressions,NULL 和 IS 谓词;2026-10-05 核对。 ↩
最后更新于