刷SQLZOO这件事,我一年里带着团队干了三次。每次有新同学入职,我都会把SQLZOO中文版甩过去,让他们先把题过一遍。原因很简单:这套题能覆盖从SELECT基础到JOIN、聚合、子查询、窗口函数的完整链路,而且浏览器打开就能跑,不需要在本地装数据库。今天这篇就把各章节的答案和解题思路整理到一起,顺带把我在刷题和带新人过程中遇到的高频坑也写明白。无论你是刚学SQL三天,还是已经在业务里写过几百条取数SQL,只要还没系统刷过这套题,我都建议按下面的章节顺序来一遍。
1. 先读懂SQLZOO:这套题到底在训练什么能力
1.1 中文版怎么用,和直接搜答案的区别在哪
SQLZOO本身是英文网站,中文版把题目和提示翻译成了中文,但表结构、数据、判题逻辑和英文版完全一致。我推荐中文版的理由是它能让你把精力集中在SQL本身,而不是被“Show the name and population...”这种英文题目绊住。但要注意,翻译后的题目偶尔会有术语偏差,真遇到对不上的时候,切到英文原版对着看就行。
关于“直接搜答案”这件事,我的态度很明确:答案可以看,但不能只看答案。SQLZOO每道题都要求你在右侧输入框里写SQL并执行,系统直接比对返回结果。这种即时反馈意味着你不需要装SQL Server,也不需要配MySQL,浏览器开一个标签页就能连续练几个小时。我带新人时一般会定个规矩:每题必须自己执行通过才算过,不许把答案抄一遍就交差。抄答案十分钟后就会忘,自己敲一遍的东西才能真正留在手上。
1.2 全章节知识点地图:一张表看清考点分布
SQLZOO中文版目前主线章节大致如下,我按练习顺序整理了一张表,方便你对照自己的进度:
| 章节 | 核心表 | 主要考点 |
|---|---|---|
| SELECT basics | world | WHERE、IN、BETWEEN |
| SELECT from WORLD | world | LIKE、算术运算、ROUND、字符串函数 |
| SELECT from Nobel | nobel | 多条件、IN、LIKE、排序、转义 |
| SELECT within SELECT | world | 标量子查询、相关子查询、ALL/ANY |
| SUM and COUNT | world | GROUP BY、HAVING、聚合函数 |
| JOIN | game / goal / eteam | INNER JOIN、多表关联、CASE统计 |
| More JOIN | movie / actor / casting | 演员-电影多对多、ord角色 |
| Using Null | teacher / dept | LEFT/RIGHT JOIN、COALESCE、CASE |
| Self join | stops / route | 自连接、换乘查询 |
| Window functions | loan 等 | RANK、OVER、PARTITION BY |
看到这张表你应该能感受到,SQLZOO不是让你背语法,而是把SQL最常接触的几类查询场景串了一遍。前五章是地基,中间三章是JOIN的重头戏,后面两章是面试经常考的复杂查询。业务里的取数需求再怎么变,拆开看基本就是这些知识点的组合。
2. 基础章答案速查:SELECT basics / WORLD / Nobel
2.1 SELECT basics:三次课打牢查询的地基
题目1:查询国家Germany的人口。
SELECT population FROM world WHERE name = 'Germany';这条考的是最简单的SELECT列、FROM表、WHERE过滤。注意,题目只要求返回population,就不要顺手把name也带出来,SQLZOO判题是按结果集合匹配的,多出来的列会导致不一致。
题目2:查询瑞典、挪威、丹麦的人口。
SELECT name, population FROM world WHERE name IN ('Sweden', 'Norway', 'Denmark');考点是IN后面接一个字符串列表。等价写法是name = 'Sweden' OR name = 'Norway' OR name = 'Denmark'。实际开发中如果列表来自程序参数,用IN在可读性和维护性上都更好。
题目3:查询面积在20万到25万平方公里之间的国家名称和面积。
SELECT name, area FROM world WHERE area BETWEEN 200000 AND 250000;BETWEEN是闭区间,等价于area >= 200000 AND area <= 250000。有个小坑:某些数据库对BETWEEN的边界处理在索引优化上会有差异,但在SQLZOO这种在线环境里基本不用管。
2.2 SELECT from WORLD:算术、字符串与ROUND的考点
这个章节大概有十来道题,几乎把SQL基础函数过了一遍。
人口过2亿的国家:
SELECT name FROM world WHERE population >= 200000000;人口过2亿国家的人均GDP:
SELECT name, gdp / population AS per_capita_gdp FROM world WHERE population >= 200000000;这里直接用gdp / population做除法就行。SQLZOO的表里没有人口为0的国家,所以不需要处理除零问题。真要部署到生产环境,记得考虑除零保护。
南美洲各国的人口(单位:百万):
SELECT name, population / 1000000 AS population_in_millions FROM world WHERE continent = 'South America';名字包含“United”的国家:
SELECT name FROM world WHERE name LIKE '%United%';%在LIKE里是任意长度通配符,_才是单个字符。很多人一开始分不清,写LIKE 'United%'会漏掉“United Arab Emirates”这种,而%United%是包含关系,更符合题意。
面积或人口至少一项超过阈值的国家:
SELECT name, population, area FROM world WHERE area > 3000000 OR population > 250000000;面积和人口只有一个超过阈值(XOR逻辑):
SELECT name, population, area FROM world WHERE (area > 3000000 AND population <= 250000000) OR (area <= 3000000 AND population > 250000000);SQL里没有XOR关键字,所以要把“仅A成立”和“仅B成立”两个分支显式写出来。这道题很容易写成OR,那就和上一题没区别了,判题必挂。
南美洲人口(百万)和GDP(十亿),保留2位小数:
SELECT name, ROUND(population / 1000000, 2) AS pop_millions, ROUND(gdp / 1000000000, 2) AS gdp_billions FROM world WHERE continent = 'South America';ROUND的第二个参数是保留的小数位。如果传负数,意思是往小数点左边四舍五入,下一题就会用到。
万亿GDP国家的人均GDP,四舍五入到千位:
SELECT name, ROUND(gdp / population, -3) AS per_capita_gdp FROM world WHERE gdp >= 1000000000000;国家名和首都名字长度一样:
SELECT name, capital FROM world WHERE LENGTH(name) = LENGTH(capital);国家名和首都首字母相同,但国家名不等于首都名:
SELECT name, capital FROM world WHERE LEFT(name, 1) = LEFT(capital, 1) AND name <> capital;名字包含全部五个元音字母且没有空格:
SELECT name FROM world WHERE name LIKE '%a%' AND name LIKE '%e%' AND name LIKE '%i%' AND name LIKE '%o%' AND name LIKE '%u%' AND name NOT LIKE '% %';这道题容易漏掉NOT LIKE '% %',导致混入带空格的国家名。SQLZOO判题很严格,少了这个条件就一定过不了。
2.3 SELECT from Nobel:排序、转义和表达式条件
Nobel这张表存的是诺贝尔奖数据,字段包括yr(年份)、subject(领域)、winner(获奖者)。这章的题开始有“陷阱”了。
1950年诺贝尔奖得主:
SELECT winner FROM nobel WHERE yr = 1950;1962年的文学奖得主:
SELECT winner FROM nobel WHERE yr = 1962 AND subject = 'Literature';爱因斯坦获奖的年份和领域:
SELECT yr, subject FROM nobel WHERE winner = 'Albert Einstein';2000年以后的和平奖得主:
SELECT winner FROM nobel WHERE yr >= 2000 AND subject = 'Peace';1980到1989年的文学奖,返回所有字段:
SELECT * FROM nobel WHERE subject = 'Literature' AND yr BETWEEN 1980 AND 1989;显示所有美国总统获奖者:
SELECT * FROM nobel WHERE winner IN ('Theodore Roosevelt', 'Woodrow Wilson', 'Jimmy Carter', 'Barack Obama');名字以John开头的获奖者:
SELECT winner FROM nobel WHERE winner LIKE 'John %';注意这里用的是'John %'而不是'John%'。SQLZOO这道题要求的是英文名里的“名+空格+姓”格式,'John %'强制要求John后面有一个空格再接姓氏;如果写'John%',像Johnston这种名字也会被匹配出来,结果就错了。这是一个很经典的LIKE边界题。
物理奖1980年或化学奖1984年的所有记录:
SELECT * FROM nobel WHERE (subject = 'Physics' AND yr = 1980) OR (subject = 'Chemistry' AND yr = 1984);1980年获奖者,但不要化学和医学:
SELECT * FROM nobel WHERE yr = 1980 AND subject NOT IN ('Chemistry', 'Medicine');1910年前的医学奖或2004年后的文学奖:
SELECT * FROM nobel WHERE (subject = 'Medicine' AND yr < 1910) OR (subject = 'Literature' AND yr >= 2004);包含单引号的姓名查询(O'Neill):
SELECT * FROM nobel WHERE winner = 'Eugene O''Neill';在标准SQL字符串里,单引号用两个单引号转义。不要用反斜杠\',在MySQL默认配置下反而会报错或得到错误结果。
名字以Sir开头的获奖者,按年份降序、姓名升序:
SELECT winner, yr, subject FROM nobel WHERE winner LIKE 'Sir %' ORDER BY yr DESC, winner;1984年获奖者按物理、化学优先排序:
SELECT winner, subject FROM nobel WHERE yr = 1984 ORDER BY subject IN ('Physics', 'Chemistry'), subject, winner;这题很隐蔽。subject IN ('Physics', 'Chemistry')返回的是布尔值,在ORDER BY里会被当成0/1参与排序。要这两个类别排在前面,按布尔值升序时,1在前面,0在后面。这个技巧在业务里做自定义优先级排序时非常实用。
3. 中阶章核心答案:JOIN、聚合与子查询
中阶章开始需要真正理解表间关系,很多人就是在这里开始卡住。我的经验是:不要急着背SQL,先把表结构画出来,搞清楚主键和外键,再动笔写。
3.1 JOIN 与 More JOIN:多表连接不要只看答案,要看连接类型
JOIN章节的数据是2012年欧洲杯进球数据,三张表:game、goal、eteam。先记一下关联关系:
- game表和goal表通过
game.id = goal.matchid关联 - goal表和eteam表通过
goal.teamid = eteam.id关联 - game表里的team1和team2也指向eteam表
德国队进球的matchid和球员:
SELECT matchid, player FROM goal WHERE teamid = 'GER';这道题只需要goal表,还算不上JOIN。
查询比赛1012的球场、球队和日期:
SELECT id, stadium, team1, team2 FROM game WHERE id = 1012;德国队进球对应的球员、球队、球场和日期:
SELECT player, teamid, stadium, mdate FROM game JOIN goal ON game.id = goal.matchid WHERE teamid = 'GER';这里是INNER JOIN,只有两边都匹配的行才会返回。如果某个进球匹配不到比赛,这一行就会被丢掉。
球员名字以Mario开头的进球记录:
SELECT team1, team2, player FROM game JOIN goal ON game.id = goal.matchid WHERE player LIKE 'Mario%';前10分钟进球对应的球员、队伍、教练和进球时间:
SELECT player, teamid, coach, gtime FROM goal JOIN eteam ON goal.teamid = eteam.id WHERE gtime <= 10;主教练是Fernando Santos的比赛日期和队伍名:
SELECT mdate, teamname FROM game JOIN eteam ON game.team1 = eteam.id WHERE coach = 'Fernando Santos';华沙国家体育场进球的球员:
SELECT player FROM game JOIN goal ON game.id = goal.matchid WHERE stadium = 'National Stadium, Warsaw';每支球队的进球数:
SELECT teamname, COUNT(*) AS goals FROM eteam JOIN goal ON eteam.id = goal.teamid GROUP BY teamname;每个球场的进球数:
SELECT stadium, COUNT(*) AS goals FROM game JOIN goal ON game.id = goal.matchid GROUP BY stadium;波兰参与的比赛和进球数:
SELECT matchid, mdate, COUNT(*) AS goals FROM game JOIN goal ON game.id = goal.matchid WHERE team1 = 'POL' OR team2 = 'POL' GROUP BY matchid, mdate;德国在每场比赛中的进球数(包括没有进球的比赛):
这类题如果要求把没进球的比赛也保留下来,就必须用LEFT JOIN:
SELECT game.id, mdate, COUNT(goal.teamid) AS goals FROM game LEFT JOIN goal ON game.id = goal.matchid WHERE team1 = 'GER' OR team2 = 'GER' GROUP BY game.id, mdate;注意这里用COUNT(goal.teamid),因为COUNT(*)会把LEFT JOIN产生的NULL行也数进去。
最终比分统计:
这是JOIN章节的压轴题,要在一行里显示两队比分:
SELECT mdate, team1, SUM(CASE WHEN teamid = team1 THEN 1 ELSE 0 END) AS score1, team2, SUM(CASE WHEN teamid = team2 THEN 1 ELSE 0 END) AS score2 FROM game LEFT JOIN goal ON game.id = goal.matchid GROUP BY mdate, team1, team2 ORDER BY mdate, matchid;用SUM(CASE WHEN...)做横向统计是标准套路。LEFT JOIN是为了保留0比0的比赛,否则没有进球的比赛会直接消失。
More JOIN章节的电影数据库更复杂,movie、actor、casting三张表,casting表里的ord字段表示第几主演。这个章节的核心就一句话:从电影找演员,和从演员找电影之间的反复切换。
1962年的电影:
SELECT id, title FROM movie WHERE yr = 1962;《公民凯恩》的年份:
SELECT yr FROM movie WHERE title = 'Citizen Kane';所有Star Trek系列电影:
SELECT id, title, yr FROM movie WHERE title LIKE '%Star Trek%' ORDER BY yr;Glenn Close的actor id:
SELECT id FROM actor WHERE name = 'Glenn Close';《卡萨布兰卡》的movie id:
SELECT id FROM movie WHERE title = 'Casablanca';《卡萨布兰卡》的演员名单:
SELECT name FROM actor JOIN casting ON casting.actorid = actor.id WHERE movieid = (SELECT id FROM movie WHERE title = 'Casablanca');《异形》(Alien)的演员名单:
SELECT name FROM actor JOIN casting ON casting.actorid = actor.id JOIN movie ON casting.movieid = movie.id WHERE title = 'Alien';哈里森·福特出演的电影:
SELECT title FROM movie JOIN casting ON casting.movieid = movie.id JOIN actor ON casting.actorid = actor.id WHERE name = 'Harrison Ford';哈里森·福特非第一主演的电影:
SELECT title FROM movie JOIN casting ON casting.movieid = movie.id JOIN actor ON casting.actorid = actor.id WHERE name = 'Harrison Ford' AND ord <> 1;1962年电影的第一主演:
SELECT title, name FROM movie JOIN casting ON casting.movieid = movie.id JOIN actor ON casting.actorid = actor.id WHERE yr = 1962 AND ord = 1;Rock Hudson每年主演超过2部电影的年份:
SELECT yr, COUNT(title) AS movies FROM movie JOIN casting ON casting.movieid = movie.id JOIN actor ON casting.actorid = actor.id WHERE name = 'Rock Hudson' GROUP BY yr HAVING COUNT(title) > 2;Julie Andrews主演电影里的第一主演:
SELECT title, name FROM movie JOIN casting ON casting.movieid = movie.id JOIN actor ON casting.actorid = actor.id WHERE movieid IN ( SELECT movieid FROM casting WHERE actorid = (SELECT id FROM actor WHERE name = 'Julie Andrews') ) AND ord = 1;这道题用了两层子查询,是More JOIN章节的分水岭。先找出Julie参演的电影集合,再在这些电影里筛第一主演。
出演过15部以上电影第一主角的演员:
SELECT name FROM actor JOIN casting ON casting.actorid = actor.id WHERE ord = 1 GROUP BY name HAVING COUNT(*) >= 15 ORDER BY name;1978年电影主演数量排名:
SELECT title, COUNT(*) AS actors FROM movie JOIN casting ON casting.movieid = movie.id WHERE yr = 1978 GROUP BY title ORDER BY COUNT(*) DESC, title;和Art Garfunkel合作过的演员:
SELECT DISTINCT name FROM actor JOIN casting ON casting.actorid = actor.id WHERE movieid IN ( SELECT movieid FROM casting JOIN actor ON casting.actorid = actor.id WHERE name = 'Art Garfunkel' ) AND name <> 'Art Garfunkel';3.2 SUM and COUNT:GROUP BY / HAVING 的组合逻辑
这一章是聚合查询入门,难度不大,但必须形成肌肉记忆。重点区分WHERE和HAVING的过滤时机。
全球总人口:
SELECT SUM(population) FROM world;所有不重复的大洲:
SELECT DISTINCT continent FROM world;非洲GDP总和:
SELECT SUM(gdp) FROM world WHERE continent = 'Africa';面积至少100万平方公里的国家数量:
SELECT COUNT(name) FROM world WHERE area >= 1000000;波罗的海三国人口总和:
SELECT SUM(population) FROM world WHERE name IN ('Estonia', 'Latvia', 'Lithuania');每个大洲的国家数量:
SELECT continent, COUNT(name) FROM world GROUP BY continent;人口至少一千万的每个大洲的国家数量:
SELECT continent, COUNT(name) FROM world WHERE population >= 10000000 GROUP BY continent;每个大洲的总人口:
SELECT continent, SUM(population) FROM world GROUP BY continent;3.3 SELECT within SELECT:三种子查询模式的实战拆解
子查询是很多人的分水岭,SQLZOO这章设计得非常好,覆盖了三种常用模式:标量子查询、IN子查询、相关子查询。
比俄罗斯人口多的国家:
SELECT name FROM world WHERE population > (SELECT population FROM world WHERE name = 'Russia');这是标量子查询,子查询只返回一个值,然后参与外层比较。
比英国人均GDP高的欧洲国家:
SELECT name FROM world WHERE continent = 'Europe' AND gdp / population > (SELECT gdp / population FROM world WHERE name = 'United Kingdom');和阿根廷或澳大利亚同属一个大洲的国家:
SELECT name, continent FROM world WHERE continent IN (SELECT continent FROM world WHERE name IN ('Argentina', 'Australia')) ORDER BY name;这是IN子查询,子查询返回一列值,外层用IN去匹配。
比德国人口更多的欧洲国家:
SELECT name FROM world WHERE continent = 'Europe' AND population > (SELECT population FROM world WHERE name = 'Germany');GDP高于所有欧洲国家的国家:
SELECT name FROM world WHERE gdp > (SELECT MAX(gdp) FROM world WHERE continent = 'Europe');这题也可以用gdp > ALL (SELECT gdp FROM world WHERE continent = 'Europe'),但MAX写法更直白、更好理解。
每个大洲中面积最大的国家:
SELECT continent, name, area FROM world x WHERE area >= ALL (SELECT area FROM world y WHERE y.continent = x.continent);这里就是相关子查询。外层表用别名x,内层查询引用x.continent,每行外层记录都会执行一次子查询。这种写法是SQLZOO的精髓,面试也经常考。
每个大洲按字母排序第一个国家:
SELECT continent, name FROM world x WHERE name <= ALL (SELECT name FROM world y WHERE y.continent = x.continent);掌握ALL的写法之后,这类“分组取最值/取第一个”的题就是一个模板。
4. 高阶章答案与思路:Using Null、Self Join、窗口函数
到了高阶章,题目不再只是“能写出来”,而是“得理解为什么这样写”。
4.1 Using Null:LEFT / RIGHT JOIN 里的空值陷阱
Using Null章节用的是teacher和dept两张表,有些老师没有分配系部,所以dept是NULL。
没有系部的老师:
SELECT name FROM teacher WHERE dept IS NULL;注意不能用dept = NULL,NULL不等于任何值,包括它自己。
所有老师和对应系部(INNER JOIN):
SELECT teacher.name, dept.name FROM teacher INNER JOIN dept ON teacher.dept = dept.id;INNER JOIN会把dept为NULL的老师丢掉,这正好是题干要求的效果。
LEFT JOIN保留所有老师:
SELECT teacher.name, dept.name FROM teacher LEFT JOIN dept ON teacher.dept = dept.id;RIGHT JOIN保留所有系部:
SELECT teacher.name, dept.name FROM teacher RIGHT JOIN dept ON teacher.dept = dept.id;COALESCE填充手机号:
SELECT name, COALESCE(mobile, '07986 444 2266') FROM teacher;COALESCE返回第一个非NULL参数,业务里经常用来给展示值兜底。
用COALESCE显示系部名,没有就显示None:
SELECT teacher.name, COALESCE(dept.name, 'None') FROM teacher LEFT JOIN dept ON teacher.dept = dept.id;统计老师和手机数量:
SELECT COUNT(name), COUNT(mobile) FROM teacher;COUNT(列名)不会统计NULL值,而COUNT(*)会统计所有行。这里题目就是想让你对比有名字和没手机号的人数。
每个系部的老师数量:
SELECT dept.name, COUNT(teacher.name) FROM teacher RIGHT JOIN dept ON teacher.dept = dept.id GROUP BY dept.name;这里用RIGHT JOIN保留没有老师的系部,COUNT(teacher.name)对空系部返回0。
CASE分类:
SELECT name, CASE WHEN dept IN (1, 2) THEN 'Sci' ELSE 'Art' END FROM teacher;三分类CASE:
SELECT name, CASE WHEN dept IN (1, 2) THEN 'Sci' WHEN dept = 3 THEN 'Art' ELSE 'None' END FROM teacher;Using Null这章本身不难,但它是理解复杂业务报表的底座。几乎任何真实报表里都会碰到NULL处理。
4.2 Self Join:公交线路题的标准解法
Self join章节用的是爱丁堡公交线路数据,stops是站点表,route是线路站点关系表。这里最大的挑战是“同一张表既要当起点表用,又要当终点表用”。
站点总数:
SELECT COUNT(*) FROM stops;Craiglockhart站点的id:
SELECT id FROM stops WHERE name = 'Craiglockhart';4路车经过的站点(含站名):
SELECT id, name FROM stops JOIN route ON stops.id = route.stop WHERE num = 4 AND company = 'LRT';从站149到站53有哪些直达线路:
SELECT company, num, COUNT(*) FROM route WHERE stop IN (149, 53) GROUP BY company, num HAVING COUNT(*) = 2;这题的关键是找同一条线路同时包含这两个站。用GROUP BY加HAVING COUNT(*)=2,比自连接更简洁。
直接自连接找出跨站线路:
SELECT a.company, a.num, a.stop AS stop_a, b.stop AS stop_b FROM route a JOIN route b ON a.company = b.company AND a.num = b.num WHERE a.stop = 53 AND b.stop = 149;加入站点名,显示从Craiglockhart到London Road的线路:
SELECT a.company, a.num, stopa.name, stopb.name FROM route a JOIN route b ON a.company = b.company AND a.num = b.num JOIN stops stopa ON a.stop = stopa.id JOIN stops stopb ON b.stop = stopb.id WHERE stopa.name = 'Craiglockhart' AND stopb.name = 'London Road';自连接加两次JOIN stops的套路要记清楚:一次给起点站取名字,一次给终点站取名字。很多人漏了第二个JOIN stops,结果只能拿到站点id。
能从Craiglockhart直达Tollcross的线路:
SELECT DISTINCT a.company, a.num FROM route a JOIN route b ON a.company = b.company AND a.num = b.num JOIN stops stopa ON a.stop = stopa.id JOIN stops stopb ON b.stop = stopb.id WHERE stopa.name = 'Craiglockhart' AND stopb.name = 'Tollcross';从Craiglockhart到Lochend的线路:
SELECT a.company, a.num FROM route a JOIN route b ON a.company = b.company AND a.num = b.num JOIN route c ON c.company = b.company AND c.num = b.num JOIN stops stopa ON a.stop = stopa.id JOIN stops stopb ON b.stop = stopb.id WHERE stopa.name = 'Craiglockhart' AND stopb.name = 'Lochend';最后一题涉及换乘方案,逻辑更复杂。核心思路是:先把从起点出发的线路找出来,再找经过换乘站的另一条线路,判断它能否到达终点。这类“两段自连接”其实是在模拟图论里的两跳路径,SQLZOO不要求最优写法,结果对就行。
4.3 窗口函数:从函数式思维理解 OVER()
窗口函数是近几年SQL面试的高频点。SQLZOO新版的window functions章节比较少见,因为大多数练习平台还停留在GROUP BY范畴。
传统GROUP BY会把多行压成一行,窗口函数则保留所有明细行,只是在每一行旁边附加计算结果。
一个典型例子:按amount排序给贷款编号:
SELECT custid, amount, RANK() OVER (ORDER BY amount DESC) AS rnk FROM loan;如果要按客户分组排名:
SELECT custid, amount, RANK() OVER (PARTITION BY custid ORDER BY amount DESC) AS rnk FROM loan;PARTITION BY相当于分组,ORDER BY决定组内排序。窗口函数题目里最常考RANK、DENSE_RANK、ROW_NUMBER三者的差异:
| 函数 | 行为 | 典型结果 |
|---|---|---|
| RANK() | 并列名次占用后续名次 | 1, 1, 3 |
| DENSE_RANK() | 并列名次不占用后续名次 | 1, 1, 2 |
| ROW_NUMBER() | 忽略并列,强制编号 | 1, 2, 3 |
碰到SQLZOO窗口函数题时,先确定窗口范围和排序键,再去选具体函数。如果要在业务里用,我建议专练SUM(...) OVER (ORDER BY ...)这种累计求和写法,做同期累计、移动平均时非常实用。
5. 把SQLZOO的答案变成自己的SQL能力
写到这里,主要章节的答案和思路已经覆盖得差不多了。但我觉得如果只看答案,价值其实不够。真正拉开差距的是下一步。
5.1 刷题流畅之后,你必须补的性能优化课
SQLZOO的题库偏重正确性,数据量只有几百到几千行,索引、执行计划这些瓶颈完全感知不到。等到了真实业务里,一张表几千万行,同样的SQL写法可能就从毫秒级变成分钟级。
几个必须掌握的点:
- EXPLAIN:MySQL里在SQL前加EXPLAIN,可以看到有没有走索引、扫描了多少行。这是排查慢SQL的第一步。
- 避免在WHERE条件的列上做函数运算,
WHERE YEAR(birthday) = 1990会放弃索引,应该改成WHERE birthday >= '1990-01-01' AND birthday < '1991-01-01'。 - 大表JOIN前先过滤,减少参与关联的行数。
- ORDER BY、GROUP BY尽量走索引。
SQLZOO没教这些,但如果你能写出所有答案,说明你读SQL和理解表关系的能力已经过关,这时候补性能优化是最划算的。
5.2 从练习题到真实业务的三个转变
第一,数据来源更脏。练习数据是加工过的,真实业务经常有NULL、重复值、类型不一致。我在SQLZOO里做Using Null时觉得轻松,但真到了数据仓库里,每条NULL都可能意味着上游某个管道漏数。
第二,字段命名不友好。题目里的表名、字段名都很规范,业务库可能叫t_cust_info、usr_nm这种缩写,需要你先花时间搞懂数据字典,否则连表都找不到。
第三,需求是模糊的。SQLZOO的题目描述很明确,业务需求往往是一句“帮我看一下这个月的留存”,你得自己拆解成SQL逻辑。我的习惯是先写注释列出步骤,再一步步翻译成SQL,能少走一半弯路。
5.3 推荐继续深入的方向和资源
如果你把SQLZOO所有章节都过了一遍,可以按照下面的方向继续:
- LeetCode数据库题库:偏面试,题目更贴近业务,比如连续登录、排名、留存。
- DataLemur这类真实业务SQL场景平台:适合有业务背景的读者。
- 官方文档:MySQL、PostgreSQL的窗口函数和公共表表达式(CTE)部分。
- 实际项目:找一个公开数据集,自己建表、导数据、写报表SQL。
我个人建议是,刷完SQLZOO后不要急着背下一套题,而是找一份本地数据集,把在SQLZOO里学的JOIN、聚合、窗口函数都用一遍。比如拿一个订单表,按月统计销售额、算环比、求品类累计占比。这些做完,SQL基本就真正上手了。
最后分享一个我自己的习惯:不要在一个地方连续刷超过两个小时。SQLZOO越到后面,越需要脑子清醒地看表结构。卡在某道题上超过二十分钟,果断跳过,第二天再回来看,大概率一眼就能想通。刷题是为了形成条件反射,不是和自己较劲。