SQL关联子查询是指子查询中引用了外层查询的字段,这类查询在部分场景下会被数据库引擎按外层查询的每一行单独执行一次子查询,类似在循环中反复执行查询逻辑,很容易造成性能问题。如果查询涉及的数据量较大,这种执行方式会让整体耗时急剧上升。

关联子查询的常见问题
我们先看一个典型的关联子查询示例,需求是查询每个部门中工资高于部门平均工资的员工信息:
-- 典型的关联子查询写法
SELECT
e1.dept_id,
e1.emp_name,
e1.salary
FROM employee e1
WHERE e1.salary > (
SELECT AVG(e2.salary)
FROM employee e2
WHERE e2.dept_id = e1.dept_id -- 子查询引用外层查询的dept_id
);
这种写法下,如果employee表有1000个部门,外层查询每处理一个部门的员工,都会执行一次子查询计算该部门的平均工资,相当于执行了1000次子查询,这就是典型的“循环执行查询”问题。
优化技巧一:改写为连接查询
大部分关联子查询都可以改写为连接查询,让数据库引擎一次性处理数据,避免循环执行。以上面的查询为例,改写后的写法如下:
-- 改写为连接查询
SELECT
e.dept_id,
e.emp_name,
e.salary
FROM employee e
INNER JOIN (
SELECT
dept_id,
AVG(salary) AS avg_salary
FROM employee
GROUP BY dept_id
) dept_avg ON e.dept_id = dept_avg.dept_id
WHERE e.salary > dept_avg.avg_salary;
改写后子查询只执行一次,计算出所有部门的平均工资,再通过连接匹配数据,执行次数从“外层行数”降为1次,性能提升非常明显。
优化技巧二:合理使用索引
如果关联子查询无法改写,或者改写后性能仍不理想,可以通过添加索引减少子查询的执行成本。以上面的关联子查询为例,给dept_id和salary字段添加联合索引可以大幅提升子查询的执行效率:
-- 添加联合索引 CREATE INDEX idx_dept_salary ON employee(dept_id, salary);
索引可以让子查询在计算平均工资时快速定位到对应部门的记录,不用全表扫描,即使多次执行子查询,单次执行的成本也会大幅降低。
优化技巧三:拆分复杂关联子查询
如果关联子查询逻辑非常复杂,嵌套多层或者涉及多个表的关联,可以拆分查询为多个步骤,先通过临时表或者CTE(公用表表达式)计算出子查询的结果,再和外层查询关联:
-- 使用CTE拆分查询
WITH dept_avg_salary AS (
SELECT
dept_id,
AVG(salary) AS avg_salary
FROM employee
GROUP BY dept_id
)
SELECT
e.dept_id,
e.emp_name,
e.salary
FROM employee e
INNER JOIN dept_avg_salary d ON e.dept_id = d.dept_id
WHERE e.salary > d.avg_salary;
这种方式可以让数据库更清晰地识别执行逻辑,部分数据库引擎会对CTE做优化,避免重复计算子查询的结果。
优化技巧四:避免不必要的关联字段引用
编写关联子查询时,要检查子查询是否真的需要引用外层查询的字段,如果子查询的结果和外层查询无关,尽量改成无关子查询,让数据库引擎可以提前执行子查询,不用跟随外层循环执行:
-- 不必要的关联子查询示例
SELECT
emp_name,
salary
FROM employee
WHERE dept_id IN (
SELECT dept_id
FROM department
WHERE dept_name = '技术部'
AND dept_id = employee.dept_id -- 这里其实是多余的引用
);
-- 优化后改为无关子查询
SELECT
emp_name,
salary
FROM employee
WHERE dept_id IN (
SELECT dept_id
FROM department
WHERE dept_name = '技术部'
);
验证优化效果
优化完成后,可以通过数据库的执行计划查看查询的实际执行逻辑,确认子查询没有被重复执行。以MySQL为例,使用EXPLAIN关键字查看执行计划:
-- 查看执行计划
EXPLAIN
SELECT
e.dept_id,
e.emp_name,
e.salary
FROM employee e
INNER JOIN (
SELECT
dept_id,
AVG(salary) AS avg_salary
FROM employee
GROUP BY dept_id
) dept_avg ON e.dept_id = dept_avg.dept_id
WHERE e.salary > dept_avg.avg_salary;
执行计划中出现“DERIVED”表示子查询被物化成了临时表,只执行一次,没有出现“DEPENDENT SUBQUERY”就说明已经避免了循环执行子查询的问题。