牛客网SQL入门题库刷题指南:从SELECT到窗口函数全攻略 我前后刷了三遍牛客网的SQL题库。第一遍是带刚转行的朋友熟悉SQL第二遍是自己跳槽前系统梳理知识点第三遍是为了写这篇东西把每个入门题的考点拆开重做了一次。如果你现在也在找一套能容纳零基础新手、又有明确难度分级的练习场牛客网的SQL入门题库是我目前见过最顺手的一套。这系列题目最大的特点是“小而准”没有冗长的业务背景绕弯子每道题都钉在一个具体的SQL语法点上从最简单的SELECT限定条件一路铺到多表JOIN、聚合统计、窗口函数。做完一遍你就等于把日常开发里七八成常用的SQL写法过了一遍。这篇文章我会从题库结构、刷题路线、核心考点、完整实操到踩坑记录一条龙说完。里面所有题目风格和内容均参考牛客网SQL入门题库的常见形式但具体示例是我按相同难度单独设计的可以直接照着练。1. 题库设计与刷题路线先搞懂它考什么再动手1.1 牛客网SQL题库的定位与优势在线SQL刷题平台不少但牛客网这个入门题库有一层很关键的筛选逻辑它把“会不会写这条SQL”和“会不会处理这个业务”剥离开。入门阶段你很容易被复杂业务绕晕带着一肚子条件去拼SQL最后根本分不清是JOIN没学会还是WHERE写错了。牛客网这套题把业务场景压到最薄每道题只聚焦一个语法点这是它适合新手的第一原因。第二个优势是它自带在线评测系统。提交SQL之后平台会拿你的结果和标准结果做逐字段比对多一列少一列、多一行少一行都会判错。这种判定方式相当“较真”但恰恰能逼着你养成精确匹配字段的习惯——很多初学者在本地数据库里跑出结果就算完事了完全不会检查列名顺序、NULL值表现和去重逻辑到了面试手写SQL环节一写就碎就是在练习阶段少了这层严格校验。第三个优势是评论区质量高。每道题下面都有大量解法讨论包括各种不同版本的思路从最朴素的子查询到窗口函数的一行写法。刷题卡住时先别急着查答案去看评论区里别人是怎么拆解的比自己做十道题还有用。1.2 一条我自己验证过的刷题路线牛客网SQL入门题库本身是按难度递进的但我建议你按专题来刷而不是纯粹按题号顺刷。我复盘了三遍之后总结了一把比较稳的顺序基础查询SELECT、WHERE、ORDER BY、LIMIT。把“取哪几列、过滤哪些行、按什么顺序展示”这三件事练成条件反射。单表聚合GROUP BY、HAVING、聚合函数COUNT、SUM、AVG、MAX、MIN。重点理解分组前后行数的变化。多表连接INNER JOIN、LEFT JOIN、RIGHT JOIN、FULL JOIN的概念差异以及ON和WHERE到底在什么阶段起作用。子查询标量子查询、列子查询、派生表FROM子句里的临时表。窗口函数ROW_NUMBER、RANK、DENSE_RANK、SUM() OVER(PARTITION BY ...)这是入门题转到进阶题的分水岭。综合应用把上面几个点混在一起做的题目这时候开始训练“先拆需求、再写代码”的习惯。按这个路线刷每个阶段它都给你足够多的重复重复到你对某个语法完全不假思索就可以往下一层走了。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_nameMySQL在某些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、3WHERE 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这关练够量一定会过。