LabHub
学习 学习路径 课程

处理客户数据

最危险的 SQL 不报错

在 LabHub 中继续学习

一句话总结

SQL 中真正可怕的不是语法错误,而是三个不会报错、却会返回貌似合理的错误结果的陷阱。

概念图: 第一,NULL 与 NOT IN。 · 哪怕有一条 · 未知 · 第三,附加在 LEFT JOIN 后的 WHERE。

为什么需要它

查询语法错误会立即被指出。真正危险的是能够执行的查询。如果你在客户面前读出一个数字,后来却发现它是错的,那么此后这个项目给出的所有数字都会受到怀疑。

FDE 尤其容易暴露在这种风险下,因为面对的是别人的模式。你不知道哪些列允许 NULL、哪些关系是 1:N,也不知道哪个值代表逻辑删除,却必须写出查询。

它如何运作

记住三种不会报错却会算错的典型情况,就能避开大多数问题。

**第一,NULL 与 NOT IN。**假设我们要找出从未下过订单的客户。

SELECT * FROM customers WHERE id NOT IN (SELECT customer_id FROM orders);

只要 orders.customer_id哪怕有一条 NULL,这个查询就会返回 0 行。既不报错,也不警告。编写报告的人随后会原样写下一个在业务上似乎合理的结论:“没有任何客户从未下过订单。”

原因是三值逻辑。id NOT IN (1, 2, NULL) 的结果不是真,而是未知,未知无法通过 WHERE。解决办法是 NOT EXISTS。它只判断是否返回行,因此不会被 NULL 绊倒。养成使用 NOT EXISTS 的习惯更安全。

**第二,COUNT(*)COUNT(열)。**两者并不相同。COUNT(*) 统计行数,而 COUNT(열) 只统计该列不是 NULL 的行。

这种差异最常出现在 LEFT JOIN 中。没有任何工单的客户,在连接结果中会表现为一行工单侧各列均为 NULL 的记录。此时 COUNT(t.id) 得到 0,而 COUNT(*) 得到 1。使用后者,就会把没有工单的客户统计成拥有一张工单。

同理,COUNT(closed_at) 只统计已关闭的工单。如果全部工单有 80 张,而这个值是 60,就表示仍有 20 张未关闭。理解后使用,它是便利的惯用法;不理解就使用,它会悄无声息地给出错误答案。

**第三,附加在 LEFT JOIN 后的 WHERE。**本来为了保留左表全部数据而使用 LEFT JOIN,却在 WHERE 子句中加入右表列的条件,那么没有匹配项的行会立刻全部被淘汰。因为 NULL 与任何值比较都不为真。结果便等同于 INNER JOIN。针对右表的条件不应放在 WHERE 中,而应该放进 ON 子句。

在实际工作中会遇到的情况

这里再补充一个 sqlite 特有的陷阱。用 .import 导入 CSV 时,**所有值都会以 TEXT 形式进入。**于是 WHERE id = 1001 会返回 0 行,因为保存的是字符串 '1001'。连接也会以同样方式悄无声息地返回 0 行。

如果导入 CSV 后连接结果空空如也,原因几乎总在这里。应当先用 CREATE TABLE 定义类型再导入,或者查询时使用 CAST

最后还有一个实务习惯。把聚合查询展示给客户之前,要检查分母。总共有多少条,其中多少条符合条件。只报告比例,就无法知道是五条中的一条,还是五万条中的一万条,而这两种情况的含义完全不同。

给出数字之前要做的检查

即使避开了前述三个陷阱,如果这个数字要在客户面前引用,还要再做一步: 你将如何证明这个数字正确?

**换一种方法再数一次。**用连接算出的合计,也用不连接的子查询再算一遍, 确认两个值是否相同。两种方法得出相同答案,出错的可能性会大幅降低; 如果不同,差异本身也会告诉你遗漏了什么。连接 1:N 关系后再汇总,导致一侧数据膨胀的事故会在这里被发现。

**同时写出分母和分子。**这与前面的建议相同,但写进报告时应更进一步。 不要只写“转化率 20%”,而要写“5 条中 1 条(20%)”, 这样读者可以自行判断这个数字有多可信。

写明时间范围和基准时刻。“上个月销售额”每个人可能会有不同理解。 必须写清按哪一列划分(下单时间还是付款时间)、采用哪个时区, 以及是否包含边界,后续人员才能再次得到同一个数字。

**写明排除项。**如果排除了测试账号、已取消订单或内部员工请求, 就同时记录这一事实及其数量。排除本身通常没有问题,但若不注明, 与别人给出的数字不一致时,往往要花几个小时寻找原因。

最后,**保留查询本身。**如果只交付结果,几周后这个数字就无法复现。 把查询和执行时刻一起留下,之后有人问“这个数是怎么得出的”时, 几秒钟就能回答;更重要的是,自己再次查看时也知道当初采用了什么假设。

后续实验要做什么

把客户支持系统的快照导入 sqlite,从客户数、已确认收入、收入最高的客户等普通问题开始,再回答需要 NULL 聚合和 LEFT JOIN 的问题。最后一个问题的答案可能会与你预想的不同。