SQLZOO章节答案详解:从基础查询到窗口函数
2026/9/13 7:12:32 网站建设 项目流程

刷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 basicsworldWHERE、IN、BETWEEN
SELECT from WORLDworldLIKE、算术运算、ROUND、字符串函数
SELECT from Nobelnobel多条件、IN、LIKE、排序、转义
SELECT within SELECTworld标量子查询、相关子查询、ALL/ANY
SUM and COUNTworldGROUP BY、HAVING、聚合函数
JOINgame / goal / eteamINNER JOIN、多表关联、CASE统计
More JOINmovie / actor / casting演员-电影多对多、ord角色
Using Nullteacher / deptLEFT/RIGHT JOIN、COALESCE、CASE
Self joinstops / route自连接、换乘查询
Window functionsloan 等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_infousr_nm这种缩写,需要你先花时间搞懂数据字典,否则连表都找不到。

第三,需求是模糊的。SQLZOO的题目描述很明确,业务需求往往是一句“帮我看一下这个月的留存”,你得自己拆解成SQL逻辑。我的习惯是先写注释列出步骤,再一步步翻译成SQL,能少走一半弯路。

5.3 推荐继续深入的方向和资源

如果你把SQLZOO所有章节都过了一遍,可以按照下面的方向继续:

  • LeetCode数据库题库:偏面试,题目更贴近业务,比如连续登录、排名、留存。
  • DataLemur这类真实业务SQL场景平台:适合有业务背景的读者。
  • 官方文档:MySQL、PostgreSQL的窗口函数和公共表表达式(CTE)部分。
  • 实际项目:找一个公开数据集,自己建表、导数据、写报表SQL。

我个人建议是,刷完SQLZOO后不要急着背下一套题,而是找一份本地数据集,把在SQLZOO里学的JOIN、聚合、窗口函数都用一遍。比如拿一个订单表,按月统计销售额、算环比、求品类累计占比。这些做完,SQL基本就真正上手了。

最后分享一个我自己的习惯:不要在一个地方连续刷超过两个小时。SQLZOO越到后面,越需要脑子清醒地看表结构。卡在某道题上超过二十分钟,果断跳过,第二天再回来看,大概率一眼就能想通。刷题是为了形成条件反射,不是和自己较劲。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询