用子查询其实就像在玩脱衣象棋:一开始看着挺简单,直到你下错一步就尴尬了。那这些错误都从哪来的?大多是因为没搞懂语法、忽略了 SQL 的逻辑细节,或者就是手滑没注意。这节课我们就聊聊最常见的错误,还有怎么避免它们。
语法错误
子查询对语法要求很严。漏了逗号、括号或者 alias,整个查询都能崩掉。来看看几个典型问题。
漏括号
括号在用子查询时超级重要。子查询要用圆括号包起来,哪怕少一对都能让 SQL 报语法错。
错误示例:
SELECT student_name
FROM students
WHERE student_id IN SELECT student_id FROM enrollments);
错误:
ERROR: syntax error at or near "SELECT"
修正:
SELECT student_name
FROM students
WHERE student_id IN (SELECT student_id FROM enrollments);
小贴士:IN 里的查询一定要用括号包起来,这样 SQL 才知道你要执行子查询。
缺少 alias
如果你在 FROM 里用子查询,记得一定要给它起个 alias。不然 PostgreSQL 会懵逼。
错误示例:
SELECT student_name, avg_score
FROM (SELECT student_id, AVG(score) AS avg_score FROM grades GROUP BY student_id)
WHERE avg_score > 80;
错误:
ERROR: subquery in FROM must have an alias
修正:
SELECT student_name, avg_score
FROM (SELECT student_id, AVG(score) AS avg_score FROM grades GROUP BY student_id) AS subquery
WHERE avg_score > 80;
小贴士:PostgreSQL 要求每个临时表(FROM 里的子查询结果)都得有个名字。
性能问题
子查询,尤其是没优化好的那种,能让你的数据库变得超级慢。性能问题大多是因为重复计算或者没加索引。
重复计算
SELECT 里的子查询会对每一行都算一遍,时间成本爆炸。
示例:
SELECT student_name,
(SELECT COUNT(*) FROM enrollments WHERE enrollments.student_id = students.student_id) AS course_count
FROM students;
如果 students 表有几万行,这个子查询会每行都重新跑一遍。
优化:
WITH course_counts AS (
SELECT student_id, COUNT(*) AS course_count
FROM enrollments
GROUP BY student_id
)
SELECT s.student_name, c.course_count
FROM students s
LEFT JOIN course_counts c ON s.student_id = c.student_id;
CTE(Common Table Expression)或者 join 能避免每行都重复算。这个方法我们后面还会讲到 :P
没加索引
如果你在 WHERE 里用复杂子查询,记得要给相关字段加索引。
示例:
SELECT student_name
FROM students
WHERE student_id IN (SELECT student_id FROM enrollments WHERE course_id = 10);
如果 enrollments 表里的 student_id 字段没索引,子查询会全表扫描,超级慢。
优化: 建个索引:
CREATE INDEX idx_enrollments_course_id ON enrollments (course_id);
你是不是已经听过无数次“要加索引”了?但别急,后面还会详细讲。索引很重要,不过它们主要是帮你加速查询,不是让烂查询变好查询。它们能让你的查询在 production 上飞起来,尤其是表有几百万行的时候。
逻辑错误
子查询里的逻辑错误其实也很常见。比如没搞懂 NULL 怎么处理、过滤条件写错、聚合函数用错等等。
NULL 用错了
NULL 是个大坑,专坑新手。用 IN 或 NOT IN 做子查询时,NULL 的存在会影响结果。
错误示例:
SELECT student_name
FROM students
WHERE student_id NOT IN (SELECT student_id FROM enrollments);
如果 enrollments 表里有 student_id = NULL 的行,这个查询啥都查不出来。因为 NOT IN 条件会变成 NULL IS NOT IN,结果就全挂了。
修正:
SELECT student_name
FROM students
WHERE student_id NOT IN (SELECT student_id FROM enrollments WHERE student_id IS NOT NULL);
用 NOT IN 时一定要先过滤掉 NULL。
过滤条件写错
子查询经常用来做复杂过滤,但条件写错了,结果就完全不是你想要的。
错误示例:
SELECT student_name
FROM students
WHERE (SELECT AVG(score) FROM grades WHERE grades.student_id = students.student_id) > 80;
如果有学生没有成绩,子查询会返回 NULL,这个学生就查不出来了。
修正:
SELECT student_name
FROM students
WHERE COALESCE((SELECT AVG(score) FROM grades WHERE grades.student_id = students.student_id), 0) > 80;
用 COALESCE 把 NULL 换成默认值。
防止出错的小建议
想避免用子查询时踩坑,记住这些:
括号和 alias 要用对。 有问题先检查括号是不是都闭合了,子查询有没有 alias。
优化查询。 能用 JOIN、WITH 或索引的地方就别用子查询。
注意 NULL。 子查询里可能有 NULL,要用 IS NOT NULL、COALESCE 或类似的写法处理。
多测试。 每个子查询都单独跑一下,看看结果是不是你想要的。
代码要好读。 多用缩进和 alias,让你的代码更清楚。记住:一个月后你自己都可能看不懂自己写的啥。
GO TO FULL VERSION