跳到主要内容
约 5 分钟5 张卡

SQL 查询

SQL 查询用声明式条件描述需要返回的数据。本文负责 MySQL 8.0 的过滤、空值、去重、排序和分页。

1. 面试核心​

先记住这几条,再读正文

2. 最小表与过滤​

CREATE TABLE candidates (
id INTEGER PRIMARY KEY,
city VARCHAR(20),
score INTEGER
);
INSERT INTO candidates VALUES
(1, '上海', 80), (2, '杭州', NULL),
(3, '上海', 95), (4, '杭州', 80);

SELECT id, score
FROM candidates
WHERE score >= 80
ORDER BY score DESC, id ASC;

结果依次为 (3,95)、(1,80)、(4,80)。id=2 的比较结果是 UNKNOWN,因此被 WHERE 移除。查询未填写成绩者使用 WHERE score IS NULL。score = NULL 的结果仍是 UNKNOWN。

3. NOT IN 与 NULL​

NOT IN 要求目标值与集合中每个值都不相等。

SELECT 3 NOT IN (1, NULL) AS result;
SELECT NULL <=> NULL AS null_equal;

第一条返回 NULL,因为与 NULL 的比较为 UNKNOWN。WHERE 会排除该结果。第二条返回 1,<=> 是 MySQL 的空值安全等值运算。

子查询列可空时,应明确是否排除 NULL。查询“没有关联记录”可用 NOT EXISTS 表达。具体连接写法见 存在性与反连接。

4. 去重与分页​

DISTINCT 对所选列的组合去重。

SELECT DISTINCT city FROM candidates ORDER BY city;
SELECT id, score FROM candidates
WHERE score IS NOT NULL
ORDER BY score DESC, id ASC LIMIT 2 OFFSET 1;

第一条返回上海、杭州各一行。中文先后取决于排序规则。第二条跳过最高成绩,返回 (1,80)、(4,80)。同分记录按唯一 id 升序排列,因此顺序确定。

逻辑上,FROM 与 JOIN 形成来源行。WHERE 筛选行,GROUP BY 和 HAVING 处理组。随后计算窗口与输出列,再去重、排序和取 LIMIT。优化器可调整物理步骤,但须保持相同语义。

5. 游标分页​

大 OFFSET 需要先读取并跳过前面的记录。游标分页则从上页最后一个排序键继续查找。

-- 上页最后一行为 id=1、score=80。
SELECT id, score FROM candidates
WHERE score < 80 OR (score = 80 AND id > 1)
ORDER BY score DESC, id ASC LIMIT 2;

结果为 (4,80)。成绩降序对应 <80,同分的 id 升序对应 >1。实际请求应绑定上页游标,避免硬编码。

排序列频繁变化时,行仍会跨过游标边界。完整导出可使用稳定键,并明确数据截止范围。

6. UNION 与 UNION ALL​

UNION 合并兼容的结果集,并去除重复行。UNION ALL 直接保留所有行,省去去重步骤。

SELECT city FROM candidates WHERE id=1
UNION ALL
SELECT city FROM candidates WHERE id=3;

结果包含两行上海。改为 UNION 后只剩一行。分支结果的拼接顺序没有保证。需要最终顺序时,在合并结果上使用 ORDER BY。

7. 窗口排名与分组前几名​

窗口函数为每行计算组内结果,仍保留明细行。MySQL 8.0 起支持窗口函数。

SELECT id, city, score FROM (
SELECT id, city, score,
ROW_NUMBER() OVER (
PARTITION BY city ORDER BY score DESC, id ASC
) AS rn
FROM candidates WHERE score IS NOT NULL
) ranked
WHERE rn <= 1 ORDER BY id;

结果为 (3,上海,95)、(4,杭州,80)。内层对各城市按成绩编号,外层选每组第一行。用派生表过滤,是因为 WHERE 早于窗口结果计算。

ROW_NUMBER 为每行分配不同序号。按成绩使用 RANK 时,并列后跳过对应名次。DENSE_RANK 为并列成绩分配相同且连续的名次。成绩为 95、95、80 时,RANK 为 1、1、3。DENSE_RANK 则为 1、1、2。

8. 使用边界​

AND 的优先级高于 OR。混合条件可用括号明确组合关系。字符串比较由字符集和排序规则决定。

稳定排序只确定单次查询中的先后。跨页期间新增或修改数据,仍会改变分页位置。需要完整一致的数据集时,还需约定快照范围。

9 · 面试题卡片​

本页题目与参考回答

9.1 为什么 score = NULL 查不到空值,分页时又为什么需要稳定排序?​

考察重点:NULL 的三值逻辑、分页决胜列与多次查询间的数据变化。

相关内容:面试核心、最小表与过滤、使用边界。

参考回答

score=NULL 得到 UNKNOWN,WHERE 只保留 TRUE。

判断空值应使用 IS NULL。分页还需 ORDER BY,并用唯一列确定同分顺序。稳定排序确定单次结果,跨次查询的一致性需另行设计。

9.2 NOT IN 的子查询含 NULL 时,为什么会漏掉预期结果?​

考察重点:三值逻辑与反连接。

相关内容:NOT IN 与 NULL。

参考回答

未命中其他值时,与 NULL 的比较仍是 UNKNOWN。

因此整个 NOT IN 条件无法得到 TRUE。可明确过滤子查询 NULL,或用 NOT EXISTS 表达无关联记录。外层字段可空时,还需定义它是否应进入结果。

9.3 成绩降序、ID 升序时,如何写下一页的游标条件?​

考察重点:混合排序方向与唯一决胜列。

相关内容:游标分页。

参考回答

下一页满足 score<lastScore,或同分且 id>lastId。

ORDER BY 保持 score DESC,id ASC。该方式省去逐页增加的 OFFSET 跳过量。排序值改变仍会移动行,因此还需明确快照要求。

9.4 UNION、UNION ALL 与 DISTINCT 如何处理重复行?​

考察重点:合并结果与去重粒度。

相关内容:UNION 与 UNION ALL。

参考回答

UNION 对合并后的结果去重,UNION ALL 保留重复行。

DISTINCT 对一条查询所选列的组合去重。三者都按输出列判断重复。需要保留重复明细时,应使用 UNION ALL。

9.5 每组取前 N 行时,ROW_NUMBER、RANK、DENSE_RANK 如何选择?​

考察重点:明细保留、并列名次与过滤阶段。

相关内容:窗口排名与分组前几名。

参考回答

固定最多 N 行可用 ROW_NUMBER,并加入唯一决胜列。

需要保留同分名次时,用 RANK 或 DENSE_RANK。RANK 在并列后跳号,DENSE_RANK 连续编号。窗口结果应在外层查询中按名次过滤。

10. 参考资料​