SQL 面试题:9 道带真实查询结果的详解

九道题,全部围绕「看上去明显是对的、结果却不是」的查询。每组先说面试官到底在查什么,然后给你一张小表和一个查询——先自己推出结果,再展开解析。

SQL 面试题分两类。定义型的(「什么是 JOIN」)可以背,而且 Google 也乐意直接替你答了。另一类给你一个看上去明显正确的查询,问它返回什么——决定面试成败的是后者,因为它考的是你能否在脑子里执行 SQL,而不只是认得 SQL。

本页全部是第二类。四个方面覆盖了大部分被问到的东西:JOIN 如何把行数乘开、过滤条件该放在哪、NULL 为什么会破坏比较和聚合、聚合条件为什么不能写进 WHERE、以及窗口函数如何在不折叠行的前提下排名。

这里的内容都是标准 SQL,在 MySQL、PostgreSQL 和 SQL Server 上行为一致;凡涉及方言差异的地方解析里都会说明。每道题先自己推一遍再看答案——解析还会点出通常紧跟着来的那个追问。

题数
7 题
分组
4 组
费用与账号
免费,无需注册
解析
逐题答案与解析,可展开查看

四个方面,九道题

JOIN:把两种答案区分开的那些问题

2 道题

「LEFT JOIN 保留左表未匹配行」人人会背。分数在后半句:那些行的右表列全是 NULL,而这一个事实就解释了两个经典 bug——在 WHERE 里对右表列加条件,会把 LEFT JOIN 悄悄变回 INNER JOIN;COUNT(右表列) 会把它们静默跳过。把 NULL 这个后果说出来,两个追问就一起答掉了。

通常怎么问:「LEFT JOIN 和 INNER JOIN 有什么区别?」·「匹配不上的行会怎样?」

第 1 题

表 users 有 100 行,表 orders 只有其中 30 个用户的订单。两个查询各返回多少行?

-- A
SELECT * FROM users u INNER JOIN orders o ON o.user_id = u.id;

-- B
SELECT * FROM users u LEFT JOIN orders o ON o.user_id = u.id;
  1. AA 返回 30 行,B 返回 100 行——一定如此
  2. BA 每条匹配订单一行;B 是这一批再加上 70 个未匹配用户各一行(右表列全 NULL)
  3. C两个都恰好返回 100 行
  4. DA 返回 100 行,B 返回 30 行
▶看答案与解析

正确答案:B. A 每条匹配订单一行;B 是这一批再加上 70 个未匹配用户各一行(右表列全 NULL)

🐱 选项 A 那个「30」正是陷阱:一个用户有 5 条订单就产生 5 行,所以 INNER JOIN 返回的是每条匹配订单一行,不是每个用户一行。B 在此基础上再加 70 行、其 orders 列全为 NULL。这种「行数被乘开」的效应,正是 join 之后 COUNT(*) 经常看着不对的原因——面试官通常是在看你会不会自己发现,而不必等他点破。

第 2 题

为什么加上这个 WHERE 之后,LEFT JOIN 就变了味?

SELECT u.id, o.total
FROM users u
LEFT JOIN orders o ON o.user_id = u.id
WHERE o.total > 100;
  1. A不会变;LEFT JOIN 永远保留左表全部行
  2. B未匹配行的 o.total 是 NULL,而 NULL > 100 不为真,于是被过滤掉——结果等同于 INNER JOIN
  3. CWHERE 先于 join 执行,所以先过滤了 orders 表
  4. D这在标准 SQL 里是语法错误
▶看答案与解析

正确答案:B. 未匹配行的 o.total 是 NULL,而 NULL > 100 不为真,于是被过滤掉——结果等同于 INNER JOIN

🐱 join 确实保留了那些未匹配行——然后 WHERE 又把它们扔掉了,因为它们的 o.total 是 NULL,而任何与 NULL 的比较结果都是 UNKNOWN,WHERE 按「非真」处理。想过滤又不丢行,就把条件写进 ON(LEFT JOIN orders o ON o.user_id = u.id AND o.total > 100),或者补 OR o.total IS NULL。真正被考的能力,是知道条件该放在哪里。

NULL:那个不是值的值

2 道题

NULL 表示「未知」,既不是空也不是零——你调试过的所有反直觉 SQL 结果,几乎都由此而来。与 NULL 比较返回 UNKNOWN(所以要用 IS NULL),聚合函数会跳过 NULL(所以 COUNT(列) 和 COUNT(*) 不一样),而 NOT IN 一旦对上含 NULL 的列表会一行都不返回。面试官爱考 NULL,因为它的行为完全学得会、又完全猜不出。

通常怎么问:「COUNT(列) 到底数了什么?」·「为什么 WHERE x = NULL 不管用?」

第 3 题

orders 表有 10 行,其中 3 行的 coupon_code 是 NULL。三个表达式各返回什么?

SELECT COUNT(*), COUNT(coupon_code), COUNT(DISTINCT coupon_code)
FROM orders;
  1. A10, 10, 10
  2. B10、7、以及去重后的非 NULL 优惠码个数
  3. C7, 7, 7
  4. D10, 3, 3
▶看答案与解析

正确答案:B. 10、7、以及去重后的非 NULL 优惠码个数

🐱 COUNT(*) 数的是行、什么都不跳:10。COUNT(列) 只数非 NULL 值:7。COUNT(DISTINCT 列) 同样忽略 NULL,再去重。这是报表查询里「总数对不上」最常见的单一来源——也正是当你想问「有多少行」时,COUNT(*) 才是安全默认写法的原因。

第 4 题

明明有大量用户的 id 既不是 1 也不是 2,为什么这个查询返回零行?

SELECT * FROM users
WHERE id NOT IN (SELECT manager_id FROM staff);
-- staff.manager_id 里含有 1、2 和 NULL
  1. ANOT IN 不能用于子查询
  2. Bid NOT IN (1, 2, NULL) 对每一行都求值为 UNKNOWN,于是没有任何行通过 WHERE
  3. C子查询返回了零行
  4. DNOT IN 需要索引才能工作
▶看答案与解析

正确答案:B. id NOT IN (1, 2, NULL) 对每一行都求值为 UNKNOWN,于是没有任何行通过 WHERE

🐱 x NOT IN (1, 2, NULL) 展开成 x <> 1 AND x <> 2 AND x <> NULL,最后那个比较是 UNKNOWN——于是整个表达式永远不可能为 TRUE,结果静默地成了空集。改用 NOT EXISTS(它能正确处理 NULL),或在子查询里加 WHERE manager_id IS NOT NULL。这题之所以是面试官心头好,恰恰因为这个查询看上去明显是对的。

GROUP BY、HAVING,以及过滤条件该放哪

1 道题

这两个问题的答案是同一个:逻辑执行顺序。FROM 与 JOIN 先造出行,WHERE 过滤单行,GROUP BY 把行折叠成组,HAVING 过滤组,然后才是 SELECT,最后 ORDER BY。按原始列过滤用 WHERE;按聚合结果过滤用 HAVING——而且条件要尽量往前放,因为先过滤再分组意味着要分组的行更少。

通常怎么问:「WHERE 和 HAVING 的区别?」·「为什么不能选一个不在 GROUP BY 里的列?」

第 5 题

找出 2026 年下单超过 3 次的客户。哪个查询是对的?

-- A
SELECT user_id, COUNT(*) c FROM orders
WHERE COUNT(*) > 3 AND year = 2026
GROUP BY user_id;

-- B
SELECT user_id, COUNT(*) c FROM orders
WHERE year = 2026
GROUP BY user_id
HAVING COUNT(*) > 3;
  1. AA——过滤条件永远该写在 WHERE 里
  2. BB——WHERE 在分组之前过滤行,聚合条件必须写进 HAVING
  3. C两者完全等价
  4. D都不对,必须用子查询
▶看答案与解析

正确答案:B. B——WHERE 在分组之前过滤行,聚合条件必须写进 HAVING

🐱 A 直接就错:WHERE 在行被分组之前执行,此时 COUNT(*) 还不存在——多数引擎会直接报错。B 是对的,而且顺带是高效写法:先筛到 2026 年,要分组的行就少了。可以直接说出口的规则:原始列条件进 WHERE,聚合条件进 HAVING。

窗口函数:不折叠行也能排名

2 道题

「每组前 N 名」是最常被问的 SQL 难题,而窗口函数正是面试官期待的答案:与 GROUP BY 不同,它在一组行上计算却不折叠行,所以每一列都还在。三个排名函数的差别只在怎么处理并列——ROW_NUMBER 从不并列(1,2,3,4),RANK 并列后跳号(1,1,3),DENSE_RANK 并列且不跳号(1,1,2)。「取前三名且含并列」该选哪个,考的就是这个。

通常怎么问:「找第二高的薪水」·「每组前 N 名」·「ROW_NUMBER、RANK、DENSE_RANK 有什么区别?」

第 6 题

三名员工的薪水分别是 100、100、90。三个排名函数按此顺序各返回什么?

  1. A三个函数都返回 1, 1, 2
  2. BROW_NUMBER: 1,2,3 · RANK: 1,1,3 · DENSE_RANK: 1,1,2
  3. CROW_NUMBER: 1,1,2 · RANK: 1,2,3 · DENSE_RANK: 1,1,3
  4. D它们是同一个函数的别名
▶看答案与解析

正确答案:B. ROW_NUMBER: 1,2,3 · RANK: 1,1,3 · DENSE_RANK: 1,1,2

🐱 ROW_NUMBER 不管并列、一律给唯一序号,所以两个 100 会任意地拿到 1 和 2。RANK 给两个 1,然后跳到 3。DENSE_RANK 给两个 1,然后接着是 2。实际后果:要「薪水前三名且包含并列」用 DENSE_RANK;要「恰好三行」用 ROW_NUMBER——选错就是行被静默丢掉或多出来的原因。

第 7 题

取每个客户最近的一笔订单,并保留订单的全部列。哪种做法合适?

SELECT * FROM (
  SELECT o.*,
         ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY created_at DESC) rn
  FROM orders o
) t WHERE rn = 1;
  1. A写错了——正确做法是 GROUP BY user_id 配 MAX(created_at)
  2. B正确:PARTITION BY 让编号按客户重新开始,rn = 1 就是每位客户最新的那一行,且所有列都在
  3. C它总共只返回一行,而不是每位客户一行
  4. D窗口函数的结果不能用来过滤
▶看答案与解析

正确答案:B. 正确:PARTITION BY 让编号按客户重新开始,rn = 1 就是每位客户最新的那一行,且所有列都在

🐱 PARTITION BY 是窗口版的 GROUP BY,区别是行不会消失:编号在每个 user_id 内重新开始,于是 rn = 1 取到每位客户最新的订单、且整行列都在。选项 A 的 GROUP BY 写法只能给出最大时间戳,拿不到那一行的其余字段——要取回来就得自连接,而这份笨拙正是窗口函数要消除的东西。注意过滤必须放在外层查询:窗口函数在 WHERE 之后才计算。

继续练

SQL 面试题 — 常见问题

几乎每场面试都会问的 SQL 题有哪些?

LEFT JOIN 与 INNER JOIN 的区别(以及未匹配行会怎样)、WHERE 与 HAVING、COUNT(*) 与 COUNT(列)、怎么找第二高的值、以及每组前 N 名。后两个正是窗口函数的用武之地——即使你日常工作没用到,面试前也值得学。

初级岗位需要会窗口函数吗?

越来越需要——「每组前 N 名」在初级面试里就会问,而自连接的写法笨到面试官一眼能看出你会不会窗口写法。掌握 ROW_NUMBER、RANK、DENSE_RANK 加 PARTITION BY,基本够用。

该用哪种 SQL 方言作答?

除非对方指定引擎,否则按标准 SQL。本页内容在 MySQL、PostgreSQL 和 SQL Server 上表现一致。若题目触及方言特性,主动说明你假设的是哪个引擎——这读起来是严谨,不是不懂。

「这个查询返回什么」这类题怎么练?

把中间结果集一步步手写出来:join 之后、WHERE 之后、GROUP BY 之后各是什么。绝大多数错误来自跳步——尤其是忘了 join 会在其他一切之前先把行数乘开。

这些是真实面试题吗?

它们是围绕面试反复考查的概念编写的原创题,不是任何公司的实录。目的是练机制,而不是背题单。