子查询
子查询是嵌套在另一个查询中的查询。当无法直接从数据表中获取结果时,可以通过子查询从查询结果集中再次查询,得到最终想要的结果。
准备数据
player 表(球员表)
sql
CREATE TABLE player (
player_id INT(11) NOT NULL AUTO_INCREMENT COMMENT '球员ID',
team_id INT(11) NOT NULL COMMENT '球队ID',
player_name VARCHAR(255) NOT NULL COMMENT '球员姓名',
height FLOAT(3, 2) COMMENT '球员身高',
PRIMARY KEY (player_id) USING BTREE,
UNIQUE INDEX uk_player_name (player_name) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 示例数据
INSERT INTO player VALUES
(10001, 1001, '韦恩-艾灵顿', 1.93),
(10002, 1001, '雷吉-杰克逊', 1.91),
(10003, 1001, '安德烈-德拉蒙德', 2.11),
(10004, 1001, '索恩-马克', 2.16),
(10021, 1002, '维克多-奥拉迪波', 1.93),
(10022, 1002, '博扬-博格达诺维奇', 2.03),
(10023, 1002, '多曼塔斯-萨博尼斯', 2.11);team 表(球队表)
sql
CREATE TABLE team (
team_id INT(11) NOT NULL COMMENT '球队ID',
team_name VARCHAR(255) NOT NULL COMMENT '球队名称',
PRIMARY KEY (team_id) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 示例数据
INSERT INTO team VALUES
(1001, '底特律活塞'),
(1002, '印第安纳步行者'),
(1003, '亚特兰大老鹰');player_score 表(球员比赛成绩表)
sql
CREATE TABLE player_score (
game_id INT(11) NOT NULL COMMENT '比赛ID',
player_id INT(11) NOT NULL COMMENT '球员ID',
is_first TINYINT(1) NOT NULL COMMENT '是否首发',
playing_time INT(11) NOT NULL COMMENT '出场时间',
score INT(11) NOT NULL COMMENT '得分',
rebound INT(11) NOT NULL COMMENT '篮板',
assist INT(11) NOT NULL COMMENT '助攻'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;height_grades 表(身高等级表)
sql
CREATE TABLE height_grades (
height_level VARCHAR(255) NOT NULL COMMENT '身高等级',
height_lowest FLOAT(3, 2) NOT NULL COMMENT '最低身高',
height_highest FLOAT(3, 2) NOT NULL COMMENT '最高身高'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
INSERT INTO height_grades VALUES
('A', 2.00, 2.50),
('B', 1.90, 1.99),
('C', 1.80, 1.89),
('D', 1.60, 1.79);子查询分类
非关联子查询
子查询只执行一次,结果作为主查询的条件:
sql
-- 查询身高最高的球员
SELECT player_name, height
FROM player
WHERE height = (SELECT MAX(height) FROM player);执行过程:
- 子查询
SELECT MAX(height) FROM player先执行,返回 2.16 - 主查询变为
SELECT player_name, height FROM player WHERE height = 2.16
关联子查询
子查询需要执行多次,每次依赖外部查询的值:
sql
-- 查询每个球队中身高大于该球队平均身高的球员
SELECT player_name, height, team_id
FROM player AS a
WHERE height > (
SELECT AVG(height)
FROM player AS b
WHERE a.team_id = b.team_id
);执行过程:
- 外部查询取出第一条记录,team_id = 1001
- 子查询计算 team_id = 1001 的平均身高
- 判断该球员身高是否大于平均值
- 重复以上步骤处理每一条记录
对比总结
| 类型 | 执行次数 | 是否依赖外部查询 | 性能 |
|---|---|---|---|
| 非关联子查询 | 1 次 | 否 | 较好 |
| 关联子查询 | N 次 | 是 | 较差 |
EXISTS 子查询
EXISTS 用于判断子查询是否返回结果,返回 True 或 False。
EXISTS 示例
sql
-- 查询出场过的球员
SELECT player_id, team_id, player_name
FROM player
WHERE EXISTS (
SELECT player_id
FROM player_score
WHERE player.player_id = player_score.player_id
);NOT EXISTS 示例
sql
-- 查询未出场过的球员
SELECT player_id, team_id, player_name
FROM player
WHERE NOT EXISTS (
SELECT player_id
FROM player_score
WHERE player.player_id = player_score.player_id
);EXISTS 工作原理
- 外部查询的每一行都会执行一次子查询
- 子查询返回任何行,EXISTS 就返回 True
- 子查询不返回任何行,EXISTS 就返回 False
集合比较子查询
IN 子查询
判断值是否在子查询结果集中:
sql
-- 查询出场过的球员(使用 IN)
SELECT player_id, team_id, player_name
FROM player
WHERE player_id IN (
SELECT player_id
FROM player_score
);ANY 子查询
与子查询返回的任意一个值比较:
sql
-- 查询比步行者任意球员身高都高的球员
SELECT player_id, player_name, height
FROM player
WHERE height > ANY (
SELECT height
FROM player
WHERE team_id = 1002
);
-- 等价于:height > (SELECT MIN(height) FROM player WHERE team_id = 1002)ALL 子查询
与子查询返回的所有值比较:
sql
-- 查询比步行者所有球员身高都高的球员
SELECT player_id, player_name, height
FROM player
WHERE height > ALL (
SELECT height
FROM player
WHERE team_id = 1002
);
-- 等价于:height > (SELECT MAX(height) FROM player WHERE team_id = 1002)SOME 子查询
SOME 是 ANY 的别名,作用相同:
sql
SELECT player_id, player_name, height
FROM player
WHERE height > SOME (
SELECT height FROM player WHERE team_id = 1002
);集合比较总结
| 操作符 | 含义 | 等价形式 |
|---|---|---|
| IN | 在集合中 | = ANY |
| NOT IN | 不在集合中 | <> ALL |
| > ANY | 大于任意一个 | > (MIN(...)) |
| > ALL | 大于所有 | > (MAX(...)) |
| < ANY | 小于任意一个 | < (MAX(...)) |
| < ALL | 小于所有 | < (MIN(...)) |
子查询作为计算字段
子查询可以作为 SELECT 语句中的计算字段:
sql
-- 查询每个球队的球员数量
SELECT
team_name,
(SELECT COUNT(*) FROM player WHERE player.team_id = team.team_id) AS player_num
FROM team;结果:
| team_name | player_num |
|---|---|
| 底特律活塞 | 4 |
| 印第安纳步行者 | 3 |
| 亚特兰大老鹰 | 0 |
IN vs EXISTS 选择
选择原则
sql
-- IN 子查询
SELECT * FROM A WHERE cc IN (SELECT cc FROM B);
-- EXISTS 子查询
SELECT * FROM A WHERE EXISTS (SELECT cc FROM B WHERE B.cc = A.cc);| 场景 | 推荐 | 原因 |
|---|---|---|
| 表 A 大,表 B 小 | IN | B 表索引有效 |
| 表 A 小,表 B 大 | EXISTS | A 表索引有效 |
| 子查询结果集小 | IN | 直接匹配 |
| 子查询结果集大 | EXISTS | 逐行判断 |
性能分析
sql
-- 场景:player 表大,player_score 表小
-- 推荐 IN
SELECT player_name FROM player
WHERE player_id IN (SELECT player_id FROM player_score);
-- 场景:player 表小,player_score 表大
-- 推荐 EXISTS
SELECT player_name FROM player p
WHERE EXISTS (
SELECT 1 FROM player_score ps
WHERE ps.player_id = p.player_id
);子查询位置
WHERE 子句
sql
SELECT * FROM player
WHERE height > (SELECT AVG(height) FROM player);HAVING 子句
sql
SELECT team_id, AVG(height) AS avg_height
FROM player
GROUP BY team_id
HAVING AVG(height) > (
SELECT AVG(height) FROM player
);FROM 子句(派生表)
sql
-- 子查询作为临时表
SELECT t.team_name, p.avg_height
FROM team t
JOIN (
SELECT team_id, AVG(height) AS avg_height
FROM player
GROUP BY team_id
) p ON t.team_id = p.team_id;SELECT 子句
sql
SELECT
player_name,
height,
(SELECT AVG(height) FROM player) AS overall_avg,
height - (SELECT AVG(height) FROM player) AS diff
FROM player;子查询优化建议
1. 使用连接替代子查询
sql
-- 子查询方式
SELECT player_name
FROM player
WHERE player_id IN (SELECT player_id FROM player_score);
-- 连接方式(通常更快)
SELECT DISTINCT p.player_name
FROM player p
INNER JOIN player_score ps ON p.player_id = ps.player_id;2. 避免相关子查询
sql
-- 差:相关子查询
SELECT player_name, height,
(SELECT team_name FROM team WHERE team.team_id = player.team_id) AS team_name
FROM player;
-- 好:使用连接
SELECT p.player_name, p.height, t.team_name
FROM player p
LEFT JOIN team t ON p.team_id = t.team_id;3. 使用 EXISTS 替代 IN
sql
-- 当子查询结果集很大时
-- 差
SELECT * FROM player
WHERE player_id IN (SELECT player_id FROM player_score WHERE score > 20);
-- 好
SELECT * FROM player p
WHERE EXISTS (
SELECT 1 FROM player_score ps
WHERE ps.player_id = p.player_id AND ps.score > 20
);4. 限制子查询返回的列
sql
-- 好:只返回需要的列
SELECT player_name FROM player
WHERE player_id IN (SELECT player_id FROM player_score);
-- 差:返回所有列
SELECT player_name FROM player
WHERE player_id IN (SELECT * FROM player_score);实战案例
案例 1:查找各部门薪资最高的员工
sql
SELECT e.name, e.salary, e.department_id
FROM employee e
WHERE salary = (
SELECT MAX(salary)
FROM employee e2
WHERE e2.department_id = e.department_id
);案例 2:查找有重复记录的数据
sql
SELECT * FROM player
WHERE player_name IN (
SELECT player_name
FROM player
GROUP BY player_name
HAVING COUNT(*) > 1
);案例 3:分页统计
sql
-- 统计每个球队的球员数,并分页
SELECT * FROM (
SELECT
t.team_name,
(SELECT COUNT(*) FROM player p WHERE p.team_id = t.team_id) AS player_count
FROM team t
) AS team_stats
ORDER BY player_count DESC
LIMIT 5 OFFSET 0;总结
子查询类型
| 类型 | 说明 | 使用场景 |
|---|---|---|
| 非关联子查询 | 独立执行一次 | 单值比较 |
| 关联子查询 | 依赖外部查询 | 逐行判断 |
| EXISTS | 判断是否存在 | 存在性检查 |
| IN | 集合成员判断 | 离散值匹配 |
| ANY/ALL | 集合比较 | 范围判断 |
最佳实践
- 优先使用连接:连接通常比子查询性能更好
- 合理选择 IN/EXISTS:根据表大小选择
- 避免多层嵌套:复杂查询考虑使用 CTE 或临时表
- 注意 NULL 值:NOT IN 遇到 NULL 会返回空结果
- 使用索引:确保连接字段有索引