1. PostgreSQL子查询的本质与分类
在PostgreSQL中,子查询(Subquery)是指嵌套在另一个SQL语句中的SELECT查询。它们就像是SQL语句中的"迷你查询",可以出现在SELECT、FROM、WHERE、HAVING甚至ORDER BY子句中。理解子查询的工作原理是优化它们的前提。
子查询主要分为两大类:相关子查询和非相关子查询。非相关子查询是完全独立的,不引用外部查询的任何列。例如:
sql复制SELECT name
FROM employees
WHERE department_id IN (SELECT id FROM departments WHERE location = 'New York');
这个子查询(SELECT id FROM departments WHERE location = 'New York')可以独立执行,不需要知道外部employees表的任何信息。
而相关子查询则依赖于外部查询的值,例如:
sql复制SELECT e.name,
(SELECT MAX(salary) FROM salaries WHERE employee_id = e.id) AS max_salary
FROM employees e;
这里的子查询(SELECT MAX(salary)...)需要外部查询中每一行的e.id值才能执行。
从性能角度看,非相关子查询通常不是问题,因为PostgreSQL可以预先计算它们的结果(称为"InitPlan")。而相关子查询则可能成为性能瓶颈,因为它们需要为外部查询的每一行执行一次。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 标量子查询的性能陷阱与优化策略
标量子查询是指返回单个值的子查询,通常出现在SELECT列表或WHERE条件中。它们是最容易导致性能问题的子查询类型。
2.1 为什么标量子查询性能差
PostgreSQL执行标量子查询时,只能采用嵌套循环(Nested Loop)的方式。对于外部查询的每一行,都要执行一次子查询。如果外部表有100万行,子查询就会被执行100万次!
考虑这个例子:
sql复制SELECT product_name,
(SELECT AVG(price) FROM prices WHERE product_id = products.id) AS avg_price
FROM products;
即使products表只有1000行,而prices表有1000万行,这个查询也会对products的每一行扫描一次prices表。
2.2 标量子查询的重写技巧
最有效的优化方法是将标量子查询转换为JOIN操作。上面的例子可以重写为:
sql复制SELECT p.product_name, avg_p.avg_price
FROM products p
LEFT JOIN (
SELECT product_id, AVG(price) AS avg_price
FROM prices
GROUP BY product_id
) avg_p ON p.id = avg_p.product_id;
这种写法允许PostgreSQL使用更高效的哈希连接(Hash Join)或合并连接(Merge Join)策略。我在实际项目中测试过,对于大表这种改写通常能带来10-100倍的性能提升。
提示:使用LEFT JOIN而不是INNER JOIN可以保持与原查询相同的语义,即使某些产品没有价格记录也会返回NULL而不是过滤掉该行。
2.3 复杂标量子查询的优化
有时标量子查询包含复杂的条件,直接转为JOIN可能不太直观。例如:
sql复制SELECT o.order_id,
(SELECT SUM(amount)
FROM payments
WHERE order_id = o.id
AND status = 'completed'
AND payment_date BETWEEN o.create_date AND
