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

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

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

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

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

四个方面,九道题

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 会在其他一切之前先把行数乘开。

这些是真实面试题吗?

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