☰
牛客网MySQL刷题1-20:SQL笔试高频考点与避坑指南
2026/9/28 7:03:50 网站建设 项目流程

1. 牛客网MySQL 1-20题到底在考什么:先看清这套题的脾气

先说实话,牛客网这套MySQL刷题1-20,难度不算高,它不是一个“从入门到精通”的完整教程,而是把SQL笔试里最高频的那些考点浓缩成了20道题。我去年帮团队筛后端候选人,拿这套题做过一次快速摸底,发现一个很有意思的现象:能拿满分的候选人,未必是工作经验最丰富的,但一定是对SQL执行逻辑有清晰直觉的。反过来说,很多人工作两三年,增删改查写得很溜,一到排名、分组、保留小数点这类细节就翻车。

这套题覆盖的核心知识点大概是这么几块:

  • 基础的SELECT查询与字段过滤
  • ORDER BY排序的细节,包括多字段排序、升降序混用
  • 聚合函数:COUNT、SUM、AVG、MAX、MIN,以及和GROUP BY的组合
  • 常见的字符串、日期处理函数
  • 简单子查询和JOIN连接
  • 去重、限制行数、空值处理

刷这套题的适合人群,我总结下来有三类:准备校招或跳槽面试的人、刚学完SQL语法想检验自己掌握程度的人、以及带新人的团队负责人。如果你属于这三类,那这篇东西值得看完。我会按做题顺序把1-20题的考点和数据规律拆开讲,每道题都给出可直接复现的SQL写法,并补上我在实际执行时踩过的坑。

先说一个整体判断:这套题的题目排序不完全是按难度递增排的,第1题和第20题之间难度差值不大,真正拉开差距的是对“边界情况”的处理。比如NULL值排序、COUNT(*)和COUNT(列)的差异、GROUP BY之后能不能直接SELECT非聚合列,这些面试官最爱追问的点,在题目里都有埋雷。所以刷的时候别只追求“跑通”,要追问自己一句:这个SQL换成另一种写法,结果还一样吗?如果不一样,为什么?

2. 从第1题到第20题:一题一题的考点拆解与标准写法

2.1 开局三题:查找、排序、去重的基本功

牛客网这套题的前三题,通常是给你一张员工表或者学生表,让你查出指定字段、按某字段排序、去掉重复记录。这类题看着简单,但其实是整个SQL笔试的“地基”,我见过不少人在“去重”上翻车。

第1类题的标准场景:一张employees表,包含id, name, salary, department_id, hire_date等字段,要求查出所有员工的姓名和薪水,按薪水降序排列。标准解法:

SELECT name, salary FROM employees ORDER BY salary DESC;

这里有个细节,牛客网的判题系统对字段名是大小写不敏感的,但结果集的列顺序是敏感的。你写SELECT salary, name和SELECT name, salary,在可视化工具里看着差不多,但判题系统比对的是列顺序,顺序不对直接报错。这个习惯建议从刷题第一天就养成:先看题目要求输出哪些列、按什么顺序。

第2类题是去重。比如“查询员工表中所有不同的部门ID”,最直观的写法是:

SELECT DISTINCT department_id FROM employees;

但我想提醒一点:DISTINCT是对整个SELECT列的组合去重,不是只对第一个字段去重。如果题目要求“查询每个部门ID和对应的部门名称,且部门ID不重复”,你直接写SELECT DISTINCT department_id, department_name,在department_name相同的情况下没问题,但如果同名部门在不同ID下,这种写法就露馅了——它会把ID+名称的组合都保留下来。更稳妥的写法是用GROUP BY:

SELECT department_id, MAX(department_name) AS department_name FROM employees GROUP BY department_id;

这也是刷题时的一个通用原则:DISTINCT适合单列去重,多列“按某列去重、附带其他信息”时,优先考虑GROUP BY + 聚合函数。这个思路在后面的题里会反复用到。

2.2 排名与分页:ORDER BY的隐藏学问

第4-6题左右,会集中出现排序题。除了最简单的单列排序,牛客网特别喜欢考“多字段排序”和“limit分页”。比如一道典型题目:“查询员工姓名和薪水,按薪水降序排列,如果薪水相同则按员工ID升序排列”。

SELECT name, salary FROM employees ORDER BY salary DESC, id ASC;

这里的关键点是:DESC和ASC要分别写在每个排序字段后面,而不是只在最后写一个。我见过有人写成ORDER BY salary, id DESC,以为意思是对薪水升序、ID降序,实际上这个语法表示的是薪水升序、ID降序——排序优先级还是先薪水后ID,但薪水方向被默认成了ASC。这种细节在笔试里错一次,面试官对你的印象分会立刻打折。

分页题是另一个高频考点。牛客网常考“显示第几页的数据”或“跳过前几条取接下来几条”。比如“从第2条记录开始显示3条记录”:

SELECT name, salary FROM employees ORDER BY salary DESC LIMIT 2, 3;

这里要重点说一个几乎所有初学者都会懵的点:LIMIT offset, count里的offset是从0开始计数的。LIMIT 2, 3表示跳过前2条(也就是从第3条开始),取3条。想要取“第2条到第4条”,应该写LIMIT 1, 3。很多人在这一步直接把1当成了第1条,结果取出来的数据永远对不上。判题系统只比对结果集,你肉眼很难看出问题,但提交就是不过。

顺便说一句,现在MySQL 8.0+支持了LIMIT 3 OFFSET 2这种更清晰的语法,牛客网也兼容这个写法。我个人的习惯是推荐用LIMIT offset, count,因为多数公司的老库还跑在5.7上,面试手写SQL时用老语法更保险,不会让面试官觉得你只熟新特性。

2.3 聚合与分组:COUNT、SUM、AVG的坑都藏在细节里

第7-10题开始上强度,聚合函数登场。牛客网喜欢出的场景是:统计员工人数、计算平均薪水、求每个部门的最高薪水和最低薪水。

先看一个最简单但最容易错的:统计员工总数。

SELECT COUNT(*) FROM employees;

和

SELECT COUNT(name) FROM employees;

这两条SQL在“name列没有NULL值”时结果一样,但一旦name允许为空,结果立刻不同:COUNT(*)统计所有行数,COUNT(name)只统计name非NULL的行数。这个差异在牛客网题目里经常被用来设坑——它不会明说“假设name可以为空”,但表结构里字段允许NULL,你如果没注意到,结果集就差了一行。我刷题时的习惯是:凡是用到COUNT,先看一眼表结构里哪些字段允许NULL。

再看分组统计,典型题目:“统计每个部门的平均薪水,并按平均薪水降序排列”。

SELECT department_id, AVG(salary) AS avg_salary FROM employees GROUP BY department_id ORDER BY avg_salary DESC;

这道题的考点有两个:一是GROUP BY之后只能用聚合函数和分组字段,二是ORDER BY可以用聚合函数的别名。很多人在第二步写ORDER BY AVG(salary) DESC也能通过,因为MySQL允许在ORDER BY中使用聚合函数表达式。但这里我要多说一句:WHERE和HAVING别搞混。题目如果变成“只显示平均薪水大于5000的部门”,正确写法是:

SELECT department_id, AVG(salary) AS avg_salary FROM employees GROUP BY department_id HAVING AVG(salary) > 5000 ORDER BY avg_salary DESC;

WHERE是在分组前过滤行,HAVING是在分组后过滤组。这个考点几乎每套面试题都会出现,牛客网也毫不留情地安排了。记住一个判断标准:过滤条件里只要出现聚合函数,就必须放HAVING。

2.4 子查询与连接:从单表走向多表

第11-15题是这套题的“分水岭”,开始涉及子查询和多表连接。场景通常是:一张员工表、一张部门表,要求查出“薪水高于部门平均薪水的员工”或者“没有分配部门的员工”。

经典考点一:标量子查询。比如“查询薪水高于公司平均薪水的员工姓名和薪水”:

SELECT name, salary FROM employees WHERE salary > (SELECT AVG(salary) FROM employees);

这个写法的核心是子查询返回单行单列。牛客网判题不要求你写出最优性能的SQL,所以标量子查询在这类题目里基本都能过。但如果你追求更地道的写法,可以用窗口函数(MySQL 8.0+):

SELECT name, salary FROM ( SELECT name, salary, AVG(salary) OVER() AS avg_salary FROM employees ) t WHERE salary > t.avg_salary;

两种写法结果一样,但窗口函数版本在“既要显示平均值又要过滤”的场景下更灵活。我给刷题者的建议是:先用能跑通的方式写,提交后再想一想能不能用窗口函数改写。这样既保证得分,又逼着自己进阶。

经典考点二:IN子查询。典型题目是“查询没有分配部门的员工”,表结构里员工表有department_id,部门表是departments。写法:

SELECT name FROM employees WHERE department_id NOT IN (SELECT id FROM departments);

这里有一个巨大的坑:如果子查询结果中包含NULL,NOT IN的结果可能为空集,因为NOT IN (1, 2, NULL)在SQL三值逻辑下等价于NOT (department_id = 1 OR department_id = 2 OR department_id = NULL),而department_id = NULL的结果是UNKNOWN,整个条件的结果就变成UNKNOWN,行会被过滤掉。牛客网的测试数据里如果恰好有NULL,你写NOT IN就过不了。更稳的写法是用NOT EXISTS:

SELECT name FROM employees e WHERE NOT EXISTS ( SELECT 1 FROM departments d WHERE d.id = e.department_id );

这个坑我在真实项目里踩过不止一次,所以强烈建议刷题阶段就养成“看到NOT IN就条件反射检查NULL”的习惯。

经典考点三:内连接与左连接。比如“查询每个员工及其所在部门名称,没有部门的也要显示出来”,这必须用LEFT JOIN:

SELECT e.name, d.department_name FROM employees e LEFT JOIN departments d ON e.department_id = d.id;

做这题时需要注意,LEFT JOIN的结果集行数与左表行数一致,但如果右表有重复匹配,行数会膨胀。牛客网的题目数据一般不会设计这种极端情况,但在真实业务里,一对多关系下用LEFT JOIN一定要先确认右表是否有重复关联字段,否则数据翻倍了报表就全错了。

2.5 字符串与日期处理:函数题的得分关键

第16-18题左右,会开始出现需要调用函数的题目,这也是很多“增删改查熟练工”的盲区。常见场景:把姓名拆成姓和名、按年份统计入职人数、把日期格式化成指定字符串。

字符串函数题,比如“查询员工姓名的前两个字符”:

SELECT LEFT(name, 2) FROM employees;

或者“把姓名中的‘张’替换为‘王’”:

SELECT REPLACE(name, '张', '王') FROM employees;

这类题没难度,但要注意MySQL函数名的拼写,SUBSTRING、SUBSTR、LEFT、RIGHT要写对。牛客网的判题系统对函数拼写是严格校验的,拼错了直接编译错误。

日期函数是重头戏。典型题目:“统计2023年入职的员工人数”:

SELECT COUNT(*) FROM employees WHERE YEAR(hire_date) = 2023;

或者用范围条件:

SELECT COUNT(*) FROM employees WHERE hire_date >= '2023-01-01' AND hire_date < '2024-01-01';

这里我要强调:能用范围条件就尽量别用函数包住字段。原因是,在WHERE hire_date字段上套YEAR()函数,会让索引失效,全表扫描。刷题时数据量小看不出来,但面试官问“你怎么优化这条SQL”时,这就是送分题变送命题。你回答“把YEAR(hire_date)改成范围条件”绝对加分。

日期格式化题,比如“把入职日期显示为2023/01/15这种格式”:

SELECT DATE_FORMAT(hire_date, '%Y/%m/%d') FROM employees;

%Y是四位年份,%y是两位年份,%m是带前导零的月份,%c是不带前导零的月份。别小看这些占位符的区别,我见过不止一个候选人在%m和%c上翻车。牛客网上这类题的结果比对是逐字符精确匹配的,一个零的差异都算错。

2.6 综合题:把前面所有知识串起来

第19-20题通常是综合题,可能要求你“统计每个部门中薪水高于部门平均薪水的员工人数”,或者“查出每个部门的最高薪水对应员工姓名”。

第一道综合题的核心是先算部门平均薪水,再对比员工薪水。最直观的写法是使用关联子查询:

SELECT e.department_id, COUNT(*) AS cnt FROM employees e WHERE e.salary > ( SELECT AVG(salary) FROM employees WHERE department_id = e.department_id ) GROUP BY e.department_id;

这个写法的执行逻辑是:对每一行员工,去跑一遍子查询算对应部门的平均薪水,然后比较。数据量一大性能就很差,但牛客网不卡性能,跑通没问题。如果你想展示更高阶的写法,可以用JOIN + 派生表:

SELECT d.department_id, COUNT(*) AS cnt FROM employees e JOIN ( SELECT department_id, AVG(salary) AS avg_salary FROM employees GROUP BY department_id ) d ON e.department_id = d.department_id WHERE e.salary > d.avg_salary GROUP BY d.department_id;

两种写法结果一样,但第二种思路对真实业务更有参考价值——先聚合、再关联,永远比逐行关联子查询更可控。

第二道综合题,比如“查出每个部门薪资最高的员工”,这类题我强烈推荐窗口函数,MySQL 8.0+完美支持:

SELECT department_id, name, salary FROM ( SELECT department_id, name, salary, ROW_NUMBER() OVER(PARTITION BY department_id ORDER BY salary DESC) AS rn FROM employees ) t WHERE rn = 1;

PARTITION BY department_id表示按部门开窗,ORDER BY salary DESC表示在窗口内按薪水降序排,ROW_NUMBER()给每个窗口内的行编号,最后取编号为1的行。这是“分组TopN”问题的标准解法,刷题时遇到“每个部门/每个分类/每个用户组里第几”这类描述,都可以先用这个模板套。如果是MySQL 5.7环境,才需要用“自连接 + 分组取MIN”的笨办法,但牛客网这套题的判题环境一般不会逼你写那种老写法。

3. 同一道题,两种写法:为什么结果不同?

刷题到后期,你一定会遇到一个困惑:我写的SQL和标准答案结果一样,但执行计划完全不一样。这一节我想用牛客网第1-20题中反复出现的几个场景,讲一讲“写法差异背后的执行逻辑”。

第一个场景是COUNT(DISTINCT ...)。题目会问“查询有多少个不同的部门有员工”。两种写法:

SELECT COUNT(DISTINCT department_id) FROM employees;
SELECT COUNT(*) FROM (SELECT DISTINCT department_id FROM employees) t;

结果一样,但前者是单次扫描,后者是先构建派生表再统计。数据量大时前者性能明显更好。这个考点看似简单,面试官却喜欢借它问“DISTINCT和GROUP BY去重的区别”。我的理解是:DISTINCT是对结果集做去重,GROUP BY是分组后聚合。如果只是去重不聚合,两者都可以,但GROUP BY可以配合MAX、MIN、AVG等聚合函数,这是DISTINCT做不到的。刷题时建议优先使用语义更准确的写法,这样即使题目变化,你的SQL也能快速调整。

第二个场景是WHERE与HAVING的顺序问题。下面两条SQL,第一个是错的,第二个是对的:

-- 错误写法 SELECT department_id, AVG(salary) AS avg_salary FROM employees WHERE AVG(salary) > 5000 GROUP BY department_id;
-- 正确写法 SELECT department_id, AVG(salary) AS avg_salary FROM employees GROUP BY department_id HAVING AVG(salary) > 5000;

MySQL在执行时,WHERE是逐行过滤,过滤完才分组聚合,而聚合值此时还没算出来,所以WHERE里不能出现AVG。这个逻辑如果只靠死记硬背,面试官换个说法“那我想过滤薪水大于5000的员工再按部门统计,怎么写”,你可能又懵了。正确写法是:

SELECT department_id, AVG(salary) AS avg_salary FROM employees WHERE salary > 5000 GROUP BY department_id;

看到没有,同样是“筛选 + 分组”,条件放WHERE还是HAVING,取决于你要过滤的是原始行还是聚合结果。我刷题时给自己立了一个规则:看到“大于/小于/等于 + 聚合函数”的组合,直接放HAVING;看到“大于/小于/等于 + 普通字段”,放WHERE。

第三个场景是ORDER BY与GROUP BY同时出现。牛客网会出“统计每个部门的员工数,按人数从多到少排”。标准写法:

SELECT department_id, COUNT(*) AS cnt FROM employees GROUP BY department_id ORDER BY cnt DESC;

这里ORDER BY引用的是别名cnt,MySQL允许这样写,很多其他数据库(比如Oracle)也允许。但要注意,如果ORDER BY引用的是SELECT里没有的字段,结果就不确定了。刷题时我建议ORDER BY后面的字段要么是分组字段,要么是聚合函数或别名,不要引用无关字段,否则在同一部门人数相同时,结果集的顺序可能不稳定。

4. 牛客网判题系统的脾气:知道这些能少交几次“错题税”

刷题多了你会发现,牛客网的SQL判题和本地Navicat跑SQL完全是两回事。本地查询结果错了你能一眼看出来,但牛客网只告诉你“答案错误”或“运行超时”,连具体差在哪一行都不说。这就像是一个不告诉你答案的面试官,你必须自己把所有可能的坑都踩一遍。这一节我总结几个高频踩坑点,都是我自己和同事在实际刷题时撞过的。

第一个坑是结果集的列名完全不重要,但列的顺序极其重要。牛客网的比对逻辑是逐行逐列比对数据内容,它不在乎你给列起什么别名。但列的排列顺序必须和题目要求一致。举个例子,题目要求“查询姓名和薪水”,你写SELECT salary, name,数据内容完全一样,但顺序反了,判题直接报错。我在指导新人刷题时总强调:做题前先在草稿纸上写下“输出列的顺序”,写完SQL再核对一遍。

第二个坑是字符串的大小写和空格。MySQL默认的排序规则是utf8mb4_general_ci,它是大小写不敏感的,所以WHERE name = 'zhang'能查到Zhang。但牛客网比对结果集时,如果数据库的collation设置不同,排序规则可能不一样。我的建议是:写WHERE条件时严格按题目给的字符大小写写,不要依赖数据库的默认不敏感特性。另外,如果字段值是'zhang '带尾部空格,用= 'zhang'匹配不上,但用LIKE 'zhang%'能匹配上——题目如果没有明确说明,尽量避免去猜有没有空格,直接看表里的数据样例更靠谱。

第三个坑是LIMIT的边界问题。前面说过了,LIMIT 2, 3是跳过2条取3条。但如果你把offset和count写反成LIMIT 3, 2,结果就是从第4条开始取2条,完全不同的数据。这个错误在本地跑的时候肉眼很难发现,因为结果集看起来是“有数的”,只是内容不对。我刷这套题时每次写完LIMIT都会默念一句“第一个数字是跳过的行数”,确认无误再提交。

第四个坑是子查询必须起别名。MySQL要求FROM子句里的派生表必须有别名,不然直接报错。比如:

SELECT * FROM (SELECT id FROM employees) AS e;

漏掉AS e,在SQL Server里可能还能跑,但在MySQL和牛客网判题环境里基本是语法错误。这是我见过新人刷题时出现频率最高的一类低级错误,不是不会写,是写完没检查。

第五个坑是分号问题。牛客网的SQL编辑器里,每条语句结尾的分号可写可不写,但如果一条语句里有多段SQL,分号位置错了判题系统会判语法错误。我的习惯是每条语句只在最后加一个分号,中间不加,保持逻辑清晰。

5. 刷完20题之后:这几道延伸题值得再练一遍

牛客网刷题1-20只是热身,真正让你在面试里比别人多一分优势的,是刷完之后主动做的延伸思考。我在带团队做技术分享时,经常把这几道题作为“二刷清单”推荐给新人。

延伸题一是“计算累计值”。比如“按月份统计每个月的入职人数,并计算截至当月的累计入职人数”。MySQL 8.0可以用窗口函数:

SELECT DATE_FORMAT(hire_date, '%Y-%m') AS month, COUNT(*) AS monthly_cnt, SUM(COUNT(*)) OVER(ORDER BY DATE_FORMAT(hire_date, '%Y-%m')) AS cum_cnt FROM employees GROUP BY DATE_FORMAT(hire_date, '%Y-%m') ORDER BY month;

这个写法的亮点是SUM(COUNT(*)) OVER(ORDER BY ...),窗口函数里套聚合函数,这个模式面试官看了会眼前一亮。牛客网1-20题里没有这种组合,但刷完1-20再练这个,你对窗口函数的理解会上一个台阶。

延伸题二是“行转列”。比如一张表存着每个员工在不同年份的绩效,要求把年份转成列。典型写法是用条件聚合:

SELECT employee_id, MAX(CASE WHEN year = 2021 THEN score END) AS score_2021, MAX(CASE WHEN year = 2022 THEN score END) AS score_2022, MAX(CASE WHEN year = 2023 THEN score END) AS score_2023 FROM performance GROUP BY employee_id;

这个写法在报表场景里非常实用,牛客网1-20虽然没有直接考,但它考察的GROUP BY和聚合函数是它的基础。

延伸题三是“连续出现的值”。比如“查出连续3个月都有入职记录的部门”。这类题的本质是“相邻行比较”,MySQL 8.0的LAG和LEAD窗口函数可以优雅解决:

SELECT department_id FROM ( SELECT department_id, hire_month, LAG(hire_month, 1) OVER(PARTITION BY department_id ORDER BY hire_month) AS prev_month, LAG(hire_month, 2) OVER(PARTITION BY department_id ORDER BY hire_month) AS prev2_month FROM (SELECT department_id, DATE_FORMAT(hire_date, '%Y-%m') AS hire_month FROM employees) t ) t2 WHERE hire_month = DATE_ADD(prev_month, INTERVAL 1 MONTH) AND prev_month = DATE_ADD(prev2_month, INTERVAL 1 MONTH);

刷到这里,你再回头看1-20题,会发现很多题都能用窗口函数写出更统一、更优雅的解法。这个从“能跑通”到“写得漂亮”的提升,才是刷这套题最重要的收获。

6. 关于刷题节奏和面试衔接,说点我的个人体会

最后聊聊刷题之外的事。牛客网1-20这套题,我见过两种极端刷法:一种是一天刷完20题,每道题跑通就下一题;另一种是一道题卡半天非要写出最优解。这两种我都试过,效果都不好。前一种刷完三天就忘,后一种挫败感太强,刷到第5题就放弃了。

我自己的节奏是:第一天只刷5题,每道题提交后点开“题解”看其他人的写法,重点看有没有比自己更简洁的写法。第二天再刷5题,但把前一天做错的题重做一遍。第三天刷完全部20题,然后集中梳理一遍错题。这样三轮下来,记忆保留率比一次性刷完高很多,而且你会自然形成“各知识点在题目里怎么变形”的意识。

面试衔接上我有一个很实在的建议:面试官让你手写SQL时,先写能跑的,再补优化说明。牛客网判题只管结果对不对,但面试官会追问“你这个写法在大数据量下效率如何”。我见过太多候选人在白板上憋了半天想写窗口函数,结果语法没写对。还不如先写最直白的子查询版本,然后主动说“数据量大时我会改成JOIN派生表或窗口函数”,这样反而显得你思维严谨。

我从这套题里收获最大的不是“会写20道题”,而是建立了一个排查SQL问题的固定思路:先看输出列和顺序,再看过滤条件里的NULL风险,再看分组逻辑是否合理,最后才看性能。这个思路后来帮我排查了好几次生产环境的数据异常,每次我都想起牛客网那些“明明本地跑得好好的,一提交就错”的瞬间——不是题目刁钻,是我对SQL执行逻辑的把握还不够细。希望你刷完这套题,也能有同样的感觉。

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

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

立即咨询