子查询
子查询涉及 IN, EXISTS, ANY/ALL 以及相关子查询的逻辑嵌套,是解决复杂筛选的核心。
1978. 上级经理已离职的公司员工 (Employees Whose Manager Left the Company)
题目描述
Employees 表包含公司员工的信息:
employee_id: 员工 ID,主键。name: 姓名。manager_id: 上级经理的 ID。salary: 薪水。
请编写一个解决方案,找出所有 薪水严格小于 30000 且 上级经理已离职 的员工。当一个员工有一个关联的 manager_id,但该 ID 已不再出现在 Employees 表的 employee_id 列中时,即认为该经理已经离职。
结果请按 employee_id 排序。
解题思路
多维子查询筛选。
- 基础过滤:
salary < 30000且manager_id不为 NULL(因为顶级经理没有上级也算离职是不合理的)。 - 离职判断:使用
manager_id NOT IN (SELECT employee_id FROM Employees)来锁定那些指向了不存在的 ID 的记录。
SQL 实现 (MySQL)
SELECT employee_id
FROM Employees
WHERE salary < 30000
AND manager_id IS NOT NULL
AND manager_id NOT IN (SELECT employee_id FROM Employees)
ORDER BY employee_id;
626. 换座位 (Exchange Seats)
题目描述
Seat 表记录了每名学生及其分配的座位 ID:
id: 座位 ID,连续的整数。student: 学生姓名。
请编写一个解决方案,交换相邻两名学生的座位。如果学生人数是奇数,则最后一名学生的座位不进行交换。
解题思路
分段逻辑变换。
- 坐标偏移:通常情况下,奇数位的学生移动到
id+1,偶数位的学生移动到id-1。 - 尾项处理:如果全表人数是奇数,则最后一项(其
id等于全表行数)应保持原样,不参与+1操作。 - 计算总行数:使用子查询
(SELECT COUNT(*) FROM Seat)动态获取总数。
SQL 实现 (MySQL)
SELECT (
CASE
WHEN id % 2 = 1 AND id = (SELECT COUNT(*) FROM Seat) THEN id
WHEN id % 2 = 1 THEN id + 1
ELSE id - 1
END
) AS id, student
FROM Seat
ORDER BY id;
1341. 电影评分 (Movie Rating)
题目描述
你有三张表:
Movies表:包含电影信息。movie_id: 电影 ID,主键。title: 电影名称。
Users表:包含用户信息。user_id: 用户 ID,主键。name: 用户名。
MovieRating表:记录了用户对电影的评分。movie_id: 电影 ID。user_id: 用户 ID。rating: 评分。created_at: 评分日期。
请编写一个解决方案,输出两部分结果:
- 最积极用户:找出评论电影次数最多的用户名。如果有多个用户并列,则返回字典序较小的那一个。
- 最佳电影:找出在 2020 年 2 月 平均评分最高的电影名称。如果有多个电影并列,则返回字典序较小的那一个。
解题思路
UNION ALL 结果拼接。
- 各司其职:分别为两个子需求编写完整的
SELECT语句,每个语句内部使用GROUP BY,ORDER BY(双重排序)并配合LIMIT 1。 - 合而为一:使用
UNION ALL将两个只有一行的结果合并在一起。
SQL 实现 (MySQL)
(SELECT u.name AS results
FROM MovieRating mr JOIN Users u USING(user_id)
GROUP BY user_id
ORDER BY COUNT(*) DESC, name ASC LIMIT 1)
UNION ALL
(SELECT m.title AS results
FROM MovieRating mr JOIN Movies m USING(movie_id)
WHERE created_at LIKE '2020-02-%'
GROUP BY movie_id
ORDER BY AVG(rating) DESC, title ASC LIMIT 1);
1321. 餐馆营业额变化增长 (Restaurant Growth)
题目描述
Customer 表记录了顾客在不同日期的消费额:
customer_id: 顾客 ID。visited_on: 访问日期。amount: 消费金额。
你需要计算:从每一个可能的日期开始,过去 7 天内(包含当天)的累计消费额和平均消费额。
- 金额总和 (amount):7 天的总额。
- 平均额 (average_amount):7 天的总额除以 7,保留两位小数。
注意:结果应从第 7 天(即能够凑齐过去 7 天数据的日期)开始展示。结果按日期升序排列。
解题思路
窗口函数滚动聚合。
- 预处理:由于同一天可能有多笔交易,先按天合并总额
SUM(amount) GROUP BY visited_on。 - 滑动窗口:使用
SUM(...) OVER(ORDER BY visited_on ROWS 6 PRECEDING)计算当前日期及前 6 天的总和。 - 起始过滤:由于窗口函数会自动处理不足 7 天的情况,我们需要根据日期排名或 ID 过滤掉前 6 条记录。
SQL 实现 (MySQL)
SELECT visited_on, amount, ROUND(amount / 7, 2) AS average_amount
FROM (
SELECT visited_on, SUM(SUM(amount)) OVER(ORDER BY visited_on ROWS 6 PRECEDING) AS amount,
DENSE_RANK() OVER(ORDER BY visited_on) AS rnk
FROM Customer
GROUP BY visited_on
) t
WHERE rnk >= 7;
602. 好友申请 II :谁有最多的好友 (Friend Requests II: Who Has the Most Friends)
题目描述
RequestAccepted 表保存了通过的好友申请记录:
requester_id: 发送者 ID。accepter_id: 接收者 ID。accept_date: 通过日期。
请编写一个解决方案,找出 好友数量最多 的人 ID 及其对应的好友总数。题目保证仅有一人拥有最多的好友。
解题思路
双列合并与分组计数。
- 视角转换:一个人无论是发送者还是接收者,只要产生了一条通过记录,就增加了一名好友。
- 堆叠数据:使用
UNION ALL将两列 ID 合并成一个单列集合。 - 聚合:按 ID 分组统计出现次数,取最大值即可。
SQL 实现 (MySQL)
SELECT id, COUNT(*) AS num FROM (
SELECT requester_id AS id FROM RequestAccepted
UNION ALL
SELECT accepter_id AS id FROM RequestAccepted
) t
GROUP BY id
ORDER BY num DESC
LIMIT 1;
585. 2016年的投资 (Investments in 2016)
题目描述
Insurance 表保存了保险单的信息:
PID: 投保人 ID。TIV_2015: 2015 年的总投资。TIV_2016: 2016 年的总投资。LAT,LON: 经纬度位置信息。
请查询所有满足以下 两个条件 的投保人的 2016 年总投资 (TIV_2016) 之和(保留两位小数):
- 至少有一个其他投保人的
TIV_2015与其相同。 - 该投保人的地理位置
(LAT, LON)必须是唯一的(即没有任何其他投保人位于同一地点)。
解题思路
复合子查询过滤。
- 条件一:在主表记录中筛选出那些
TIV_2015在全表中重复出现次数大于 1 的行。 - 条件二:同时筛选出那些
(LAT, LON)组合在全表中只出现过 1 次的行。 - 加和:对满足双重条件的行进行求和。可以使用窗口函数或嵌套子查询来实现。
SQL 实现 (MySQL)
SELECT ROUND(SUM(tiv_2016), 2) AS tiv_2016
FROM Insurance
WHERE tiv_2015 IN (
SELECT tiv_2015 FROM Insurance GROUP BY tiv_2015 HAVING COUNT(*) > 1
)
AND (lat, lon) IN (
SELECT lat, lon FROM Insurance GROUP BY lat, lon HAVING COUNT(*) = 1
);
185. 部门工资前三高的所有员工 (Department Top Three Salaries)
题目描述
你有两张表:Employee (包含 id, name, salary, departmentId) 和 Department (包含 id, name)。
公司规定:“高额薪水” 是指一个部门内排名在前 三名 的薪水。注意,并列排名的薪水也应该被考虑进去(例如,如果有两个人的薪水并列第一,接下来的高薪水排名应该是第三?不,按照 LeetCode 规范通常是指前三档不同的薪水,即 DENSE_RANK 逻辑)。
请查询每个部门中哪些员工属于高额薪水阶层(即该部门内排名前三档的薪水获得者)。
解题思路
窗口函数分区排名。
- 分区排名:使用
DENSE_RANK() OVER(PARTITION BY departmentId ORDER BY salary DESC)为每个部门的员工按工资降序打分。 - 什么是 DENSE_RANK:如果两个员工薪水都是 1000 并列第一,那么下一个薪水 900 的员工排名为 2。这正好对应“前三高”的实际意义。
- 过滤结果:在外层查询中保留排名小于等于 3 的员工,并连接部门表获取其名称。
SQL 实现 (MySQL)
SELECT d.name AS Department, e.name AS Employee, salary AS Salary
FROM (
SELECT name, salary, departmentId, DENSE_RANK() OVER(PARTITION BY departmentId ORDER BY salary DESC) AS rnk
FROM Employee
) e JOIN Department d ON e.departmentId = d.id
WHERE rnk <= 3;
(注:DENSE_RANK 处理并列情况:例如若有两人工资第一,接下来第三个人的名次是第二)。 (注:LeetCode 中的困难题目逻辑通常与此题类似)。