不管是校招还是社招,只要岗位和数据沾边,SQL面试题基本是躲不掉的关卡。我这些年面试别人、也被别人面过,最大的感受是:SQL题看起来千变万化,实际上考点非常集中。很多人刷题爱往偏题怪题上使劲,结果真正上场时,被一道窗口函数或自连接题问得哑口无言。这篇文章不打算罗列几百道题目,而是把SQL常考面试题背后的核心考点拆开揉碎,讲清楚每类题为什么考、怎么答、有哪些坑,最后附上我自己的刷题心得。
1. 内容整体设计与思路拆解
1.1 面试官到底在考什么
先想明白一件事:面试官出SQL题,不是为了让你证明“你会写SQL”,而是通过题目观察你的数据思维、逻辑拆解能力和工程素养。一个候选人能写出正确结果,只能拿基础分;能讲清楚执行过程、分析性能差异、说明边界情况,才是真正的加分项。
我把SQL常考面试题粗分为四类:
- 语法基础类:考察SELECT、JOIN、GROUP BY、子查询的熟练度,往往混合条件过滤和排序。
- 逻辑推理类:给定一张表,让你用SQL解决某个具体业务问题,比如连续登录天数、分组TopN、环比同比,核心是考察你能否把业务语言翻译成集合语言。
- 性能优化类:给定一条慢SQL,让你分析原因并给出优化方案,或者直接问索引失效场景、执行计划怎么看。
- 理论概念类:事务隔离级别、三大范式、存储引擎区别,属于背多分内容,但很多候选人栽在“能背定义、不会应用”上。
从出题频率看,前三类是绝对主力。有经验的面试官很少问“SELECT和WHERE的执行顺序”这种纯背诵题,更爱用一道带有业务场景的SQL题,逼你现场写、现场讲。
1.2 为什么这些题目高频出现
高频题之所以高频,是因为它们恰好踩中了实际开发中最容易出错、也最能体现功力的点。比如“连续N天登录”这类题,业务上对应留存分析、活跃用户统计,技术上涉及窗口函数、日期处理、自连接多种解法,一道题能同时试探你多个维度的能力。
再比如“分组取TopN”,几乎所有报表需求里都会出现“每个部门工资最高的员工”“每类商品销量前三”这类诉求。它考的不只是ROW_NUMBER()语法,还包括并列排名怎么处理、取前三还是前三名、分区键选什么,这些细节恰恰是实战中决定结果正确与否的关键。
换句话说,面试题不是故意刁难人,它们更像是把真实业务场景抽象成的最小复现case。理解这一层,复习方向就不会跑偏。
1.3 我的复习策略建议
如果你还有一到两周准备时间,我建议按这个顺序来:先过一遍窗口函数,尤其ROW_NUMBER、RANK、DENSE_RANK、SUM OVER、LAG/LEAD,这是最高频的考点;再练JOIN和子查询的等价改写,重点是IN/EXISTS、JOIN/INNER JOIN、LEFT JOIN的语义差异;然后是聚合类题目,把GROUP BY的隐式列问题和HAVING过滤条件搞透;最后花半天看索引与执行计划的理论,能把EXPLAIN结果讲明白的人,在面试官眼里已经超过一半候选人。
别贪多。每类题挑三五个经典案例,写一遍、讲一遍、总结一遍,比泛泛刷两百道题有效得多。
2. 窗口函数:面试题里的提分大杀器
2.1 窗口函数为什么这么受欢迎
窗口函数在SQL面试题里的地位,这几年越来越高。原因很简单:它能把以前要写复杂子查询或自连接的逻辑,用一行OVER()解决,可读性和维护性都好很多。面试官喜欢问窗口函数,因为它能快速区分“只会基础SQL的人”和“有真实数据分析经验的人”。
窗口函数的本质:对每一行数据,在它所在的分区内计算一个聚合值或排名值,但不会像GROUP BY那样把多行合并成一行。理解这个“不折叠行”的特性,是做对题目的基础。
举一个生活化类比:GROUP BY像班级按小组排出平均分,每组只剩一行;窗口函数则是给每个同学写上“所在小组的平均分”,行数一条不少。业务报表里很多时候需要的是后者——既要明细,又要汇总值。
2.2 高频题型一:分组TopN
题目原型:给定员工表,字段包括部门ID、员工姓名、薪资。求每个部门薪资最高的前三名员工。
通用写法:
SELECT 部门ID, 姓名, 薪资 FROM ( SELECT 部门ID, 姓名, 薪资, DENSE_RANK() OVER (PARTITION BY 部门ID ORDER BY 薪资 DESC) AS rk FROM 员工表 ) t WHERE rk <= 3这里我故意用DENSE_RANK而不是ROW_NUMBER或RANK,是有讲究的。如果题目问“薪资最高的前三名员工”,意味着有人薪资并列时,并列的人应该同时出现在结果里。ROW_NUMBER遇到并列会随机或按物理顺序排名次,RANK遇到并列会跳号,比如两个人并列第一,第二名的RANK就是3。DENSE_RANK则不会跳号,第一并列后仍继续排第二第三,最贴合“前三名员工”这个语义。
但如果面试题说“薪资排名前三的员工”,用RANK也是合理的。这就是细节,面试官非常看重你能不能说清楚不同排名函数的区别。
补充一个实战经验:子查询的别名t和列名rk是固定的套路,很多新人在这一步漏写别名导致语法错误。另外,WHERE rk <= 3不能直接写成WHERE DENSE_RANK() OVER ... <= 3,因为SQL的执行顺序里WHERE在窗口函数之前,窗口函数只能在SELECT或ORDER BY里使用,所以必须套一层子查询。这个点我在面试里问过很多人,能答上来的是真懂执行顺序的。
2.3 高频题型二:连续登录天数
题目原型:用户登录表,字段包括用户ID、登录日期。求每个用户连续登录的最大天数。
这类题有很多变体,但核心解法是同一个:用日期减去行号,得到连续的组标识。
WITH login_ranked AS ( SELECT 用户ID, 登录日期, DATE_SUB(登录日期, INTERVAL ROW_NUMBER() OVER (PARTITION BY 用户ID ORDER BY 登录日期) DAY) AS grp FROM 用户登录表 ) SELECT 用户ID, DATEDIFF(MAX(登录日期), MIN(登录日期)) + 1 AS 连续天数 FROM login_ranked GROUP BY 用户ID, grp原理是这样的:同一用户按日期排序后,连续日期的行号差是一个定值。比如某用户登录了1月1日、1月2日、1月3日,行号分别为1、2、3,日期减行号后得到12月31日、12月31日、12月31日,三者相同,说明是同一段连续区间。一旦中间断档,日期减行号的差值就会改变。
这套“日期减行号”的技巧,实际工作中做留存分析、活跃分析非常常用,建议写进自己的SQL工具库。
再说一个细节:去重。原始登录表里同一个用户一天可能有多条记录,如果不提前去重,ROW_NUMBER会把同一天的重复记录排成不同行号,导致连续天数被算错。面试时如果先把这个情况说清楚,再写出带去重的SQL,印象分会明显提升。
2.4 高频题型三:同比环比与移动平均
数据分析岗特别喜欢考这类题。比如:给定销售表,字段包括月份和销售额,求每个月相对于上个月的增长率。
SELECT 月份, 销售额, 销售额 / LAG(销售额) OVER (ORDER BY 月份) - 1 AS 环比增长率 FROM 销售表LAG和LEAD用来取上一行或下一行的值,同比可以再用LAG(..., 12)取去年同月。移动平均则用AVG OVER配合ROWS BETWEEN CURRENT ROW AND 2 FOLLOWING这类窗口边界。
这里最大的坑是排序键的唯一性。如果ORDER BY的列有重复值,窗口的行序是未定义的,结果可能不稳定。实际业务里要确保排序列能唯一定义行序,比如加一个日期+流水号。
2.5 窗口函数面试话术建议
写出答案只是第一步,面试时建议主动讲清楚:为什么用窗口函数,和GROUP BY的区别是什么,有没有替代方案。比如TopN题还可以用JOIN + COUNT实现:
SELECT e1.部门ID, e1.姓名, e1.薪资 FROM 员工表 e1 LEFT JOIN 员工表 e2 ON e1.部门ID = e2.部门ID AND e2.薪资 > e1.薪资 GROUP BY e1.部门ID, e1.姓名, e1.薪资 HAVING COUNT(DISTINCT e2.员工ID) < 3这种写法能跑,但逻辑绕、性能也相对差。主动对比两种写法,展示你对方案取舍的理解,比闷头敲代码更像有经验的从业者。
3. JOIN与子查询:关联题的思路与坑
3.1 一道题摸清你的JOIN功底
题目:有两张表,一张是订单表,一张是用户表。查出所有下过订单的用户信息,以及从未下过订单的用户信息,两类结果怎么分别写?
下过订单的用户,最简单的写法是:
SELECT DISTINCT u.* FROM 用户表 u INNER JOIN 订单表 o ON u.用户ID = o.用户ID也可以用EXISTS:
SELECT u.* FROM 用户表 u WHERE EXISTS (SELECT 1 FROM 订单表 o WHERE o.用户ID = u.用户ID)这两者语义等价,但面试官会追问性能差异。小表驱动大表时,EXISTS的直觉是“外层每行判断一次”,INNER JOIN则可能产生临时大结果集。现代优化器很多时候会自动改写,结果不一定有差距,但你能主动说出这个分析角度,就说明不是只会套模板。
从未下过订单的用户,用NOT EXISTS或LEFT JOIN + IS NULL:
SELECT u.* FROM 用户表 u LEFT JOIN 订单表 o ON u.用户ID = o.用户ID WHERE o.订单ID IS NULLLEFT JOIN后IS NULL的写法,很多人在字段选择上出问题:判断NULL的字段应该来自右表主键,而不是任意字段。如果右表某个业务字段恰好为NULL,这个写法就会把有订单的用户也误判成无订单。正确做法是选右表的主键或非空字段来判断。
3.2 JOIN的过滤条件放WHERE还是ON
这是SQL面试题里一个经典陷阱。看这个例子:
SELECT u.*, o.金额 FROM 用户表 u LEFT JOIN 订单表 o ON u.用户ID = o.用户ID AND o.金额 > 100和:
SELECT u.*, o.金额 FROM 用户表 u LEFT JOIN 订单表 o ON u.用户ID = o.用户ID WHERE o.金额 > 100第一句会返回所有用户,没匹配到订单的用户金额为NULL;第二句只返回有金额大于100订单的用户,没匹配的用户直接被WHERE过滤掉了。LEFT JOIN的语义是“保留左表所有行”,但WHERE会把结果集重新过滤,相当于把LEFT JOIN降级成INNER JOIN。这个点非常基础,但实际翻车率极高,建议面试时主动说清楚ON和WHERE的过滤时机差异。
3.3 自连接:同一张表里找关系
题目原型:给定一张员工表,字段包括员工ID、姓名、经理ID,求每个员工对应的经理姓名。
SELECT e.姓名 AS 员工姓名, m.姓名 AS 经理姓名 FROM 员工表 e LEFT JOIN 员工表 m ON e.经理ID = m.员工ID自连接最容易忽略的边界是中国式复杂关系的处理,实际上核心只是给表起两个不同的别名,把一张表当成两张表用。另一个常见坑是经理ID为NULL的情况。有的员工没有经理,如果题目要求查出所有员工,必须用LEFT JOIN而不是INNER JOIN,否则这类员工会整行消失。
自连接还有一个更隐蔽的问题:数据量放大。自连接本质是笛卡尔积的局部,如果不加连接条件或连接条件不够精确,结果集会急剧膨胀。面试题里偶尔会故意给一张用户表让你找粉丝关系,这时连接条件要想清楚“A关注了B”和“B关注了A”是不是同一关系。
3.4 子查询和CTE怎么选
SQL面试题里,子查询和CTE的取舍也常被问到。简单对比一下:
- 子查询直接嵌在FROM或WHERE里,适合一次性逻辑。
- CTE用WITH定义,可以多次引用,逻辑分层清晰,可读性好。处理嵌套层级很深的题目,比如“先算A,再基于A算B”,用CTE会舒服得多。
- 性能上两者没有绝对优劣,优化器可能把CTE物化也可能内联,关键还是看执行计划。
我个人的习惯是:只要层级超过两层,一律用CTE。面试时CTE的写法也会让面试官更容易跟上你的思路,毕竟他是边听边在脑子里模拟执行结果的。
4. 聚合与分组:GROUP BY前面的隐形地雷
4.1 可能行的隐式列问题
有一道高频送分题:查询每个部门的部门ID和部门内最高薪资。
SELECT 部门ID, MAX(薪资) FROM 员工表 GROUP BY 部门ID这种写法没问题。但换一个问法:“查每个部门的部门名称和部门内最高薪资”,如果部门表和员工表已经关联,很多人会写出这样的SQL:
SELECT 部门名称, MAX(薪资) FROM 员工表 GROUP BY 部门ID这在很多数据库里会直接报错,因为SELECT里的部门名称既不在GROUP BY里,也不在聚合函数里。在MySQL低版本默认配置下这条SQL能跑通,返回的部门名称还是随机值——这是最坑的地方,开发环境能跑,换一个数据库或换版本就出问题。面试时主动指出这个问题,并写出正确写法:
SELECT 部门名称, MAX(薪资) FROM 员工表 GROUP BY 部门ID, 部门名称写出这一步,基本就是这个考点的满分回答。背后原理是SQL的“分组列必须和非聚合查询列保持一致”这一规则。
4.2 WHERE和HAVING的分工
这题几乎每三场面试会出现一次:查出平均薪资大于某值的部门,用WHERE还是HAVING?
SELECT 部门ID, AVG(薪资) AS 平均薪资 FROM 员工表 GROUP BY 部门ID HAVING AVG(薪资) > 8000WHERE在分组前过滤原始行,HAVING在分组后过滤聚合结果。如果你写WHERE AVG(薪资) > 8000,数据库根本不知道AVG是什么,因为WHERE执行时还没做分组。这就是执行顺序的问题:FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY。
有个很容易犯错的实际例子:求“每个部门薪资大于8000的员工平均薪资”,这个条件属于筛选参与聚合的原始行,应该用WHERE:
SELECT 部门ID, AVG(薪资) AS 平均薪资 FROM 员工表 WHERE 薪资 > 8000 GROUP BY 部门ID同样是“薪资大于8000”,放WHERE是筛选员工,放HAVING是筛选部门,语义完全不同。面试时把这两条SQL的差异讲清楚,能展示你对聚合内外过滤的精准理解。
4.3 COUNT的细微差别
COUNT()和COUNT(列名)的区别,属于看起来简单但很多人讲不透的问题。COUNT()数的是结果集的行数,包括NULL和重复行;COUNT(列名)只数该列非NULL的行。业务里统计“有多少人填写了手机号”,用COUNT(手机号);统计“有多少条订单记录”,用COUNT(*)。
COUNT(DISTINCT 列名)也常考:数不重复的非NULL值。比如统计每天活跃用户数,通常写成:
SELECT 登录日期, COUNT(DISTINCT 用户ID) FROM 登录表 GROUP BY 登录日期字符串列用COUNT(DISTINCT)可能因为排序规则导致大小写不敏感而结果偏小,这类细节平时不容易注意,面试里被问到算加分项。
4.4 聚合不一定是GROUP BY的专利
很多人一看到“求最大值”“求和”就条件反射写GROUP BY,其实窗口函数里的SUM OVER、MAX OVER可以做到“既要分组汇总,又保留明细行”。这和第2节的内容呼应。面试题里常见的“求订单表中每个用户订单金额占该用户总金额的比例”,用窗口函数一行搞定:
SELECT 用户ID, 订单金额, 订单金额 / SUM(订单金额) OVER (PARTITION BY 用户ID) AS 占比 FROM 订单表这类题的考察意图不是难为你,而是看你在聚合和信息保留之间有没有灵活的取舍意识。
5. 索引与执行计划:理论题不能只会背定义
5.1 索引失效场景
理论题里,索引失效是被问烂了又不断有人踩坑的话题。最常见的几个场景:
- 对索引列使用函数,比如WHERE DATE(create_time) = '2024-01-01',会让该列的索引失效,正确写法是范围查询WHERE create_time >= '2024-01-01' AND create_time < '2024-01-02'。
- 隐式类型转换,比如索引列是字符串类型,用WHERE user_id = 123去查,数据库会先把列转成数字,索引就废了。正确写法是显式传字符串WHERE user_id = '123'。
- 前模糊匹配,WHERE name LIKE '%张三%',因为通配符在开头,B+树无法从中间开始定位,索引失效。如果业务真的需要,可以考虑全文索引。
- 最左前缀原则的前提被破坏:联合索引(a, b, c),查询条件只用b或只用c时,索引可能无法利用。这个点面试官特别喜欢追问“为什么”,本质是联合索引存储结构按第一列优先排序。
5.2 覆盖索引
覆盖索引指的是查询的字段全部命中了索引包含的列。此时InnoDB可以不需要回表查原数据行,直接通过索引数据返回结果,效率高很多。
面试题经典版本:SELECT姓名 FROM 员工表 WHERE 部门ID = ?,希望避免回表,可以建联合索引(部门ID, 姓名)。这样WHERE条件用部门ID定位索引段,SELECT要的姓名就在索引里,不用回表了。
这类题目价值在于它是真实SQL优化中最高频的手段。面试时答出覆盖索引的收益逻辑,比单纯背“有覆盖索引就不回表”这句话强得多。
5.3 EXPLAIN判断经验
讲到执行计划,让候选人现场解释EXPLAIN结果是很多资深面试官爱干的事。重点看几个字段:
- type:从好到坏常见排列是system > const > eq_ref > ref > range > index > ALL。看到ALL基本就是全表扫描。
- key:实际用到的索引。possible_keys没被用上的话,值得问一句为什么。
- rows:预估扫描行数,越小越好,但只是优化器的估算,不是真实值。
- Extra:如果出现Using filesort或Using temporary,通常意味着有额外的排序或临时表开销,常见于GROUP BY、ORDER BY和DISTINCT组合不当。
数据库每张表有索引不一定代表SQL走索引,优化器的选择基于基数字、数据分布和查询成本估算。理解了这一层,才算真正看懂了执行计划。
5.4 给出优化思路
面试题常给一条慢SQL,要求优化。我不会上来就大谈索引,一般按这个顺序讲:
- 先看执行计划,确认瓶颈在全表扫描、排序、临时表还是联表连接顺序。
- 检查WHERE条件里的列有没有索引,索引有没有失效。
- 检查SELECT的列能不能用覆盖索引。
- 分析是否必须取全量数据,LIMIT能否下推,排序字段是否能配合索引。
- 最后才会考虑是不是要拆SQL或者换思路。
这个顺序概括下来就是“先定位瓶颈,再对症下药”。面试能把这个思路讲清楚,比背出一堆优化命令有用得多。
6. 事务、锁与隔离:数据库理论必考点
6.1 事务四大特性ACID
ACID是事务题的地基。原子性、一致性、隔离性、持久性,多数人都能背。容易被追问的是“数据库是怎么实现原子性的”。这里最常提到的是undo log:事务执行时先记undo日志,如果事务回滚,可以用undo log把数据恢复到事务前的状态。
同样,持久性和redo log相关:事务提交后,会先把redo log写入磁盘,保证即使数据库崩溃,恢复时也能把已提交事务的重做出来。这两个log的区别,面试里经常成对出现,能讲清楚会明显加分。
6.2 隔离级别与脏读、不可重复读、幻读
隔离级别这个知识点属于必背。从低到高排列:
- 读未提交
- 读已提交
- 可重复读
- 串行化
标准SQL中,读未提交可能脏读;读已提交解决脏读,但不可重复读仍可能发生;可重复读解决不可重复读,但幻读仍可能出现;串行化三者都能避免。
MySQL默认隔离级别是可重复读,这个事实经常被当作单独的一个问题来问。更关键的是MySQL的可重复读用间隙锁一定程度解决了部分幻读问题,但并不完全等同于标准SQL里的可重复读,面试里稍微提一嘴这个差异,能体现你用的是MySQL的实践经验而非死背教材。
6.3 乐观锁和悲观锁
SQL面试题里锁的概念,不会问得太深,但乐观锁和悲观锁是最常见的二选一。
悲观锁直观理解:做事之前先上锁,防止别人来动这份数据,别人才得等我做完。SQL里常见就是SELECT ... FOR UPDATE。
乐观锁思路是不加锁,更新时检查版本号或时间戳。例如更新用户余额前,先取出version,UPDATE时带上WHERE version = 旧值,如果影响行数为0,说明数据被改过,需要重试。这种机制很贴合互联网高并发场景的轻量状态更新。
面试时问到“超卖问题怎么解决”,本质是问你悲观锁和乐观锁的取舍。库存扣减这类写冲突频繁的场景,用悲观锁更稳;读多写少的资产信息更新,用乐观锁更合适。能结合业务特性讲取舍,是面试官希望看到的思考方式。
7. 避坑经验与学习路线建议
7.1 我见过的翻车现场
我在面试里见过很多候选人,技术底子不差,但输在细节上。归纳起来,翻车主要集中在几个地方:
第一,题目没审清就动手。问“连续登录最大天数”,有人上来就写DISTINCT日期,忘记同一天可能有重复登录记录;问“每个部门前三名”,没问并列规则就默认用ROW_NUMBER。面试不是笔试机器,写之前先花三十秒把需求边界问清楚,反而显得专业。
第二,不检查和NULL相关的行为。COUNT(列名)不数NULL,LEFT JOIN后判断右表字段是否为NULL来筛选,聚合函数遇到NULL的行为。很多SQL题的结果偏差都来自对NULL的处理不当。
第三,语法能跑通就停止思考。面试官追问“为什么这么写”“还有没有更好写法”,答不上来。要记住,SQL面试题的考察重点一直是你是否能写出正确、可维护、性能可感知的SQL,而不是“会一种写法”。
7.2 刷题工具与资源心得
如果本地有数据库,建议直接建一套练习用的表和样本数据,把经典的十道题亲手写一遍,再练习换不同写法。没有本地环境的话,在线练习平台思路清晰,但一定要自己动笔写,光看题解的效果非常差。
我刷题的时候习惯用一个记录表,把每道题的错误点、优化点、涉及的函数记下来。比如“日期减行号解决连续区间”“DENSE_RANK处理并列TopN”,后续面试前翻一遍,几分钟就能复习完。
7.3 面试实战的小技巧
最后分享几个面试中亲自验证过的小技巧。
把答案说成“思路+代码+验证”三段式。先讲你用几分钟拆解了题目,得到什么逻辑,然后写SQL,最后说怎么验证或有没有边界场景要考虑。这样即使代码有瑕疵,面试官也能看到完整思维链路。
主动谈复杂度和性能。SQL题写完,随口分析一下数据量和执行特点。比如“这个子查询在百万级数据下可能会退化成多次扫描,如果实际数据量大,我会用JOIN或窗口函数改写”。这句话不一定要最优解,但展示的是工程判断力。
不要假装完全不会的题目。SQL题偶尔会出到冷门方言特性,比如特定数据库的专用语法。坦白说“这个语法我不常用,但根据我的理解应该是……”然后给出推测方向,运行验证后再修正。面试官在乎的是学习能力和应激反应,而不是题库覆盖率。
我自己的真实体会是:SQL面试题如果只当成考试去准备,你永远追不完题目。但如果你把这些题目当成理解数据之间关系的窗口,用心记下每类题的套路和边界条件,面试时反而轻松,因为底层思考方式是一样的。刷题之后也可以试着用SQL去解决手头真实业务问题,比如算一算自己负责模块的留存率变化。把这些高频题目真正变成自己的肌肉记忆,下次面试再看到它们,你会像我一样,心里先笑了笑:老朋友又来了。