那条花了我 3 小时调试的 SQL 查询(别犯我的错误)
我花了一整个下午在我的数据库里追一个幽灵。罪魁祸首是什么?一个基本的格式化工具本来可以在五分钟内发现的问题。
让我描述一下那个场景。星期四下午 2 点。我手边有咖啡、降噪耳机,以及一条需要关联四个表并在下午 5 点前生成报告 SQL 查询。没什么大不了的,对吧?
经典的最后一句话。
地狱般的查询
那条查询大约 120 行。不算特别长,但也不短。它有嵌套子查询、几个 CTE、比婚宴还多的 JOIN——而现在我意识到,完全没有格式化。就是一面文字墙。大写的 KEYWORDS 紧贴着和小写的列名,没有空格、没有缩进、毫不留情。
大概就是这个样子(我已经匿名化了,但恐怖程度是一样的):
SELECT a.id, b.name, c.total, (SELECT COUNT(*) FROM orders o WHERE o.user_id = a.id AND o.status = 'active') as order_count FROM users a LEFT JOIN profiles b ON a.id = b.user_id INNER JOIN (SELECT user_id, SUM(amount) as total FROM transactions GROUP BY user_id) c ON a.id = c.user_id WHERE a.created_at > '2025-01-01' AND b.name IS NOT NULL AND (SELECT COUNT(*) FROM orders o WHERE o.user_id = a.id AND o.status = 'pending') > 5;眼睛疼吗?很好。我写它的时候就是这个感觉。但它能用。大部分时候。
那个 Bug
报告返回了 47 条记录。我预期大约 1200 条。某些条件筛掉了几乎所有的用户,但我怎么也找不到原因。我盯着那团乱码看了一个小时。我加了注释,分成几部分,对每个 CTE 单独 SELECT *。什么都没发现。
到第二小时,我确信数据库闹鬼了。我开始嘟囔"数据幽灵"。我的同事 Sarah 给了我一个表情,意思是请你回家吧。
解决方案
然后,在极度绝望中,我把整个代码粘贴到了我们的 SQL formatter 里。不是因为我觉得这会有用——只是因为我希望在我失败的时候缩进能漂亮一点。
然后我立刻就发现了。就在眼前,明明白白。
那个 AND b.name IS NOT NULL 子句。它位于 INNER JOIN 的子查询作用域内,但我没有缩进它,所以没看到它排除了所有没有个人资料也没有待处理订单的用户。实际上,由于缺少一对括号导致的运算符优先级问题,它被应用到了 WHERE 子句上,结果删除了四分之三的结果。
粘贴到格式化工具后五分钟,我就修复了那个 bug。三个小时的生命,就这样没了。就因为少了一些空白字符。
别学我
我知道格式化感觉像是"锦上添花"。不,它不是。它就像系安全带——无聊得很,直到你出了事故。在运行 SQL 之前先格式化它。不是之后。如果我一开始就格式化了那条查询,我大概八秒钟就能发现那个糟糕的括号问题,而不是花三个小时。
使用 SQL formatter。早点用。经常用。你晚上的自己会感谢你的——而且你不会因为少写了一个括号而去怪罪幽灵。