ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

牛客网MySQL刷题1-20:SQL笔试高频考点与避坑指南

牛客网MySQL刷题1-20:SQL笔试高频考点与避坑指南 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 BYSELECT 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.0SELECT 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);这里有一个巨大的坑如果子查询结果中包含NULLNOT 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 EXISTSSELECT name FROM employees e WHERE NOT EXISTS ( SELECT 1 FROM departments d WHERE d.id e.department_id );这个坑我在真实项目里踩过不止一次所以强烈建议刷题阶段就养成“看到NOT IN就条件反射检查NULL”的习惯。经典考点三内连接与左连接。比如“查询每个员工及其所在部门名称没有部门的也要显示出来”这必须用LEFT JOINSELECT 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引用的是别名cntMySQL允许这样写很多其他数据库比如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执行逻辑的把握还不够细。希望你刷完这套题也能有同样的感觉。
返回列表