
最近帮团队筛简历候选人十个里九个说自己“SQL熟练”结果一到牛客在线编程或者现场手写能一次跑对的不超过三成。我自己当年刷牛客SQL题库也踩过不少坑从最简单的select开始到被窗口函数、慢SQL优化、动态SQL这些八股反复拷打才慢慢摸清面试官真正想听什么。这篇就把我梳理过的高频考点、现场答题节奏、还有那些“知道但说不清”的细节一次性讲透面向正在准备数据分析、后端开发、运维开发岗面试的朋友也适合需要给团队做SQL内训的同学参考。1. 牛客刷题热度背后SQL面经的高频考点变化牛客社区的SQL面经热度一向很高这不是偶然。我观察了很久评论区发现大家刷“sql面试题”并不是因为题目简单反而是因为“眼睛会了手上一写就废”的题目越来越多。早些年问的最多是“会不会写基本的select、join”现在面试官早就把考察重心移到真实业务场景窗口函数、连续登录、留存率、去重保留最新一条、索引失效这类题成了标配。1.1 为什么现在SQL面经这么卷岗位边界在扩张。数据分析、后端开发、测试开发、数据开发甚至产品经理岗都要考SQL这是第一个原因。第二个原因是SQL是少数几个“逻辑能力直接可见”的手写环节面试官从一段SQL能看出候选人有没有处理过脏数据、懂不懂执行顺序、知不知道count和sum的边界。第三个原因更实际很多项目的线上慢接口、报错日志最终都指向一条低效SQL团队招人当然要筛掉只会背概念的人。有人觉得“SQL必知必会”这本书翻完就够了但牛客面经里的题已经远远超出入门书范围。你不仅要会写还要会解释为什么这样写还要能扛住面试官连环追问。所以别再只刷简单题了下面的考点分布就是我从牛客评论区留言里整理出来的方向。1.2 高频考点分布从牛客评论区提炼我整理了一张高频考点表覆盖了我看过的大多数SQL面经题目大家按这个方向准备基本不会跑偏。考点分类出现频率典型关键词基础查询与连接高JOIN、LEFT JOIN、ON条件、笛卡尔积聚合与分组高GROUP BY、HAVING、COUNT、SUM、AVG窗口函数很高ROW_NUMBER、RANK、DENSE_RANK、LAG、LEAD日期与字符串函数中高DATE_FORMAT、DATE_SUB、TRIM、SUBSTRING去重与空值处理高DISTINCT、GROUP BY、IS NULL、去空子查询与CTE中高WITH AS、嵌套SELECT、EXISTS索引与慢SQL中EXPLAIN、索引失效、覆盖索引动态SQL与安全中MyBatis、#{ }、${ }、SQL注入牛客常见的笔试题型是选择题加编程题编程题一般要求在线上环境直接跑通。选择题喜欢考执行顺序、NULL比较、聚合函数忽略空值、事务隔离级别。编程题则更贴近业务比如“查找第N高的薪水”“统计各科成绩前三名”“连续登录天数”。这些题刷多了以后你会发现核心方法就那么几种但细节是真正的分水岭。2. 窗口函数与分组聚合面试官最爱的“伪简单”题窗口函数是牛客面经里绕不开的大头。很多候选人能背出row_number、rank、dense_rank的区别但一结合分组和排序就乱套。这里我逐一拆开讲清楚。2.1 group by 的一个经典误区先问个问题下面这串SQL能不能跑通SELECT dept_id, emp_name, MAX(salary) FROM emp GROUP BY dept_id;在不同数据库里结果不一样。MySQL 5.7之前如果没开ONLY_FULL_GROUP_BY这条语句能执行但返回的emp_name是分组内随机一条不是最大工资对应的那个人。SQL Server和Oracle这类数据库会直接报错因为emp_name既不在GROUP BY里也没有被聚合函数包裹语义不合法。面试官追问“为什么MySQL能跑、SQL Server报错”时很多人答不上来。核心原因是分组操作把多行压成一行没有聚合的非分组列没有唯一值数据库物化方式不同。MySQL老版本为了兼容性默认放行新版默认开启ONLY_FULL_GROUP_BY后也会报错。这个坑在分组统计、报表开发里特别常见建议从一开始就养成习惯SELECT里出现的非聚合列必须全部放进GROUP BY或者用ANY_VALUE这类函数明确意图。2.2 row_number、rank、dense_rank排名三兄弟的边界条件排名三兄弟是窗口函数里最高频的考点。很多面试题会直接丢一张学生成绩表让你“查询每门课成绩前三名”。不写窗口函数的话传统写法就是用自连接和子查询绕逻辑复杂还有边界漏洞。窗口函数一行搞定SELECT student_id, course_id, score, ROW_NUMBER() OVER (PARTITION BY course_id ORDER BY score DESC) AS row_num, RANK() OVER (PARTITION BY course_id ORDER BY score DESC) AS rk, DENSE_RANK() OVER (PARTITION BY course_id ORDER BY score DESC) AS drk FROM score;三兄弟的区别是面试必问函数是否处理并列排名是否跳号适用场景ROW_NUMBER不处理强制唯一序号不跳号取前N条精确记录RANK处理并列会跳号如1、1、3保留并列且允许名次空缺DENSE_RANK处理并列不跳号如1、1、2保留并列且名次连续比如“每科前三名如果第三名并列这两个人都要”用DENSE_RANK再过滤drk 3因为并列第三不会挤掉后续排名。如果面试官说“我只要三条记录并列也砍掉”那就用ROW_NUMBER。这里已经不再是函数名背没背熟的问题而是能不能根据业务语义选对函数的问题。2.3 窗口框架sum() over() 的边界最容易写错窗口函数里最容易被忽略的是框架范围。很多人以为SUM(amount) OVER (ORDER BY pay_time)是整个表求和实际不是。当ORDER BY存在时默认窗口范围是从分区起点到当前行这叫累计值常用于计算截至每天的销售额。比如订单表SELECT pay_date, SUM(amount) AS day_amount, SUM(amount) OVER (ORDER BY pay_date) AS cumulative_amount FROM orders GROUP BY pay_date;注意这里的SUM(amount) OVER (ORDER BY pay_date)是在GROUP BY之后计算也就是对每天汇总后再做累计顺序不能搞混。如果需要移动平均就要显式指定ROWS BETWEENSELECT pay_date, AVG(day_amount) OVER ( ORDER BY pay_date ROWS BETWEEN 2 PRECEDING AND CURRENT ROW ) AS avg_3day FROM daily_summary;不写ROWS BETWEEN的话默认也是从分区起点到当前行移动窗口就失效了。这个问题在牛客评论区里问过很多次面试时很容易被当成细节扣分项。3. 索引、慢SQL与执行计划八股里最值钱的部分如果说窗口函数是语法题那索引和慢SQL就是工程题。你可以在牛客刷题刷出排名但真正让面试官眼睛亮起来的是你把一条慢SQL从2秒优化到50毫秒的完整过程。热搜词里“慢sql优化”“并行sql优化”“druid sql injection violation”“jvm或者spring boot会设置sql执行10秒自动关闭吗”都是大家实际工作中碰到的场景。3.1 索引失效的六种常见场景别只背口诀面试官问“索引为什么失效”如果只回答“最左前缀、like、函数”大概率会被追问“为什么这些情况会失效”。索引失效的本质是破坏了B树的有序性或者让优化器认为全表扫描更划算。我列一下高频场景联合索引不满足最左前缀。比如索引(a, b, c)条件只写b或者只写b、c无法使用索引。隐式类型转换。WHERE phone 13800001111phone是varchar类型数据库会先把phone转成数值再比较等于对索引列用了函数。LIKE以通配符开头。LIKE %abc无法利用索引的有序性LIKE abc%可以。对索引列使用函数。WHERE DATE(create_time) 2025-06-01应该改成create_time 2025-06-01 AND create_time 2025-06-02。OR连接时其中一个条件没有索引。NOT IN、! 在某些情况下优化器会放弃索引但这不是绝对的。面试时如果能补一句“优化器会结合数据分布判断索引失效不是绝对的要用EXPLAIN验证”会显得更有实战经验。3.2 从执行计划到慢SQL定位一个完整的排查链路定位慢SQL不能靠猜。MySQL里先开启慢查询日志SET GLOBAL slow_query_log ON; SET GLOBAL long_query_time 1;然后对目标SQL执行EXPLAIN看几个关键字段字段关注点type从好到差system const eq_ref ref range index ALLrows预估扫描行数越小越好ExtraUsing filesort、Using temporary 通常意味着需要额外排序或临时表举个实际例子分页深翻页是慢SQL重灾区-- 慢limit偏移量越大扫描的无效行越多 SELECT * FROM orders ORDER BY create_time DESC LIMIT 100000, 20; -- 优化延迟关联先在索引上定位主键再回表取数 SELECT o.* FROM orders o JOIN ( SELECT id FROM orders ORDER BY create_time DESC LIMIT 100000, 20 ) t ON o.id t.id ORDER BY o.create_time DESC;如果你用的SQL Server可以用SET STATISTICS IO ON; SET STATISTICS TIME ON;看逻辑读和CPU时间原理类似。在国产数据库比如达梦的现场排查里如果进程崩溃产生了core文件通过bt命令查看栈帧往往能定位到正在执行的SQL语句这是我遇到过的真实场景面试时讲出来会很有画面感。3.3 “SQL执行超过10秒会自动关闭”这个问题怎么答牛客热搜词里有条“jvm或者spring boot会设置sql执行10秒自动关闭吗”一看就是被线上问题折磨过的人问的。答案是默认不会。Spring Boot本身不设置JDBC查询超时除非你在连接池或数据库驱动层显式配置。比如Druid连接池可以配置queryTimeoutMySQL JDBC驱动有socketTimeout这些参数都不会因为“JVM觉得SQL太慢”而自动生效。如果生产环境确实在10秒左右报超时通常是某层配置了查询超时比如DruidDataSource dataSource new DruidDataSource(); dataSource.setQueryTimeout(10); // 单位秒0表示不限制或者MyBatis的defaultStatementTimeout设成了10。回答这类问题从连接池、驱动、数据库三层逐层排查比直接背结论更有说服力。顺便说一句Druid的WallFilter拦截到非法SQL时报错信息是“sql injection violation”那位搜索“druid sql injection violation, dbtype oracle”的同学大概率是被防火墙规则拦了不是被超时掐断的这两件事经常被人混在一起。4. 去重、NULL与数据清洗细节题才是真正的分水岭热搜词里“sql语句去重查询”“sql去除空值”“清洗---sql语句去重”常年靠前。去重这个知识点看起来简单但牛客面经里凡是涉及去重的题面试官都喜欢一层一层追问追问到NULL、DISTINCT语义和保留最新记录很多人就开始含糊了。4.1 distinct 与 group by 的去重差异SELECT DISTINCT user_id, order_id FROM orders这句话是对(user_id, order_id)组合去重。如果只想看有多少个不同的user_id写成SELECT COUNT(DISTINCT user_id)即可。但DISTINCT有个天然限制它只能对查询出来的列做去重不能配合聚合返回“每组最大那条”。比如订单表里同一个订单号出现多条记录需要去重并保留金额最大的一条。用DISTINCT根本做不到得用ROW_NUMBERSELECT * FROM ( SELECT *, ROW_NUMBER() OVER (PARTITION BY order_id ORDER BY amount DESC) AS rn FROM orders ) t WHERE rn 1;GROUP BY的优势在于能陪聚合函数一起用比如GROUP BY user_id HAVING COUNT(*) 5。面试时如果只答“distinct去重group by分组”说明还没理解两者在分析场景下的分工。补充一句优化器在执行层经常会把DISTINCT改写为GROUP BY所以不要纠结性能上的绝对差异要看业务表达是不是清晰。4.2 NULL值处理五个一碰就错的点NULL不是0也不是空字符串它是一种“未知”状态。牛客选择题里特别喜欢考NULL我总结了五个高频考点场景结果说明COUNT(column)忽略NULL计数时只统计非NULL值COUNT(*)不忽略NULL统计行数NULL 1 或 NULL NULL结果是UNKNOWN所以不能用 NULL 判断NULL 1NULL任何算术运算结果都是NULL排序不同数据库不一样MySQL默认NULL最小SQL Server默认NULL最大实际业务里sql去除空值不能只写WHERE col IS NOT NULL还要考虑空字符串和空格。正确姿势是SELECT name FROM user WHERE name IS NOT NULL AND TRIM(name) ;面试里如果能把COUNT(*)和COUNT(column)的差异讲清楚再补一句“所以统计活跃用户数时COUNT(user_id)可能比COUNT(*)少因为它会把user_id为NULL的行漏掉”面试官基本就知道你踩过这个坑。4.3 数据清洗类SQL实战去重取最新、补空值数据清洗是数据分析岗面试最爱考的。牛客上有道经典题“删除重复的电子邮箱”很多人知道用DELETE配合自连接但一到现场就会忽略“先验证再删除”。我的习惯是先查一遍影响行数-- 查询所有需要删除的id SELECT id FROM email_table a WHERE EXISTS ( SELECT 1 FROM email_table b WHERE a.email b.email AND a.id b.id );确认无误后再执行删除。如果数据量很大优先用窗口函数方案比如保留每个用户最新一条登录记录WITH ranked AS ( SELECT id, user_id, ROW_NUMBER() OVER(PARTITION BY user_id ORDER BY login_time DESC) AS rn FROM login_log ) DELETE FROM login_log WHERE id IN (SELECT id FROM ranked WHERE rn 1);在MySQL里同表子查询DELETE经常报“You cant specify target table for update in FROM clause”解决办法是先包一层临时表。这种报错在牛客评论里被反复提过说明很多人真到了生产环境还是会踩。另一个清洗点是对账场景金额比较不能直接用等号浮点误差会导致明明相等的金额被判不等应该用ABS(a.amount - b.amount) 0.01。5. SQL注入与动态SQL一条报错引发的深度追问SQL注入是后端面试必问但很多候选人只会背“不要拼接SQL、要用预编译”。真正有经验的回答能从一条报错开始讲清楚攻击原理、防御层次和框架层的坑。5.1 一次牛客面经中的报错复盘druid的拦截逻辑网上搜“sql injection violation, dbtype oracle, druid”的人很多这个报错是阿里Druid连接池的WallFilter触发的。它会对SQL语句做黑名单和语法分析一旦发现OR 11、UNION SELECT、注释符、分号堆叠等特征直接抛出异常。这个报错不一定代表有人攻击也可能是业务代码误触发了规则。排查链路是这样的抓住完整SQL和参数看是否有用户输入被拼接到SQL中。检查Druid的WallFilter配置确认命中了哪条规则。如果是误伤可以精细化调整防火墙配置但不要全局关闭。根本修复是改代码用参数化查询而不是绕过滤规则。面试官问这个报错实际是想看你对“纵深防御”有没有概念。数据库防火墙是最后一层兜底应用层预编译才是第一道防线。两层都有才算合格。5.2 从万能密码绕过聊注入本质“sql注入万能密码绕过”是经常被搜的词。经典payload长这样 OR 11。它为什么能绕过登录因为拼接后的SQL变成SELECT * FROM user WHERE username OR 11 AND password anything;只要OR 11把整体条件变成恒真登录校验就被绕过了。注意这类操作只允许在你自己搭的靶场或测试环境里复现生产环境、他人系统上做任何尝试都属于违规行为。注入的本质不是“某个特殊字符”而是数据和代码的边界被打破用户输入被当成SQL结构执行了。所以防御的核心是“参数与结构分离”。常见手段使用预编译语句也就是PreparedStatement让输入永远只当数据。使用ORM框架的参数绑定比如MyBatis的#{}、JPA的位置参数。动态表名、排序字段不能直接拼接必须走白名单校验。数据库连接账号按最小权限分配应用账号不应该有DROP、TRUNCATE权限。能按这个层次回答面试官通常不会再深挖攻击细节他会知道你心里有安全边界。5.3 MyBatis动态SQL的#{}与${}边界条件是关键牛客热搜词里“mybatis动态sql”出现频率很高。MyBatis的#{}会生成?占位符走预编译${}是字符串拼接直接替换到SQL里。所以绝大多数参数都应该用#{}。select idselectByCondition resultTypeUser SELECT * FROM user where if testname ! null AND name #{name} /if if testage ! null AND age gt; #{age} /if /where /select${}什么时候不得不用一是动态表名比如历史分表user_202501二是排序字段ORDER BY ${sortColumn}。这两种场景都要对输入做严格白名单校验不能直接信任前端传参。比如前端传sortColumncreate_time就映射到白名单里的create_time列名而不是原样拼接。动态SQL标签里还有一个高频函数是foreach用于批量in查询foreach collectionids itemid open( separator, close) #{id} /foreach这里同样只能用#{}。有人问“flowable能写自定义sql吗”实际项目中工作流引擎的复杂统计报表经常需要扩展自定义Mapper查询走的就是同一套MyBatis动态SQL逻辑只要记住“能参数绑定就别拼接”流程表再复杂也不容易写出注入漏洞。6. 牛客在线编程实战答题节奏与提交易错点刷牛客SQL题库和真正现场笔试还是有点区别主要在于时间感和自测习惯。我见过太多人平时写得不错一上笔试页面就慌最后提交答案发现少了GROUP BY字段或者JOIN条件写错。6.1 手写SQL的时间分配先读表再动手拿到一道SQL编程题不要急着写SELECT。我的节奏是先看表结构和字段含义圈出主键、外键、日期字段。读题时把考点标出来去重、排序、连接、分组、保留前N条。写SQL前先在草稿上说出执行顺序FROM、ON、JOIN、WHERE、GROUP BY、HAVING、SELECT、ORDER BY、LIMIT。写完先自查三件事JOIN会不会造成重复行COUNT要不要加DISTINCTNULL会不会把条件漏掉最后再点运行。举个例子查询各部门员工数SELECT d.department_id, d.name, COUNT(DISTINCT e.emp_id) AS emp_cnt FROM department d LEFT JOIN employee e ON d.department_id e.department_id GROUP BY d.department_id, d.name ORDER BY emp_cnt DESC;这段SQL里GROUP BY必须写两个列因为SELECT里出现了d.name。如果只GROUP BY d.department_id很多数据库会报错这就是细节分。6.2 本地练习环境怎么搭才不劝退牛客在线环境已经很好用但本地保留一套环境能帮你练EXPLAIN、看慢日志、调索引。我最推荐的组合是Docker装MySQL 8.0再配一个DBeaver社区版导入sql文件全程半小时搞定。很多人一搜“sql server 2008 r2下载”“sql server 2022安装教程”光安装就卡半天然后热情耗尽。如果你不是专门做SQL Server维护没必要在老旧版本上浪费时间。真要学选2019或2022 Developer版即可安装时实例名、Windows认证这些默认选项就是最省事的。还有人到处找“pl sql 64 15注册码”Oracle的PL/SQL Developer其实有官方试用版个人学习直接用免激活的替代工具或者IDE插件就行不建议碰破解类资源。本地环境最大的好处是可以随意建表、灌数据然后看执行计划。养成用EXPLAIN验证索引是否生效的习惯比单纯刷题有用得多。6.3 面经里“脑筋急转弯”类SQL的通用思路牛客面经里最让人头疼的是连续登录、留存率、行列转换这类题。先说连续登录核心套路是“用窗口函数生成分组标记”WITH t AS ( SELECT user_id, login_date, ROW_NUMBER() OVER(PARTITION BY user_id ORDER BY login_date) AS rn FROM login_log ) SELECT user_id, MIN(login_date) AS start_date, MAX(login_date) AS end_date, COUNT(*) AS continuous_days FROM ( SELECT user_id, login_date, DATE_SUB(login_date, INTERVAL rn DAY) AS grp FROM t ) x GROUP BY user_id, grp HAVING COUNT(*) 3;原理很巧妙如果登录日期连续那么login_date - rn是常量所有连续行会被分到同一个组。SQL Server用户要把DATE_SUB换成DATEADD(day, -rn, login_date)。留存率又是另一类高频题思路是“按注册日分组再用条件聚合判断是否活跃”SELECT reg_date, COUNT(DISTINCT user_id) AS new_users, COUNT(DISTINCT CASE WHEN DATEDIFF(active_date, reg_date) 1 THEN user_id END) AS retain_1day FROM user_info GROUP BY reg_date;这类题一旦见过一次面试现场就只是在复用套路。我自己的体会是SQL没有太多“创造”更多是“识别模式”。窗口函数解决排名和分组内编号自连接解决行与行比较条件聚合解决留存和转化日期函数解决连续性问题。把这几种模式记熟牛客上的“脑筋急转弯”基本都能拆解。我自己带新人时有一个习惯写完SQL先不要跑眼睛执行一遍从FROM开始到WHERE、GROUP BY、HAVING、SELECT、ORDER BY、LIMIT把这个执行顺序刻在脑子里。把这一步养成肌肉记忆笔试的时候会少踩一大半坑。