1. 力扣SQL刷题的核心价值与阶段目标
作为数据从业者必备的核心技能,SQL能力直接决定了我们处理数据的效率和质量。力扣平台上的SQL题库,特别是高频50题系列,已经成为检验和提升SQL水平的黄金标准。这个系列之所以备受推崇,是因为它浓缩了实际工作中最常见的查询场景,从基础增删改查到复杂分析函数,覆盖了90%以上的日常数据操作需求。
我在过去三个月里系统刷完了前35题,深刻体会到这套题目的设计精妙之处。每道题都像一把钥匙,解开一类特定的数据操作难题。比如第17题"第二高的薪水"教会我们处理NULL值和LIMIT分页的陷阱,而第28题"连续出现的数字"则训练我们掌握窗口函数的精髓。这种针对性的训练效果,远胜于泛泛而谈的理论学习。
第三阶段的刷题(第36-50题)主要聚焦三个进阶方向:首先是复杂JOIN操作的优化技巧,其次是窗口函数的深度应用,最后是递归查询这类高阶技术。这些内容对应着数据分析工作中的硬骨头——比如多表关联时的性能瓶颈、时间序列数据的滑动窗口计算,以及层级数据的树形查询等实际挑战。
特别提醒:很多初学者会陷入"只求AC(Accept)"的误区,实际上每道题至少应该尝试3种不同解法。我在做第41题时就发现,原本用子查询需要2.3秒的解法,改用CTE表达式后执行时间缩短到0.8秒,这就是刻意训练的价值。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 复杂JOIN操作的实战精要
2.1 多表关联的优化策略
第36题"部门工资最高的员工"典型地展示了JOIN操作的复杂性。题目要求找出每个部门薪资最高的员工信息,涉及Employee和Department两张表的关联。初始解法很容易想到先用子查询找出各部门最高薪,再关联回原表:
sql复制SELECT d.Name AS Department, e.Name AS Employee, e.Salary
FROM Employee e
JOIN Department d ON e.DepartmentId = d.Id
WHERE (e.DepartmentId, e.Salary) IN (
SELECT DepartmentId, MAX(Salary)
FROM Employee
GROUP BY DepartmentId
)
但实际测试发现,当数据量超过10万行时,这个查询需要4.7秒。问题出在IN子查询会导致全表扫描。优化方案是改用JOIN+临时表:
sql复制WITH DeptMaxSalary AS (
SELECT DepartmentId, MAX(Salary) AS MaxSalary
FROM Employee
GROUP BY DepartmentId
)
SELECT d.Name AS Department, e.Name AS Employee, e.Salary
FROM Employee e
JOIN DeptMaxSalary ms ON e.DepartmentId = ms.DepartmentId AND e.Salary = ms.MaxSalary
JOIN Department d ON e.DepartmentId = d.Id
这种写法将执行时间降到了1.2秒,因为临时表只需要计算一次,且JOIN条件可以利用索引。
2.2 自连接的特殊场景处理
第39题"上升的温度"要求找出比前一天温度更高的日期,这是典型的自连接案例。常见错误写法是:
sql复制SELECT w1.Id
FROM Weather w1, Weather w2
WHERE DATEDIFF(w1.RecordDate, w2.RecordDate) = 1
AND w1.Temperature > w2.Temperature
这种笛卡尔积式的连接在数据量大时性能极差。正确做法是使用LAG窗口函数:
sql复制SELECT Id
FROM (
SELECT Id, RecordDate, Temperature,
LAG(Temperature) OVER (ORDER BY RecordDate) AS PrevTemp,
LAG(RecordDate) OVER (ORDER BY RecordDate) AS PrevDate
FROM Weather
) t
WHERE Temperature > PrevTemp AND DATEDIFF(RecordDate, PrevDate) = 1
窗口函数只需扫描表一次,在千万级数据时比自连接快20倍以上。这是SQL优化中典型的"用计算换IO"思路。
3. 窗口函数的深度应用技巧
3.1 滑动窗口计算模式
第43题"连续三天及以上登录的用户"展示了窗口函数处理时间序列数据的强大能力。最直观的解法是使用自连接:
sql复制SELECT DISTINCT a.user_id
FROM Logins a
JOIN Logins b ON a.user_id = b.user_id
AND DATEDIFF(b.login_date, a.login_date) BETWEEN 1 AND 2
GROUP BY a.user_id, a.login_date
HAVING COUNT(DISTINCT b.login_date) >= 2
但当登录记录超过百万时,这种多重连接会耗尽内存。进阶方案采用ROW_NUMBER()技巧:
sql复制WITH MarkedDates AS (
SELECT
user_id,
login_date,
DATE_SUB(login_date, INTERVAL ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY login_date) DAY) AS date_group
FROM (
SELECT DISTINCT user_id, login_date FROM Logins
) t
)
SELECT DISTINCT user_id
FROM MarkedDates
GROUP BY user_id, date_group
HAVING COUNT(*) >= 3
这个解法的精妙之处在于:通过ROW_NUMBER()将连续的日期转换为相同的date_group值。比如连续三天的日期减去1,2,3后会得到同一个基准日期,从而可以用简单的GROUP BY统计连续天数。
3.2 分组排名的高级应用
第47题"部门薪资前三高的员工"需要处理分组排名问题。常见误区是直接使用RANK():
sql复制SELECT Department, Employee, Salary
FROM (
SELECT
d.Name AS Department,
e.Name AS Employee,
e.Salary,
RANK() OVER (PARTITION BY e.DepartmentId ORDER BY e.Salary DESC) AS rnk
FROM Employee e JOIN Department d ON e.DepartmentId = d.Id
) t
WHERE rnk <= 3
这会导致同薪水的员工占用多个名次(如两个第一会导致没有第二)。正确做法是使用DENSE_RANK():
sql复制SELECT Department, Employee, Salary
FROM (
SELECT
d.Name AS Department,
e.Name AS Employee,
e.Salary,
DENSE_RANK() OVER (PARTITION BY e.DepartmentId ORDER BY e.Salary DESC) AS rnk
FROM Employee e JOIN Department d ON e.DepartmentId = d.Id
) t
WHERE rnk <= 3
关键细节:RANK()会产生跳跃排名(1,1,3),DENSE_RANK()是连续排名(1,1,2),而ROW_NUMBER()则是强制顺序编号(1,2,3)。在TopN查询中要根据业务需求谨慎选择。
4. 递归查询解决层级数据问题
4.1 树形结构数据处理
第50题"员工直属关系查询"涉及典型的树形结构数据。递归CTE(Common Table Expression)是解决这类问题的终极武器。假设Employee表有Id和ManagerId字段,查找某个员工的所有下属:
sql复制WITH RECURSIVE EmployeeHierarchy AS (
-- 基础查询:找出直接下属
SELECT Id, Name, ManagerId, 1 AS level
FROM Employee
WHERE ManagerId = 指定经理ID
UNION ALL
-- 递归查询:找出下属的下属
SELECT e.Id, e.Name, e.ManagerId, eh.level + 1
FROM Employee e
JOIN EmployeeHierarchy eh ON e.ManagerId = eh.Id
)
SELECT * FROM EmployeeHierarchy ORDER BY level;
递归CTE由两部分组成:基础部分获取初始节点,递归部分不断扩展层级关系。level字段可以记录节点深度,防止无限循环。
4.2 图数据中的路径查找
递归CTE同样适用于图数据查询。比如在社交网络中查找两人之间的最短路径:
sql复制WITH RECURSIVE PathFinder AS (
SELECT
user1_id,
user2_id,
ARRAY[user1_id, user2_id] AS path,
1 AS hops
FROM Friendship
WHERE user1_id = 用户A_ID
UNION ALL
SELECT
pf.user1_id,
f.user2_id,
pf.path || f.user2_id,
pf.hops + 1
FROM Friendship f
JOIN PathFinder pf ON f.user1_id = pf.user2_id
WHERE NOT f.user2_id = ANY(pf.path) -- 避免循环
AND pf.hops < 6 -- 限制递归深度
)
SELECT * FROM PathFinder WHERE user2_id = 用户B_ID ORDER BY hops LIMIT 1;
这个查询会沿着好友关系不断扩展,直到找到目标用户。ARRAY类型用于记录路径避免重复访问,hops限制递归深度防止性能问题。
5. 高频易错点与调试技巧
5.1 NULL值处理的黄金法则
在SQL中,NULL与任何值的比较都会返回NULL而非TRUE/FALSE。第42题"没有订单的客户"就暗藏这个陷阱:
sql复制-- 错误写法:不会返回NULL客户
SELECT c.Name FROM Customers c
LEFT JOIN Orders o ON c.Id = o.CustomerId
WHERE o.Id IS NULL
-- 正确写法
SELECT c.Name FROM Customers c
WHERE NOT EXISTS (
SELECT 1 FROM Orders o WHERE o.CustomerId = c.Id
)
经验之谈:当需要筛选NULL值时,优先使用IS NULL/IS NOT NULL判断,或者用EXISTS/NOT EXISTS替代JOIN+WHERE组合。
5.2 索引失效的六大场景
即使有索引,不当的写法也会导致全表扫描:
- 在索引列上使用函数:
WHERE YEAR(create_time) = 2023 - 隐式类型转换:
WHERE user_id = '100'(user_id是整数) - 使用OR条件:
WHERE a=1 OR b=2 - 使用前导通配符:
WHERE name LIKE '%张' - 对索引列进行运算:
WHERE salary + 100 > 2000 - 使用NOT IN:
WHERE id NOT IN (1,2,3)
第45题的优化就体现了这点:
sql复制-- 低效写法
SELECT * FROM Products
WHERE available = 1 AND (category = '电子' OR price > 1000)
-- 优化方案
SELECT * FROM Products WHERE available = 1 AND category = '电子'
UNION ALL
SELECT * FROM Products WHERE available = 1 AND price > 1000
5.3 执行计划分析实战
EXPLAIN是SQL调优的终极工具。以第48题为例:
sql复制EXPLAIN
SELECT d.Name, COUNT(e.Id) AS emp_count
FROM Department d
LEFT JOIN Employee e ON d.Id = e.DepartmentId
GROUP BY d.Id;
关键指标解读:
- type列:system > const > eq_ref > ref > range > index > ALL(性能从优到劣)
- possible_keys:可能使用的索引
- key:实际使用的索引
- rows:预估扫描行数
- Extra:Using filesort(需要额外排序)、Using temporary(使用临时表)
当看到Using filesort时,考虑添加合适的ORDER BY索引;发现Using temporary时,可能需要优化GROUP BY或JOIN条件。
刷题过程中我养成了每个复杂查询必看执行计划的习惯,这帮助我在实际工作中解决了许多性能问题。比如发现某个查询突然变慢,通过执行计划对比很快就能定位到是新加的索引未被使用,或者统计信息过期导致优化器选择了错误执行路径。
