SQL 连接边界实验室:ON、WHERE 与 HAVING 的三层过滤
SQL 查询里,条件放在哪里会改变结果的形状。把 status = ‘paid’ 放进 LEFT JOIN … ON,可以保留没有已支付订单的用户;把它放进 WHERE,这些用户可能整行消失;当结果按用户分组时,HAVING 又会在分组之后过滤用户组。
一组固定数据
四个用户里,小林有一笔已支付和一笔未支付订单,小周只有未支付订单,小陈没有订单,小吴的订单状态是 NULL。另有一笔孤立订单,userId 不属于左表。
CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT NOT NULL);
CREATE TABLE orders (id INTEGER PRIMARY KEY, userId INTEGER, status TEXT);
INSERT INTO users VALUES (1, '小林'), (2, '小周'), (3, '小陈'), (4, '小吴');
INSERT INTO orders VALUES (101, 1, 'paid'), (102, 1, 'unpaid'), (103, 2, 'unpaid'), (104, 4, NULL), (105, 99, 'paid');
条件放进 ON:左表仍然完整
SELECT u.id, u.name, o.id AS orderId, o.status
FROM users u
LEFT JOIN orders o ON u.id = o.userId AND o.status = 'paid'
ORDER BY u.id, o.id;
连接阶段只把已支付订单视为匹配。小林得到订单 101;小周、小陈和小吴没有符合条件的订单,各自得到一行由 LEFT JOIN 补出的 NULL。这正好表达“列出所有用户,并附带已支付订单”。
条件放进 WHERE:补齐行也会被过滤
SELECT u.id, u.name, o.id AS orderId, o.status
FROM users u
LEFT JOIN orders o ON u.id = o.userId
WHERE o.status = 'paid'
ORDER BY u.id, o.id;
这里先连接全部订单,再过滤 status。小周的真实订单是 unpaid,小吴的状态是 NULL,小陈的补齐行也是 NULL。对于 NULL = ‘paid’,结果是 UNKNOWN;WHERE 只保留 TRUE,所以这三个人都会消失。
如果同时要求订单编号为空,只能救回完全没有订单的小陈。小周和小吴有真实订单,订单编号非空,仍然无法通过。
分组之后再用 HAVING
如果需求变成“只保留至少有一笔已支付订单的用户组”,可以把支付条件放进 ON,再按用户分组:
SELECT u.id, u.name, COUNT(o.id) AS paidCount
FROM users u
LEFT JOIN orders o ON u.id = o.userId AND o.status = 'paid'
GROUP BY u.id, u.name
HAVING COUNT(o.id) > 0;
WHERE 过滤单行,HAVING 过滤分组。连接后的补齐行仍然存在,但补齐行的订单编号为空,不会被 COUNT 计数;因此只有小林这一组通过 HAVING。如果使用 COUNT(*),补齐行也会被计入,语义就变成每个用户组至少有一行结果。
用实验观察三值逻辑
配套的 SQL 连接边界实验室把五种写法并排展示,并增加聚合后的支付订单数大于 0 结果。可以切换一对多订单、空右表和全 NULL 状态,观察真实订单、补齐行、UNKNOWN 与组级过滤的区别。
实验页面只使用固定内存数据,不连接生产数据库、不上传输入,也不执行任意 SQL。它是语义教学工具;真实系统仍应结合约束、数据质量、索引和执行计划验证方案。
参考 PostgreSQL 的表表达式和 SQLite 的 SELECT 文档。
- 点赞
- 收藏
- 关注作者
评论(0)