子查询:小心 SQL 中的「定时炸弹」
很多人喜欢用子查询,因为「看起来清晰」。
但有时候,子查询是性能杀手——你写得爽,数据库跑得慢。
今天,我们彻底搞懂子查询。
什么是子查询?
子查询就是 SELECT 中的 SELECT,一个查询嵌套在另一个查询里。
sql
-- 子查询示例:查所有有订单的用户
SELECT * FROM users
WHERE id IN (SELECT user_id FROM orders);
-- 子查询结果:
-- (SELECT user_id FROM orders) 返回 [1, 2, 3, ...]
-- 外层查询找到 id 在这些值中的用户子查询的类型
标量子查询:返回单个值
sql
-- 查询订单数大于平均值的用户
SELECT * FROM users
WHERE (SELECT COUNT(*) FROM orders WHERE orders.user_id = users.id)
> (SELECT AVG(cnt) FROM (SELECT COUNT(*) AS cnt FROM orders GROUP BY user_id) AS t);列子查询:返回一列值
sql
-- 查询所有有订单的用户
SELECT * FROM users
WHERE id IN (SELECT user_id FROM orders);行子查询:返回一行
sql
-- 查询订单金额最大的订单
SELECT * FROM orders
WHERE (amount, created_at) = (SELECT MAX(amount), MAX(created_at) FROM orders);表子查询:返回一个表
sql
-- 查询每个用户最新的订单
SELECT * FROM orders
WHERE (user_id, created_at) IN
(SELECT user_id, MAX(created_at) FROM orders GROUP BY user_id);子查询的坑
坑一:相关子查询的性能问题
相关子查询:子查询引用了外层查询的字段。
sql
-- 相关子查询:性能差
SELECT * FROM orders o
WHERE amount > (SELECT AVG(amount) FROM orders WHERE user_id = o.user_id);
-- 每行都执行一次子查询!优化:用 JOIN 替代相关子查询。
sql
-- 优化:用 GROUP BY + JOIN
SELECT o.*
FROM orders o
INNER JOIN (
SELECT user_id, AVG(amount) AS avg_amount
FROM orders
GROUP BY user_id
) t ON o.user_id = t.user_id
WHERE o.amount > t.avg_amount;坑二:IN 子查询的性能问题
MySQL 5.6 之前,IN 子查询会先执行子查询,再执行外层查询,效率很低。
sql
-- IN 子查询
SELECT * FROM orders
WHERE user_id IN (SELECT id FROM users WHERE status = 'active');
-- MySQL 5.6+ 会自动优化为 JOIN
-- 但复杂场景可能还是慢优化:用 EXISTS 或 JOIN 替代。
sql
-- EXISTS 写法
SELECT * FROM orders o
WHERE EXISTS (SELECT 1 FROM users u WHERE u.id = o.user_id AND u.status = 'active');
-- JOIN 写法(通常更好)
SELECT DISTINCT o.*
FROM orders o
INNER JOIN users u ON o.user_id = u.id
WHERE u.status = 'active';坑三:子查询返回 NULL 的问题
sql
-- 如果子查询返回 NULL,IN 的行为会出错
SELECT * FROM users
WHERE id IN (SELECT user_id FROM orders WHERE status = 'cancelled');
-- 如果所有订单都取消了,user_id 列都是 NULL
-- IN (NULL) 永远不会匹配任何行!优化:处理 NULL 值。
sql
-- 使用 EXISTS 或 NOT EXISTS
SELECT * FROM users u
WHERE EXISTS (SELECT 1 FROM orders o WHERE o.user_id = u.id AND o.status = 'cancelled');MySQL 优化器对子查询的优化
优化一:子查询物化
MySQL 5.6+ 会将子查询结果物化为临时表,并建立索引。
sql
SELECT * FROM orders
WHERE user_id IN (SELECT id FROM users WHERE status = 'active');
-- MySQL 内部执行:
-- 1. 执行子查询,结果存入临时表(带索引)
-- 2. 主查询使用临时表进行匹配优化二:半连接(Semi-Join)
MySQL 对 IN 子查询会尝试使用半连接优化。
sql
-- 半连接优化
SELECT * FROM orders o
WHERE o.user_id IN (SELECT id FROM users WHERE status = 'active');
-- 优化器可能选择:
-- - Table pullout:将子查询表提出来做 JOIN
-- - FirstMatch:只匹配第一个满足条件的行
-- - LooseScan:扫描索引时跳过重复值优化三:物化扫描
sql
-- 物化扫描
SELECT * FROM orders o
WHERE EXISTS (SELECT 1 FROM users u WHERE u.id = o.user_id AND u.status = 'active');
-- 子查询物化为临时表,外层使用最佳实践
实践一:能用 JOIN 就不用子查询
sql
-- 不推荐:子查询
SELECT * FROM orders
WHERE user_id IN (SELECT id FROM users WHERE status = 'active');
-- 推荐:JOIN
SELECT DISTINCT o.*
FROM orders o
INNER JOIN users u ON o.user_id = u.id
WHERE u.status = 'active';实践二:用 EXISTS 替代 IN
sql
-- 不推荐:IN 子查询
SELECT * FROM orders
WHERE user_id IN (SELECT id FROM users WHERE status = 'active');
-- 推荐:EXISTS
SELECT * FROM orders o
WHERE EXISTS (SELECT 1 FROM users u WHERE u.id = o.user_id AND u.status = 'active');实践三:避免相关子查询
sql
-- 不推荐:相关子查询
SELECT *, (SELECT COUNT(*) FROM orders WHERE user_id = users.id) AS order_count
FROM users;
-- 推荐:GROUP BY + JOIN
SELECT u.*, COALESCE(t.order_count, 0) AS order_count
FROM users u
LEFT JOIN (
SELECT user_id, COUNT(*) AS order_count
FROM orders
GROUP BY user_id
) t ON u.id = t.user_id;Java 代码示例
java
@Service
public class SubqueryOptimizer {
/**
* 将子查询转换为 JOIN
*/
public String optimizeSubquery(String sql) {
// 简单示例:识别 IN 子查询并转换为 JOIN
if (sql.contains("IN (SELECT") && sql.contains("DISTINCT")) {
// 已经是 DISTINCT,不需要改
return sql;
}
// 复杂场景需要 SQL 解析器
// 这里只是演示思路
if (sql.contains("IN (SELECT")) {
// 可以考虑转换为 JOIN
return "/* 建议转换为 JOIN 以提升性能 */" + sql;
}
return sql;
}
/**
* 检查是否有相关子查询
*/
public boolean hasCorrelatedSubquery(String sql) {
// 简单检测:子查询中引用外层表
// 需要 SQL 解析器或 AST
// 这里只是演示思路
return sql.matches(".*\\(SELECT.+\\b" + extractTableAlias(sql) + "\\..*");
}
}面试追问方向
- IN 和 EXISTS 的区别?哪个性能更好?
- 子查询和 JOIN 哪个性能更好?
- MySQL 对子查询做了哪些优化?
EXISTS 和 IN 在大多数情况下性能差不多,但 EXISTS 更灵活(可以处理复杂的子查询条件)。JOIN 在数据量大且有索引时通常更好。
