知行札记
专题技术实践数据库实践

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

原记录结果:

idpayment_id形成原因
110支付 10 同时满足两个配对条件
2NULL没有任何支付配对
3NULL取消记录不满足整个 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

  1. SQLite,SELECT,外连接补值、WHERE 与示意步骤;2026-10-05 核对。 ↩ ↩2

  2. SQLite,SQL Language Expressions,NULL 和 IS 谓词;2026-10-05 核对。 ↩

最后更新于

本页目录