【重学 MySQL】四十四、相关子查询
- 相关子查询执行流程
- 示例
- 使用相关子查询进行过滤
- 使用相关子查询进行存在性检查
- 使用相关子查询进行计算
- 在 `select`,`from`,`where`,`having`,`order by` 中使用相关子查询举例
- `SELECT` 子句中使用相关子查询
- `FROM` 子句中使用相关子查询
- `WHERE` 子句中使用相关子查询
- `HAVING` 子句中使用相关子查询
- `ORDER BY` 子句中使用相关子查询
- 总结
- `EXISTS` 和 `NOT EXISTS`
- `EXISTS`
- `NOT EXISTS`
- 关键点
- 注意事项
- 替代方法
在 MySQL 中,相关子查询(也称为相关子查询或关联子查询)是一种特殊类型的子查询,其执行依赖于外部查询的当前行值。这意味着相关子查询在外部查询的每一行上都会重新执行一次,并且可以使用外部查询的列值。
相关子查询执行流程
相关子查询的执行流程涉及多个步骤,并且这些步骤在数据库管理系统(DBMS)中是高度优化的。
-
解析和优化:
- 数据库管理系统首先解析SQL语句,包括相关子查询,以确保其符合语法规则。
- 接着,系统进行语义解析,检查表名、列名、数据类型、权限等约束条件是否满足。
- 对于包含相关子查询的查询语句,DBMS会尝试找到最优的查询计划,以便快速地从数据库中检索所需的数据。这包括选择最佳的索引、使用缓存和预处理语句等优化措施。
-
生成执行计划:
- 在查询优化后,系统会生成一个执行计划,该计划描述了如何获取查询结果,包括访问哪些表、采用哪些索引、如何连接各个表等。
- 对于相关子查询,执行计划会考虑子查询与外部查询之间的依赖关系,并确定子查询的执行时机和方式。
-
执行外部查询:
- 外部查询(即包含相关子查询的查询)开始执行。在外部查询的每一行处理过程中,都会涉及到相关子查询的执行。
-
执行相关子查询:
- 对于外部查询中的每一行,DBMS都会执行一次相关子查询。
- 相关子查询依赖于外部查询的当前行值。这意味着,每次外部查询处理一行数据时,子查询都会使用该行数据中的值作为条件来执行。
- 子查询的结果通常用于过滤、排序或作为外部查询的一部分进行计算。
-
组合结果:
- 外部查询根据子查询的结果来处理每一行数据,并生成最终的查询结果集。
- 如果子查询返回多个结果,外部查询可能会使用这些结果来进行进一步的过滤或计算。
-
返回结果:
- 最后,数据库将查询结果集返回给客户端应用程序。
需要注意的是,相关子查询可能会导致性能问题,因为对于外部查询返回的每一行数据,数据库都需要重新执行子查询。因此,在编写包含相关子查询的SQL语句时,应谨慎考虑其性能影响,并尝试使用其他优化技术(如索引、连接优化、窗口函数等)来提高查询效率。
此外,虽然相关子查询在某些情况下非常有用,但在其他情况下,使用连接(JOIN)操作或窗口函数可能更加高效和直观。因此,在选择使用哪种查询技术时,应根据具体需求和性能考虑做出决策。
示例
使用相关子查询进行过滤
假设我们有两个表:employees
(员工)和 departments
(部门)。我们想要找到每个部门中工资最高的员工。
SELECT e.name, e.salary, e.department_id
FROM employees e
WHERE e.salary = (
SELECT MAX(sub.salary)
FROM employees sub
WHERE sub.department_id = e.department_id
);
在这个查询中,子查询 SELECT MAX(sub.salary) FROM employees sub WHERE sub.department_id = e.department_id
是一个相关子查询,因为它依赖于外部查询的 e.department_id
。
使用相关子查询进行存在性检查
假设我们有两个表:students
(学生)和 courses
(课程)。我们想要找到那些选修了所有课程的学生。
SELECT s.name
FROM students s
WHERE NOT EXISTS (
SELECT c.course_id
FROM courses c
WHERE NOT EXISTS (
SELECT 1
FROM enrollments e
WHERE e.student_id = s.student_id AND e.course_id = c.course_id
)
);
在这个查询中,内部子查询 SELECT 1 FROM enrollments e WHERE e.student_id = s.student_id AND e.course_id = c.course_id
是一个相关子查询,它依赖于外部子查询的 c.course_id
和外部查询的 s.student_id
。
使用相关子查询进行计算
假设我们有一个表 sales
,其中包含每个销售员的销售记录。我们想要计算每个销售员的销售总额,并找出销售额超过该销售员平均销售额的记录。
SELECT s.salesperson_id, s.sale_amount
FROM sales s
WHERE s.sale_amount > (
SELECT AVG(sub.sale_amount)
FROM sales sub
WHERE sub.salesperson_id = s.salesperson_id
);
在这个查询中,子查询 SELECT AVG(sub.sale_amount) FROM sales sub WHERE sub.salesperson_id = s.salesperson_id
是一个相关子查询,因为它依赖于外部查询的 s.salesperson_id
。
在 select
,from
,where
,having
,order by
中使用相关子查询举例
在SQL查询中,相关子查询(也称为相关子选择或相关嵌套查询)是指依赖于外部查询中的值的子查询。它们通常用于在SELECT
、FROM
、WHERE
、HAVING
和ORDER BY
子句中实现复杂的逻辑。以下是一些示例,展示了如何在这些子句中使用相关子查询。
SELECT
子句中使用相关子查询
虽然直接在SELECT
子句中使用相关子查询不太常见,但你可以通过派生表(子查询作为表)间接实现。不过,这里展示一个更直接的场景,即在SELECT
中嵌入相关子查询作为计算列。
SELECT
employee_id,
first_name,
last_name,
(SELECT COUNT(*)
FROM orders
WHERE orders.employee_id = employees.employee_id) AS order_count
FROM
employees;
这个查询为每个员工返回了一个订单计数。
FROM
子句中使用相关子查询
在FROM
子句中使用相关子查询通常通过派生表(子查询作为临时表)来实现,但相关子查询在这种场景下不常见。然而,你可以通过JOIN
与WHERE
条件实现类似的效果。
select e.last_name, e.salary, e.department_id
from employees e, (select department_id, avg(salary) avg_salary
from employees
group by department_id) t_dept_avg_salary
where e.department_id = t_dept_avg_salary.department_id
and e.salary > t_dept_avg_salary.avg_salary;
它使用了隐式内连接(也称为笛卡尔积加过滤)来比较每个员工的工资与其所在部门的平均工资。这里,您创建了一个派生表(也称为子查询或临时表)t_dept_avg_salary
,该表包含了每个部门的平均工资。然后,您将这个派生表与employees
表连接起来,以便比较每个员工的工资与其部门的平均工资。
-
派生表
t_dept_avg_salary
:(select department_id, avg(salary) avg_salary from employees group by department_id)
这个子查询从
employees
表中计算每个部门的平均工资,并将结果作为一个临时表(派生表)。这个表有两列:department_id
(部门ID)和avg_salary
(该部门的平均工资)。 -
主查询:
select e.last_name, e.salary, e.department_id from employees e, (子查询) t_dept_avg_salary where e.department_id = t_dept_avg_salary.department_id and e.salary > t_dept_avg_salary.avg_salary;
主查询从
employees
表(别名为e
)和派生表t_dept_avg_salary
中选择数据。它通过department_id
将这两个表连接起来,并过滤出那些工资高于其部门平均工资的员工。 -
结果:
查询结果将包含那些工资高于其所在部门平均工资的员工的姓氏(last_name
)、工资(salary
)和部门ID(department_id
)。
虽然您的查询在功能上是正确的,但现代SQL风格通常推荐使用显式的JOIN
语法来替代隐式连接,因为它更清晰且更易于维护。以下是使用显式JOIN
的等效查询:
SELECT e.last_name, e.salary, e.department_id
FROM employees e
JOIN (
SELECT department_id, AVG(salary) AS avg_salary
FROM employees
GROUP BY department_id
) t_dept_avg_salary ON e.department_id = t_dept_avg_salary.department_id
WHERE e.salary > t_dept_avg_salary.avg_salary;
这个查询与您的原始查询在逻辑上是相同的,但使用了显式的JOIN
语法,这通常被认为是更好的做法。
WHERE
子句中使用相关子查询
在WHERE
子句中使用相关子查询非常常见,用于过滤记录。
SELECT
employee_id,
first_name,
last_name
FROM
employees e
WHERE
(SELECT COUNT(*)
FROM orders o
WHERE o.employee_id = e.employee_id) > 5;
这个查询返回了订单数量超过5的员工。
HAVING
子句中使用相关子查询
HAVING
子句通常用于聚合查询的过滤,但在HAVING
中使用相关子查询的情况较少。这里通过一个例子展示如何在HAVING
中嵌入相关子查询。
SELECT
department_id,
COUNT(employee_id) AS employee_count
FROM
employees
GROUP BY
department_id
HAVING
COUNT(employee_id) > (SELECT AVG(emp_count)
FROM (SELECT COUNT(employee_id) AS emp_count
FROM employees
GROUP BY department_id) AS avg_dept_counts);
这个查询返回了员工数量超过所有部门平均员工数量的部门。
ORDER BY
子句中使用相关子查询
在ORDER BY
子句中使用相关子查询的情况也不常见,但可以通过派生表或窗口函数实现类似效果。不过,直接嵌入相关子查询也可以在某些特殊情况下使用。
SELECT
employee_id,
first_name,
last_name,
salary
FROM
employees
ORDER BY
(SELECT AVG(salary)
FROM employees
WHERE department_id = employees.department_id) DESC,
salary DESC;
这个查询首先按部门平均工资降序排序,然后按员工个人工资降序排序。
总结
相关子查询在SQL查询中非常强大,可以用于实现复杂的逻辑。然而,它们可能会降低查询性能,特别是在处理大量数据时。因此,在使用相关子查询时,应考虑其性能影响,并考虑使用其他优化技术,如索引、连接优化或窗口函数等。
EXISTS
和 NOT EXISTS
EXISTS
和 NOT EXISTS
是 SQL 中用于测试子查询是否返回任何行的条件运算符。它们通常用于在 WHERE
子句或 HAVING
子句中,以确定是否满足某个条件,从而决定是否包含某些行在结果集中。
EXISTS
EXISTS
运算符用于测试子查询是否返回至少一行。如果子查询返回一行或多行,EXISTS
条件就为真(TRUE),否则为假(FALSE)。
示例:
SELECT first_name, last_name
FROM employees e
WHERE EXISTS (
SELECT 1
FROM departments d
WHERE e.department_id = d.department_id
AND d.department_name = 'Sales'
);
这个查询返回了所有在名为 ‘Sales’ 的部门工作的员工的名字。子查询检查是否存在至少一个部门,其 department_id
与 employees
表中的 department_id
匹配,并且部门名称为 ‘Sales’。
NOT EXISTS
NOT EXISTS
运算符用于测试子查询是否不返回任何行。如果子查询没有返回任何行,NOT EXISTS
条件就为真(TRUE),否则为假(FALSE)。
示例:
SELECT first_name, last_name
FROM employees e
WHERE NOT EXISTS (
SELECT 1
FROM departments d
WHERE e.department_id = d.department_id
AND d.department_name = 'HR'
);
这个查询返回了所有不在名为 ‘HR’ 的部门工作的员工的名字。子查询检查是否不存在任何部门,其 department_id
与 employees
表中的 department_id
匹配,并且部门名称为 ‘HR’。
关键点
EXISTS
和NOT EXISTS
子查询通常只关心是否存在行,而不关心行的具体内容。因此,子查询中的SELECT
子句经常简单地选择常量(如SELECT 1
),因为实际选择的列并不重要。- 这些运算符通常比使用
IN
、NOT IN
、JOIN
(在某些情况下)等替代方法更高效,特别是当子查询可能返回大量行时。 - 使用
EXISTS
和NOT EXISTS
时,应确保子查询中的条件能够正确地反映你想要测试的逻辑。 - 在某些数据库系统中,
EXISTS
和NOT EXISTS
可能会利用索引来优化查询性能。因此,在设计数据库和编写查询时,考虑索引的使用是很重要的。
注意事项
-
性能问题:由于相关子查询在外部查询的每一行上都会重新执行,因此可能会导致性能问题,特别是在处理大数据集时。在这种情况下,可以考虑使用 JOIN 或其他优化技术。
-
可读性:相关子查询有时可能使查询变得难以理解和维护。因此,在编写复杂查询时,确保代码清晰并添加适当的注释。
-
索引:确保在相关子查询中使用的列上建立适当的索引,以提高查询性能。
替代方法
在某些情况下,可以使用 JOIN 或窗口函数(MySQL 8.0+ 支持)来替代相关子查询,从而获得更好的性能和可读性。例如,上面的第一个示例(找到每个部门中工资最高的员工)可以使用 JOIN 和 GROUP BY 来重写:
SELECT e1.name, e1.salary, e1.department_id
FROM employees e1
JOIN (
SELECT department_id, MAX(salary) AS max_salary
FROM employees
GROUP BY department_id
) e2 ON e1.department_id = e2.department_id AND e1.salary = e2.max_salary;
这种重写方式通常更高效,因为它避免了相关子查询的重复执行。
通过理解和使用相关子查询,你可以解决一些复杂的查询问题。然而,要注意性能问题,并考虑使用其他技术来优化查询。