☰
牛客网SQL入门题库刷题指南:从SELECT到窗口函数全攻略
2026/10/8 3:07:47 网站建设 项目流程

我前后刷了三遍牛客网的SQL题库。第一遍是带刚转行的朋友熟悉SQL,第二遍是自己跳槽前系统梳理知识点,第三遍是为了写这篇东西,把每个入门题的考点拆开重做了一次。如果你现在也在找一套能容纳零基础新手、又有明确难度分级的练习场,牛客网的SQL入门题库是我目前见过最顺手的一套。

这系列题目最大的特点是“小而准”:没有冗长的业务背景绕弯子,每道题都钉在一个具体的SQL语法点上,从最简单的SELECT限定条件,一路铺到多表JOIN、聚合统计、窗口函数。做完一遍,你就等于把日常开发里七八成常用的SQL写法过了一遍。这篇文章我会从题库结构、刷题路线、核心考点、完整实操到踩坑记录,一条龙说完。里面所有题目风格和内容均参考牛客网SQL入门题库的常见形式,但具体示例是我按相同难度单独设计的,可以直接照着练。

1. 题库设计与刷题路线:先搞懂它考什么,再动手

1.1 牛客网SQL题库的定位与优势

在线SQL刷题平台不少,但牛客网这个入门题库有一层很关键的筛选逻辑:它把“会不会写这条SQL”和“会不会处理这个业务”剥离开。入门阶段你很容易被复杂业务绕晕,带着一肚子条件去拼SQL,最后根本分不清是JOIN没学会还是WHERE写错了。牛客网这套题把业务场景压到最薄,每道题只聚焦一个语法点,这是它适合新手的第一原因。

第二个优势是它自带在线评测系统。提交SQL之后,平台会拿你的结果和标准结果做逐字段比对,多一列少一列、多一行少一行都会判错。这种判定方式相当“较真”,但恰恰能逼着你养成精确匹配字段的习惯——很多初学者在本地数据库里跑出结果就算完事了,完全不会检查列名顺序、NULL值表现和去重逻辑,到了面试手写SQL环节一写就碎,就是在练习阶段少了这层严格校验。

第三个优势是评论区质量高。每道题下面都有大量解法讨论,包括各种不同版本的思路,从最朴素的子查询到窗口函数的一行写法。刷题卡住时先别急着查答案,去看评论区里别人是怎么拆解的,比自己做十道题还有用。

1.2 一条我自己验证过的刷题路线

牛客网SQL入门题库本身是按难度递进的,但我建议你按专题来刷,而不是纯粹按题号顺刷。我复盘了三遍之后,总结了一把比较稳的顺序:

  1. 基础查询:SELECT、WHERE、ORDER BY、LIMIT。把“取哪几列、过滤哪些行、按什么顺序展示”这三件事练成条件反射。
  2. 单表聚合:GROUP BY、HAVING、聚合函数(COUNT、SUM、AVG、MAX、MIN)。重点理解分组前后行数的变化。
  3. 多表连接:INNER JOIN、LEFT JOIN、RIGHT JOIN、FULL JOIN的概念差异,以及ON和WHERE到底在什么阶段起作用。
  4. 子查询:标量子查询、列子查询、派生表(FROM子句里的临时表)。
  5. 窗口函数:ROW_NUMBER、RANK、DENSE_RANK、SUM() OVER(PARTITION BY ...),这是入门题转到进阶题的分水岭。
  6. 综合应用:把上面几个点混在一起做的题目,这时候开始训练“先拆需求、再写代码”的习惯。

按这个路线刷,每个阶段它都给你足够多的重复,重复到你对某个语法完全不假思索,就可以往下一层走了。

1.3 入门题和进阶题的边界感在哪

很多人刷到一半会困惑:这道题明明要用窗口函数,为什么还归在“入门”里?我自己的判断标准是:不依赖复杂CASE表达式、不需要多层嵌套子查询、不需要递归CTE和正则,只要能用标准SQL语法点组合出结果,就还是入门题。入门题的复杂度在“点”上,不在“面”上;它可能用到COUNT和DISTINCT的组合,但不会让你处理跨表递归的父子关系。

所以刷题时不要被“入门”两个字误导。你真正该关注的是每道题背后那个考点清单,而不是难度标签。把每一道题的考点记下来,刷完20题之后回头翻一翻,你会看到语法点的复现率很高——约束条件是颠倒着考你的,JOIN是换着表反复出现的。这种重复设计对建立肌肉记忆特别有效,我在陪朋友刷完第一遍后,最大的感触就是:“常用写法不用想了,手直接就打出来了。”

2. 入门题核心考点逐个拆解:从SELECT到窗口函数

2.1 WHERE限定条件:等于、不等于与NULL陷阱

入门题库的第一梯队基本在练WHERE。正常人都会写WHERE salary > 10000这类区间过滤,真正的坑在“不等于”和NULL上。比如你想筛出所有不是“研发部”的员工,直觉会写WHERE department != '研发部',但如果这个表里有些员工部门是NULL,这批人在MySQL里的结果是什么?答案是:不会被查出来。SQL的NULL参与比较运算,结果不是TRUE也不是FALSE,而是UNKNOWN,所以NULL != '研发部'的结果是UNKNOWN,行会被过滤掉。

这个考点牛客网入门题会换着方式考。正确写法要额外加上OR department IS NULL,或者用IFNULL(department, '') != '研发部'类的表达式把NULL先转成空串再比较。你只有在刷题时遇到过这个坑,到写真实报表时才会条件反射地想到:这个字段允许为NULL吗?这就是刷题的价值——SQL语法的行为边界是在一点点试错中建立起来的。

2.2 ORDER BY与LIMIT:排序结果里的隐藏行为

排序看起来简单,但有几个细节是牛客网入门题很喜欢埋雷的地方。第一,多字段排序的顺序。ORDER BY a DESC, b ASC只对a进行降序,b仍然升序;如果想让两个字段都降序必须写ORDER BY a DESC, b DESC。这个点新手特别容易误读。

第二,NULL值的排序位置。不同数据库处理不一致:MySQL里ORDER BY col ASC时NULL排在最前,ORDER BY col DESC时NULL排在最后;而SQL Server默认相反。牛客网的判题环境对NULL值有严格预期,你最好在练习时养成明确写IS NULL的习惯,而不是依赖默认排序位置。

第三,LIMIT与OFFSET的配合。取第3到第5行,很多人会漏写偏移量,直接LIMIT 5取成了前5行。正确的翻页写法是LIMIT 3 OFFSET 2,先偏移2行,再取3行。这类题目看似基础,却是面试手写SQL时最容易露怯的地方——因为平时不太用,一紧张就把OFFSET忘了。

2.3 GROUP BY聚合:COUNT计数的那些坑

很多人在这个专题疯狂丢分,原因不是不会写GROUP BY,而是不会数数。COUNT(*)是数行数,COUNT(字段)是数该字段非NULL的个数,COUNT(DISTINCT 字段)是去重后的非NULL个数。这三个含义完全不同,牛客网题目里换个条件就能变成三道题。比如统计每个部门的员工数,用的是COUNT(*);统计每个部门有邮箱的人数,用的是COUNT(email);统计每个部门涉及多少个不同城市,用的是COUNT(DISTINCT city)。

聚合查询还有一个隐藏要求:SELECT的标准写法里,GROUP BY后面的分组字段才能出现在SELECT的非聚合列中。比如SELECT department, COUNT(*) FROM employees GROUP BY department没问题,但如果SELECT里多了一个employee_name,MySQL在某些sql_mode下会直接报错,在另一些模式下会随机返回分组内某行的人名。牛客网判题环境通常比较严格,这类一提交就错了。我的建议是在写聚合SQL时养成良好习惯:非聚合列要么放进GROUP BY,要么用MAX、MIN之类的聚合函数包起来。

HAVING则是给分组后的结果做过滤,很多人会拿它和WHERE混用。记住一句口诀:WHERE在分组之前过滤行,HAVING在分组之后过滤组。比如“筛掉员工少于3人的部门”就是典型的HAVING场景,因为“部门人数”这个条件依赖聚合结果,WHERE阶段根本算不出来。

2.4 表连接:INNER JOIN和LEFT JOIN怎么选

多表连接的考点,核心是搞清楚“保留哪张表的全部行”。INNER JOIN只保留满足ON条件的两边匹配到的记录,不匹配的直接丢弃;LEFT JOIN保证左表所有行都会出现在结果里,右表没匹配到就用NULL填充。牛客网题库里最常见的一种考法是反过来:题目要求“统计所有部门的人数,包括没有员工的部门”。如果你写成INNER JOIN,没有员工的部门就消失了,统计结果少了行;只有LEFT JOIN才能保住每一个部门。

这个差别看着不大,但在实际的BI报表里会导致严重的统计误差。我见过有人统计全国门店数量,因为用了INNER JOIN,把那些还没开张、没有销售数据的门店全丢掉了,最后汇报少了三分之一的门店——这种错误在练习阶段就要把它练成本能反应。

还有一个关于ON和WHERE的考点也要单独说:在LEFT JOIN里,如果过滤条件写在WHERE中,通常会先过滤再连接,右表过滤后没有匹配时,左表行被保留但右表字段为NULL;如果过滤条件写在ON里,结果则完全不同。最典型的是“查所有部门及其工资超过5000的员工数”,很多人把员工条件写在WHERE里,结果无员工部门全没了,和需求里的“所有部门”完全不符。所以写LEFT JOIN时,想保留左表行,就把对右表的过滤条件放进ON。

2.5 子查询与派生表:千万别漏了那一个别名

子查询在入门题里出现的频率很高,最常见的形态是:先在内层按条件算出某个结果,再和主查询关联比较。比如“找出工资高于部门平均工资的员工”,直觉写法是WHERE salary > (SELECT AVG(salary) FROM employees e2 WHERE e2.department = e1.department),这里外层表的别名e1被内层引用,形成相关子查询。相关子查询的执行逻辑是外层每取一行,内层就执行一次,性能不是最好的,但逻辑清晰,非常适合入门理解。

派生表(FROM子句里的子查询)有一个所有数据库通用的硬性要求:必须给子查询结果起别名。SELECT * FROM (SELECT department, AVG(salary) AS avg_sal FROM employees GROUP BY department) t这个t就是必需且不能省的,很多新手第一次写的时候会漏掉,然后被报错卡半天。这类错误牛客网的报错信息其实已经提示得比较明确了,但新手容易忽略“Every derived table must have its own alias”这类关键句。

2.6 窗口函数:入门题到进阶题的分水岭

窗口函数是牛客网SQL入门题里“超纲”却又绕不开的存在。入门后期会出现“按部门分组,给每个员工按工资排序”这类题目,排序并分组的同时,每一行原本的信息都还保留着——这在GROUP BY下做不出来,因为分组会把多行压成一行。而窗口函数ROW_NUMBER() OVER(PARTITION BY department ORDER BY salary DESC)完美解决:按部门划窗、窗口内按工资降序编号、最后每一行的员工信息都在。

你还需要分清三种排序函数在并列值上的差异:

函数并列值处理方式典型场景
ROW_NUMBER并列值随机指定顺序,编号不重复需要对每行生成唯一序号
RANK并列值同号,后续号跳跃(1,1,3)比赛排行、工资并列排名
DENSE_RANK并列值同号,后续号连续(1,1,2)连续排名,不看跳号

实话说,我刚接触这三个函数时也分不清,考试前背了好几遍。直到刷题时遇到“求每个部门工资排名前三的员工”,用RANK会导致并列第三被漏掉,用DENSE_RANK才能全取出来——那一刻我才彻底记住。这类题牛客网入门题库里至少有两三道,刷到它们时建议放慢速度,把三种函数分别跑一遍看结果差异。

3. 实操复盘:一道典型的入门综合题是怎么解出来的

3.1 题目拆解与需求分析

下面用一道按牛客网入门题库风格设计的综合题来走完整流程。题目背景是员工表和部门表,需求是:查询每个部门中工资最高的员工信息,输出部门名、员工姓名、员工工资;若多个员工工资并列最高,全部列出。

看到这类题,第一件事不是写代码,而是拆条件。拆成三块:一是“每个部门”,说明要按部门分组或分区;二是“工资最高”,这是窗口内的极值;三是“并列最高全部列出”,意味着不能简单用GROUP BY + MAX,因为在标准SQL里,你无法在GROUP BY后取出工资最高那一行的非聚合字段。想通了这一点,字段的选择和表连接方向基本就定了:需要员工表和部门表做连接,再在连接结果上按部门分区排序。

3.2 手写第一版:用内连接和派生表实现

先用最直观的方式解决:先按部门算工资最大值,再把它作为临时表和员工表连接。代码如下:

SELECT d.dept_name, e.emp_name, e.salary FROM dept d JOIN emp e ON d.dept_id = e.dept_id JOIN ( SELECT dept_id, MAX(salary) AS max_sal FROM emp GROUP BY dept_id ) t ON e.dept_id = t.dept_id AND e.salary = t.max_sal ORDER BY d.dept_name;

这里有一个关键内行细节:连接条件除了部门相等,还需要e.salary = t.max_sal。只有工资精确等于部门最大值,才能保证员工行被捞出来;少了这个条件,结果就会变成每个部门所有员工的工资都带上部门最大工资字段,完全跑偏。这个派生表先按部门聚合,得到每个部门的最高工资,再反过来拉取员工明细的思路,是解决“分组取明细”问题的经典套路。

3.3 补充边界条件:处理多个最高工资

第一版代码本身已经能输出“并列最高全部列出”的结果,因为工资等于最大值的员工会全部连上。但还有一个容易踩的边界:如果某个部门压根没有员工,LEFT JOIN才能保证部门还出现在结果里。题目要求查每个部门,我实际操作时会把JOIN改成LEFT JOIN,把员工表换成驱动表侧,确保“无人的部门”也能以NULL员工信息出现在结果中。这个动作在牛客网判题环境里不一定每次都死,但养成这个习惯对真实报表场景非常重要。

实际提交后我发现还有一层的隐藏问题:ORDER BY字段的排序顺序。有些判题系统对结果集的顺序有隐含要求,多字段排序时最好在主排序基础上再加一个稳定排序,比如ORDER BY d.dept_name, e.salary DESC,避免结果行顺序不稳定导致比对失败。

3.4 第二版思路:窗口函数一行排序

接着换窗口函数重写,这是上一节提过的进阶解法:

SELECT dept_name, emp_name, salary FROM ( SELECT d.dept_name, e.emp_name, e.salary, DENSE_RANK() OVER(PARTITION BY d.dept_name ORDER BY e.salary DESC) AS rk FROM dept d LEFT JOIN emp e ON d.dept_id = e.dept_id ) t WHERE rk = 1;

这里用DENSE_RANK而不是RANK,是因为需求中“并列最高全部列出”用DENSE_RANK取排名等于1即可,两个不同员工工资并列最高时,rk都为1。如果用RANK,其实在这个场景下也能用,但当存在两个并列第一时,排名序列是1、1、3,WHERE rk = 1依然有效;只是后续如果扩展到“取前三名”,RANK会漏掉并列第三,DENSE_RANK更稳妥。我建议在练习时把这句代码和上面的派生表版本都跑一遍,然后对比执行计划,你会发现窗口函数版本逻辑更清晰、代码更短、查错更容易。

3.5 在线评测的判题逻辑与自测技巧

牛客网这类OJ平台判题时会拿你的查询结果集和目标结果集做逐行逐列比对。这意味着,差一个空格、多一个NULL、多一行重复,都会显示不通过。我自己在刷题时养成了一个习惯:先在本地IDE写SQL并查看结果,确认数据正确后再提交到平台。用本地IDE和平台环境有个差异需要注意——平台自带的表结构、字段名和数据是固定的,你本地建表时最好完全复刻它的字段类型和约束,否则NULL、字符串排序等行为会有微妙差异。

另一个判题细节是列名的顺序和别名。查询结果和标准结果是按列名做映射的,有些平台不看列名只看位置,但牛客网会对列名本身做校验。输出列时最好把别名起得和题目示例完全一致,比如AS dept_name和直接输出dept_name,展示效果一样,但有些判题器会把别名差异算作错误。为了少走弯路,题目要求你输出什么列名就写什么列名。

4. 刷题过程中最常见的5类报错与排查方法

4.1 结果为空,但数据里明明有值

这种情况新手遇到最多。原因通常有三个:字段名大小写不一致、表和字段的引用张冠李戴、连接条件写反。在牛客网的练习环境里,你先执行一遍不带WHERE的查询,确认原始数据能查出来,再逐步加上过滤条件,逐层排查。不要对着一个大SQL猜,SQL是流水线逻辑,哪一步出问题只影响那一步之后的结果。

还有一个经典原因就是我在前面讲过的NULL参与比较。凡是结果比预期少,优先看一眼参与条件的字段是否存在NULL。写查询之前先确认字段是否允许为空,是排查思路的第一步。

4.2 结果里凭空多出重复行

重复行的根源往往出在JOIN上。如果你的连接字段一端的值是重复的,结果集会变成笛卡尔积的局部放大。比如员工表和部门表连接时,如果部门表每个部门有多行记录,每个员工就会被连上多遍,重复行自然膨胀。排查方法是用SELECT DISTINCT先看数据本身是否重复,还是连接导致重复;再用GROUP BY + COUNT核对某个员工出现的次数,很快就能定位到是哪张表的哪一行重复了。

如果查询本身有DISTINCT,结果还是重复,那就要怀疑是不是字段类型或字符集问题导致两行看起来相同但底层存储不同,比如尾部空格。这个情况在平台题库里相对少见,但在真实数据里很常见,也算一个经验储备。

4.3 出现一堆不明不白的NULL

结果集里该有值的位置出现NULL,常见原因是使用了LEFT JOIN但右表没匹配到。这在业务意义上正常,但判题系统对NULL非常敏感。我在实战中的标准动作是:先确认需求到底要不要显示NULL,如果不需要,在JOIN后的字段上用IFNULL(字段, 0)或COALESCE值填充;如果需要显示,就保持原样。

另一个容易忽略的点是:COUNT(字段)不统计NULL值。比如统计每个部门实际有手机号的员工数,如果某些员工手机号字段为NULL,用COUNT(*)会把这个员工算进去,而COUNT(mobile)会把他排除在外,两个结果差距很大。这就是第2章里COUNT三种用法的实际影响,刷题时碰到一次基本就长记性了。

4.4 语法看着没错,提交却报错

最常见的语法级报错集中在三个地方:派生表没起别名(我前面强调过不止一次)、多表连接时没写清楚字段前缀、括号配对错误。这些都是肉眼很难一眼看出来的问题。我给自己的排查顺序是:先看FROM之后的每一层子查询是否都有别名,再看每个字段是否都加了正确的表别名前缀,最后把SQL逐段缩小范围单独执行。“SQL是一点一点跑通的”这句话,我每写一条复杂查询都会验证一次。

有时报错信息提示“Unknown column”,那不是数据库不知道这个字段,而是你的字段前缀写错了,或者这个词不在当前表里。这时候最快的方法是直接查看题目的表结构,逐字段核对拼写。不要觉得看表结构丢人,所有人都会看。

4.5 提交超时或返回结果集过大

入门题库的数据量都很小,正常情况下完全不可能超时。如果真遇到超时或卡顿,多半是因为相关子查询写得太差了。比如在WHERE里写了一个子查询,外层表每查一行就重跑一次内层,如果题库数据被加大,性能就会明显劣化。优化思路就是干掉逐行关联:用JOIN替代相关子查询,或者先聚合生成临时表再连接。

这类性能问题在入门阶段不是主要矛盾,但牛客网在部分题目的讨论区里会有人贴出“如何避免全表扫描”的优化讨论。我建议你把这类讨论当作进阶阅读看过一遍,至少知道WHERE salary > (SELECT ...)和JOIN (SELECT ...)之间的性能差异在哪个环节产生。

4.6 一份自检清单,提交前过一遍

经过几十道题之后,我整理出一个提交前的自检清单,每一行都是实际踩过的坑:

  • 是否漏了表别名前缀?多表查询里每个字段都指清楚来源了吗?
  • 过滤NULL的条件写了吗?字段本身允许NULL吗?
  • 分组后面用HAVING了吗?WHERE和HAVING的应用阶段分清楚了吗?
  • 连接方向对吗?LEFT JOIN的驱动表和右表过滤条件的位置对吗?
  • 派生表都有别名吗?
  • 列名和题目要求的输出列名一致吗?
  • 排序字段和题目要求的顺序一致吗?

这七条不复杂,但每次提交前默读一遍,能帮我省掉一半以上的重复提交次数。

5. 写在后面:刷完这个题库之后

如果你能完整地把牛客网这个SQL入门题库刷下来,最大的收获不是“会写几十道题”,而是脑子里建立了一个清晰的SQL语法坐标:什么场景用WHERE、什么场景用HAVING、什么场景用JOIN、什么场景用窗口函数,边界全都清晰了。

我个人在这几遍刷题中还有一个很实际的体会:刷题库和真实业务之间是有落差的,题库帮你练的是“语法正确”,业务要求的是“结果可信”。入门题刷完后,下一步是找一个真实的业务表自己出题:统计连续登录天数、计算累计求和、做环比同比,这些场景能把你在题库里学到的窗口函数、子查询、连接彻底消化成自己的内功。

最后再分享一个小经验:刷题时别怕看答案,怕的是看了答案不重写。每道题看完别人的解法后,把页面关掉,凭记忆自己敲一遍,然后再比较两个方案的差异。你可以明显感觉到思路从“背代码”变成“建模型”的那个转折点。祝刷题愉快,SQL这关,练够量一定会过。

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

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

立即咨询