1. 解题方法论沉淀
最近集中刷完了力扣SQL 50题系列的第三部分,这个阶段的题目明显开始涉及更复杂的多表操作和嵌套查询。作为每天和数据库打交道的后端开发,我发现在实际业务中遇到的SQL场景和这些高频题目高度重合。比如用户行为分析需要处理的时间窗口函数,电商系统中的层级商品分类统计,还有社交网络里的好友关系链查询——这些都能在题库里找到原型题。
先说说我的刷题装备:本地用Docker跑了MySQL 8.0和PostgreSQL 14两个环境,Navicat作为GUI工具,遇到语法差异时会特别标注。建议初学者至少要准备MySQL环境,因为力扣的判题系统就是基于MySQL的。
2. 典型题目精讲
2.1 多层嵌套查询优化
第47题"各部门工资最高的员工"就是个经典案例。初始解法很容易想到用子查询:
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 )但实际业务中数据量大时,这种写法性能会急剧下降。我的优化方案是先用CTE(Common Table Expression)预计算部门最高薪:
WITH MaxSalary AS ( SELECT departmentId, MAX(salary) AS max_salary FROM Employee GROUP BY departmentId ) SELECT d.name AS Department, e.name AS Employee, e.salary FROM Employee e JOIN Department d ON e.departmentId = d.id JOIN MaxSalary m ON e.departmentId = m.departmentId AND e.salary = m.max_salary踩坑提醒:MySQL 8.0以下版本不支持CTE语法,这时候可以用临时表替代
2.2 时间窗口函数实战
第39题"连续三天登录的用户"展示了窗口函数的威力。我的解题路径是这样的:
- 先用LEAD()函数获取后两天的日期:
SELECT DISTINCT user_id FROM ( SELECT user_id, login_date, LEAD(login_date, 1) OVER (PARTITION BY user_id ORDER BY login_date) AS next_day, LEAD(login_date, 2) OVER (PARTITION BY user_id ORDER BY login_date) AS next_next_day FROM Logins ) t WHERE DATEDIFF(next_day, login_date) = 1 AND DATEDIFF(next_next_day, next_day) = 1- 后来发现更简洁的解法是用DATE_SUB做自连接:
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) = 1 JOIN Logins c ON b.user_id = c.user_id AND DATEDIFF(c.login_date, b.login_date) = 13. 性能优化技巧
3.1 索引使用要点
在解第43题"订单量超过10的客户"时,发现没有索引的查询在百万数据量下要8秒:
-- 原始慢查询 SELECT customer_id FROM Orders GROUP BY customer_id HAVING COUNT(*) > 10添加复合索引后降到0.2秒:
ALTER TABLE Orders ADD INDEX idx_customer (customer_id, id);经验之谈:GROUP BY字段应该放在复合索引的第一位,COUNT()计数字段放后面
3.2 执行计划分析
用EXPLAIN分析第50题"员工奖金"的查询计划时,发现全表扫描非常耗资源:
EXPLAIN SELECT e.name, b.bonus FROM Employee e LEFT JOIN Bonus b ON e.empId = b.empId WHERE b.bonus < 1000 OR b.bonus IS NULL优化方案是先把Bonus表过滤后再关联:
SELECT e.name, b.bonus FROM Employee e LEFT JOIN ( SELECT empId, bonus FROM Bonus WHERE bonus < 1000 ) b ON e.empId = b.empId WHERE b.bonus IS NOT NULL OR b.empId IS NULL4. 复杂场景解决方案
4.1 递归查询处理树形结构
第45题"部门前三大高薪员工"需要处理分组排名问题。除了用DENSE_RANK()的常规解法,我还尝试了递归CTE方案:
WITH RankedSalaries AS ( 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 ) SELECT Department, Employee, salary FROM RankedSalaries WHERE rnk <= 3 ORDER BY Department, salary DESC4.2 交叉表统计技巧
第48题"学生出勤统计"需要同时计算多个条件统计量:
SELECT student_id, COUNT(*) AS total_days, SUM(attendance_type = 'present') AS present_days, ROUND(SUM(attendance_type = 'present') / COUNT(*) * 100, 2) AS attendance_rate FROM Attendance GROUP BY student_id HAVING SUM(attendance_type = 'present') / COUNT(*) < 0.6这里用SUM()配合条件表达式替代了多个子查询,性能提升明显。
5. 避坑指南
NULL值陷阱:在多条件WHERE子句中,
WHERE bonus < 1000 OR bonus IS NULL和WHERE COALESCE(bonus,0) < 1000的结果集可能不同隐式类型转换:JOIN操作时如果字段类型不匹配(如VARCHAR和INT),会导致全表扫描
GROUP BY陷阱:MySQL的sql_mode如果不是ONLY_FULL_GROUP_BY,可能返回意外结果
索引失效场景:
- 使用函数操作索引字段:
WHERE YEAR(create_time) = 2023 - 前导通配符:
WHERE name LIKE '%张' - 隐式类型转换
- 使用函数操作索引字段:
分页优化:大数据量分页避免用
LIMIT 100000,10,改用WHERE id > last_id LIMIT 10
这套题刷下来最大的收获是建立了SQL性能优化的条件反射:看到GROUP BY先想索引,遇到子查询考虑CTE,多表关联必看执行计划。建议每刷完10题就做次系统性复盘,把同类问题的解法归类比较。