SQL子查询本质上是一个嵌套在另一个SQL语句中的完整查询块。它就像俄罗斯套娃,大查询包裹着小查询,小查询的结果作为大查询的数据源或条件依据。根据子查询返回的数据类型和出现位置,我们可以将其分为以下几类:
标量子查询:返回单一值的子查询,通常出现在SELECT列表、WHERE条件或HAVING子句中。例如获取高于平均工资的员工信息:
sql复制SELECT name, salary
FROM employees
WHERE salary > (SELECT AVG(salary) FROM employees)
列子查询:返回单列多行数据的子查询,常与IN、ANY/SOME、ALL等运算符配合使用。例如查找没有订单的客户:
sql复制SELECT customer_id, name
FROM customers
WHERE customer_id NOT IN (SELECT DISTINCT customer_id FROM orders)
行子查询:返回单行多列数据的子查询,需要与行构造符配合使用。例如查找与特定员工部门和职位都相同的其他员工:
sql复制SELECT employee_id, name
FROM employees
WHERE (department, position) =
(SELECT department, position FROM employees WHERE employee_id = 101)
表子查询:返回完整表结构的子查询,通常出现在FROM子句中作为派生表。例如统计各部门薪资最高的员工:
sql复制SELECT e.department, e.name, e.salary
FROM employees e
JOIN (
SELECT department, MAX(salary) as max_salary
FROM employees
GROUP BY department
) dept_max ON e.department = dept_max.department AND e.salary = dept_max.max_salary
注意:MySQL 5.7及以下版本对子查询的性能优化有限,复杂子查询建议拆分为JOIN操作。8.0版本引入的优化器改进显著提升了子查询执行效率。
非相关子查询(独立子查询)的执行不依赖外部查询,可以独立运行。这类查询通常先执行子查询,然后将结果传递给外部查询。例如:
sql复制SELECT product_name, price
FROM products
WHERE price > (SELECT AVG(price) FROM products)
相关子查询则与外部查询存在数据关联,需要引用外部查询的字段。这类查询会对外部查询的每一行数据执行一次子查询。典型例子是查找各部门薪资高于本部门平均的员工:
sql复制SELECT e1.employee_id, e1.name, e1.salary, e1.department
FROM employees e1
WHERE salary > (
SELECT AVG(salary)
FROM employees e2
WHERE e2.department = e1.department
)
使用EXPLAIN命令可以观察MySQL如何处理子查询。对于上面的相关子查询示例,执行计划可能显示:
优化建议:
EXISTS子查询不返回实际数据,只检查是否存在满足条件的记录,非常适合存在性检查场景。例如查找至少有一个订单金额超过1000的客户:
sql复制SELECT customer_id, name
FROM customers c
WHERE EXISTS (
SELECT 1 FROM orders o
WHERE o.customer_id = c.customer_id AND o.amount > 1000
)
与IN子查询相比,EXISTS具有以下优势:
MySQL 8.0引入的CTE(Common Table Expression)支持递归查询,可以优雅地处理层级数据。例如查询组织架构中某个员工的所有下属:
sql复制WITH RECURSIVE emp_hierarchy AS (
-- 基础查询:获取初始员工
SELECT employee_id, name, manager_id
FROM employees
WHERE employee_id = 1001
UNION ALL
-- 递归查询:获取下属员工
SELECT e.employee_id, e.name, e.manager_id
FROM employees e
JOIN emp_hierarchy eh ON e.manager_id = eh.employee_id
)
SELECT * FROM emp_hierarchy;
IN转JOIN:将WHERE col IN (SELECT...)重写为INNER JOIN
sql复制-- 原始IN查询
SELECT * FROM products
WHERE category_id IN (SELECT category_id FROM categories WHERE type='ELECTRONICS')
-- 优化为JOIN
SELECT p.*
FROM products p
JOIN categories c ON p.category_id = c.category_id
WHERE c.type='ELECTRONICS'
EXISTS转JOIN:相关EXISTS查询通常可以转为LEFT JOIN+IS NOT NULL
sql复制-- 原始EXISTS
SELECT c.* FROM customers c
WHERE EXISTS (SELECT 1 FROM orders o WHERE o.customer_id = c.customer_id)
-- 优化版本
SELECT DISTINCT c.*
FROM customers c
LEFT JOIN orders o ON c.customer_id = o.customer_id
WHERE o.order_id IS NOT NULL
重点关注以下指标:
子查询中的NULL比较可能导致意外结果。例如:
sql复制SELECT * FROM table1
WHERE col1 NOT IN (SELECT col2 FROM table2 WHERE ...)
如果table2.col2包含NULL值,整个查询将返回空结果。安全写法:
sql复制SELECT * FROM table1
WHERE col1 NOT IN (SELECT col2 FROM table2 WHERE col2 IS NOT NULL AND ...)
使用子查询实现分页时,OFFSET过大将导致性能急剧下降:
sql复制-- 低效写法
SELECT * FROM large_table
ORDER BY id
LIMIT 10 OFFSET 1000000
-- 优化方案:记住上一页最后ID
SELECT * FROM large_table
WHERE id > last_seen_id
ORDER BY id
LIMIT 10
MySQL中用户变量在子查询中的行为可能不符合预期:
sql复制SET @rank=0;
SELECT (@rank:=@rank+1) AS rank, name FROM employees;
-- 子查询中的@rank可能不会按预期递增
MySQL 8.0对子查询处理进行了多项改进:
示例:利用窗口函数简化Top-N查询
sql复制-- 传统写法:使用变量或多次查询
SELECT department, name, salary
FROM (
SELECT department, name, salary,
RANK() OVER (PARTITION BY department ORDER BY salary DESC) as rnk
FROM employees
) ranked
WHERE rnk <= 3