作为一名长期与MySQL打交道的数据库工程师,我处理过无数复杂查询场景,子查询绝对是SQL工具箱中最锋利的那把瑞士军刀。简单来说,子查询就是嵌套在另一个查询内部的查询语句,它就像俄罗斯套娃中最里面的那个小人儿,虽然小巧但往往掌握着关键信息。
在实际业务场景中,子查询最常见的用武之地包括:
举个真实案例:去年我们电商系统要做用户分层运营,需要找出消费金额高于平均值的VIP用户。用子查询只需一行代码就能搞定:
sql复制SELECT user_id, total_amount
FROM orders
WHERE total_amount > (SELECT AVG(total_amount) FROM orders);
在电商系统中,我们常有这样的需求:找出已下单但未支付的用户。这时子查询就能大显身手:
sql复制SELECT DISTINCT user_name
FROM users
WHERE user_id IN (
SELECT user_id
FROM orders
WHERE payment_status = 'unpaid'
);
注意:当子查询结果集较大时,IN操作符可能导致性能问题。我实测过,当子查询结果超过1万条时,改用JOIN效率能提升40%左右。
做报表统计时,我经常用FROM子查询构建临时统计视图。比如统计每个部门的平均工资:
sql复制SELECT d.dept_name, t.avg_salary
FROM departments d
JOIN (
SELECT dept_id, AVG(salary) as avg_salary
FROM employees
GROUP BY dept_id
) t ON d.dept_id = t.dept_id;
这里有个优化技巧:给子查询中的GROUP BY字段添加索引,能使执行速度提升3-5倍。
在用户画像系统中,我们经常需要实时计算衍生指标。比如显示每个用户的订单数:
sql复制SELECT
user_id,
user_name,
(SELECT COUNT(*) FROM orders WHERE orders.user_id = users.user_id) as order_count
FROM users;
但要注意:这种关联子查询会对外层查询的每一行执行一次,当用户量超过10万时,查询时间会呈指数级增长。我的经验是改用LEFT JOIN+GROUP BY更高效。
找出公司最资深的员工:
sql复制SELECT emp_name, hire_date
FROM employees
WHERE hire_date = (SELECT MIN(hire_date) FROM employees);
这个查询有个潜在问题:如果有多位同一天入职的元老,只会返回其中一位。解决方案是改用<=比较符。
查找所有经理级员工:
sql复制SELECT emp_id, emp_name
FROM employees
WHERE emp_id IN (
SELECT manager_id
FROM departments
WHERE manager_id IS NOT NULL
);
在MySQL 8.0+版本中,我推荐改用CTE(Common Table Expression)写法,可读性更好:
sql复制WITH manager_ids AS (
SELECT DISTINCT manager_id
FROM departments
WHERE manager_id IS NOT NULL
)
SELECT emp_id, emp_name
FROM employees
WHERE emp_id IN (SELECT * FROM manager_ids);
查找与CEO同部门同职级的员工:
sql复制SELECT emp_id, emp_name
FROM employees
WHERE (dept_id, job_level) = (
SELECT dept_id, job_level
FROM employees
WHERE emp_title = 'CEO'
);
这种写法在MySQL中效率很高,因为优化器会将其转换为等值连接。
统计各部门薪资前3名的员工:
sql复制SELECT dept_name, emp_name, salary
FROM (
SELECT
d.dept_name,
e.emp_name,
e.salary,
DENSE_RANK() OVER (PARTITION BY e.dept_id ORDER BY e.salary DESC) as rnk
FROM employees e
JOIN departments d ON e.dept_id = d.dept_id
) ranked_employees
WHERE rnk <= 3;
这里用到了窗口函数,是MySQL 8.0引入的强大特性。在旧版本中,需要用复杂的自连接实现相同功能。
记得有次线上查询超时,最终发现是嵌套了5层的子查询导致的。现在我会先用EXPLAIN分析执行计划,重点关注:
sql复制-- 优化前
SELECT * FROM A WHERE id IN (SELECT id FROM B);
-- 优化后
SELECT * FROM A WHERE EXISTS (SELECT 1 FROM B WHERE B.id = A.id);
sql复制-- 不好的写法
SELECT * FROM users WHERE id IN (SELECT * FROM orders...);
-- 好的写法
SELECT * FROM users WHERE id IN (SELECT user_id FROM orders...);
去年双十一大促时,我们有个促销活动查询突然超时。分析发现是这样一个查询:
sql复制SELECT product_id
FROM inventory
WHERE quantity > (
SELECT AVG(quantity)
FROM inventory
WHERE warehouse_id = 5
)
AND warehouse_id = 5;
优化方案:
MySQL支持通过子查询结果来更新数据:
sql复制UPDATE products p
JOIN (
SELECT product_id, COUNT(*) as order_count
FROM order_items
GROUP BY product_id
) t ON p.product_id = t.product_id
SET p.hot_score = p.hot_score + t.order_count;
MySQL 8.0开始支持递归CTE,可以处理层级数据:
sql复制WITH RECURSIVE org_tree AS (
-- 基础查询:找出所有CEO
SELECT emp_id, emp_name, 1 as level
FROM employees
WHERE manager_id IS NULL
UNION ALL
-- 递归查询:找出下属
SELECT e.emp_id, e.emp_name, ot.level + 1
FROM employees e
JOIN org_tree ot ON e.manager_id = ot.emp_id
)
SELECT * FROM org_tree;
MySQL 8.0.14+支持LATERAL关键字,允许子查询引用前面表的列:
sql复制SELECT d.dept_id, d.dept_name, top_emp.*
FROM departments d,
LATERAL (
SELECT emp_name, salary
FROM employees e
WHERE e.dept_id = d.dept_id
ORDER BY salary DESC
LIMIT 3
) top_emp;
经过多年实战,我总结出这些子查询黄金法则:
最后分享一个冷知识:在MySQL中,子查询的性能在5.6版本后有了质的飞跃,这是因为优化器引入了半连接(semi-join)转换技术。所以如果你的MySQL版本较老,升级可能是最好的优化方案。