排序时遇到 NULL 默认排在哪里?如何控制 NULLS FIRST / NULLS LAST?
What is the default sort order for NULLs? How to control it with NULLS FIRST /
考察要点
这道题面试官想看你是否理解 SQL 中 NULL 的特殊性,以及它在排序操作中的行为。核心考察点包括:
- SQL 标准与实现差异:是否知道
NULL的默认排序行为在 SQL 标准中未定义,并且主流数据库(如 MySQL, PostgreSQL, Oracle, SQL Server)有不同的默认实现。 - 显式控制能力:是否掌握标准 SQL 提供的
NULLS FIRST/NULLS LAST语法来精确控制NULL的位置。 - 兼容性与变通方案:在数据库不支持标准语法时,是否能用
CASE表达式等通用方法实现相同的排序逻辑,体现解决问题的灵活性。
核心概念
NULL 在 SQL 中代表“未知”或“缺失值”,它不等于任何值,包括另一个 NULL。这导致它在排序时的行为需要特别定义。
1. 默认排序行为
SQL 标准并未规定 NULL 的默认排序顺序,交由各数据库厂商自行实现。
| 数据库 (RDBMS) | ORDER BY col ASC | ORDER BY col DESC | 默认行为总结 |
|---|---|---|---|
| MySQL / MariaDB | NULL 在最前 | NULL 在最后 | NULL 值被视为比任何非 NULL 值都小 |
| SQL Server | NULL 在最前 | NULL 在最后 | NULL 值被视为比任何非 NULL 值都小 |
| PostgreSQL | NULL 在最后 | NULL 在最前 | NULL 值被视为比任何非 NULL 值都大 |
| Oracle | NULL 在最后 | NULL 在最前 | NULL 值被视为比任何非 NULL 值都大 |
数据示例:
假设我们有表 employees:
| id | name | salary |
|---|---|---|
| 1 | Alice | 8000 |
| 2 | Bob | NULL |
| 3 | Charlie | 9000 |
| 4 | David | NULL |
- 在 MySQL / SQL Server 中执行
ORDER BY salary ASCtext1-- 输出22, Bob, NULL34, David, NULL41, Alice, 800053, Charlie, 9000 - 在 PostgreSQL / Oracle 中执行
ORDER BY salary ASCtext1-- 输出21, Alice, 800033, Charlie, 900042, Bob, NULL54, David, NULL
2. 如何控制 NULL 的位置
为了解决不同数据库实现带来的不一致性,SQL 标准(从 SQL:2003 开始)引入了 NULLS FIRST 和 NULLS LAST 子句。
NULLS FIRST:强制将NULL值排在最前面,无论ASC还是DESC。NULLS LAST:强制将NULL值排在最后面,无论ASC还是DESC。
语法支持情况:
- ✅ 支持:PostgreSQL, Oracle, MySQL 8.0+, SQLite 3.30+
- ❌ 不支持:SQL Server, MariaDB, MySQL 5.7 及更早版本
SQL 示例 (假设在 PostgreSQL 或 MySQL 8.0+ 环境):
1-- 无论升序还是降序,都将 NULL 排在最后2SELECT name, salary FROM employees ORDER BY salary ASC NULLS LAST;3SELECT name, salary FROM employees ORDER BY salary DESC NULLS LAST;
3. 兼容性解决方案
对于不支持 NULLS FIRST/LAST 的数据库,可以使用 CASE 表达式或特定函数来模拟。
a. 使用 CASE 表达式(最通用)
通过创建一个额外的排序列,将 NULL 和非 NULL 值分到不同的组。
1-- 目标:升序排列,NULL 在最后 (NULLS LAST)2-- 思路:创建一个排序列,非 NULL 为 0,NULL 为 1,先按此列升序排,再按 salary 升序排3SELECT name, salary4FROM employees5ORDER BY6 CASE WHEN salary IS NULL THEN 1 ELSE 0 END ASC,7 salary ASC;89-- 目标:降序排列,NULL 在最前 (NULLS FIRST)10-- 思路:创建一个排序列,NULL 为 0,非 NULL 为 1,先按此列升序排,再按 salary 降序排11SELECT name, salary12FROM employees13ORDER BY14 CASE WHEN salary IS NULL THEN 0 ELSE 1 END ASC,15 salary DESC;
b. 使用 IS NULL (MySQL/MariaDB 特有技巧)
在 MySQL 中,布尔表达式的结果是 1 (true) 或 0 (false)。salary IS NULL 在 salary 为 NULL 时返回 1,否则返回 0。
1-- 目标:升序排列,NULL 在最后 (NULLS LAST)2-- salary IS NULL 返回 0 (非NULL) 或 1 (NULL),升序排列此结果即可3SELECT name, salary4FROM employees5ORDER BY salary IS NULL ASC, salary ASC;
常见低分回答
- “
NULL默认都排在最前面”:这是最常见的错误,候选人只熟悉 MySQL 或 SQL Server 的行为,并将其泛化为所有数据库的规则,忽略了 PostgreSQL/Oracle 的不同实现。 - 不知道
NULLS FIRST/LAST:只知道使用CASE表达式或IFNULL等 workaround,说明对标准 SQL 的新特性了解不足,缺乏在现代数据库环境下的最佳实践经验。 - 滥用
COALESCE或IFNULL:写出ORDER BY IFNULL(salary, -1) ASC这样的代码。这有潜在风险:如果-1是一个合法的salary值,排序结果就会出错。这种方法只在替换值(如-1, 99999999)绝对不可能出现在业务数据中时才勉强可用,但健壮性差。 - 在 SQL Server 中尝试使用
NULLS LAST:写出不兼容的 SQL,表明对特定数据库方言的掌握不牢固。 - 混淆
ORDER BY col DESC和NULL的关系:例如在 PostgreSQL 中,认为ORDER BY salary DESC会把NULL放在最后,而实际上默认是放在最前。
工程实践与优化
- 优先使用标准语法:在支持
NULLS FIRST/LAST的环境中(如 MySQL 8.0+),应始终优先使用此语法。它代码更简洁、意图更明确,且数据库优化器能更好地理解和优化。 CASE表达式的性能:ORDER BY CASE WHEN col IS NULL ... END, col这种写法虽然通用,但可能影响索引的使用。优化器可能无法将其转换为简单的索引扫描,从而导致filesort(在内存或磁盘上排序),尤其是在col上有索引时。相比之下,原生NULLS FIRST/LAST更有可能被优化器利用索引高效执行。- 通过
EXPLAIN验证:无论使用哪种方法,都应该使用EXPLAIN(或EXPLAIN ANALYZE) 来检查执行计划。关注type、key和Extra字段。如果Extra中出现Using filesort,说明排序无法利用索引,需要分析原因。 - 索引设计:B+ 树索引本身就可以存储
NULL值。在 MySQLInnoDB中,NULL值被视为最小值,并记录在索引的最左侧叶子节点。因此,ORDER BY col ASC(默认NULLS FIRST) 可以直接利用索引顺序扫描,而ORDER BY col DESC也是反向扫描,效率很高。 - 避免在应用层排序:除非数据量极小且有特殊业务逻辑,否则不要将排序任务交给应用层。这会增加网络传输开销、消耗应用服务器资源,并且无法实现数据库分页(
LIMIT/OFFSET)。
常见追问与延伸题目
-
追问:如果我用的 MySQL 5.7,
ORDER BY salary IS NULL, salary这个写法的原理是什么?- 回答要点:在 MySQL 中,布尔表达式的结果为
0(false) 或1(true)。salary IS NULL这个表达式会针对每一行计算:如果salary是NULL,结果是1;如果不是NULL,结果是0。ORDER BY salary IS NULL ASC, salary ASC的意思是:- 首先,按照
salary IS NULL的结果升序排序。所有非NULL行(结果为0)会排在NULL行(结果为1)的前面。 - 然后,在
0和1这两个大组内部,再按照salary自身的值升序排序。这就实现了非NULL值内部有序,且所有NULL值被统一放到了最后。
- 首先,按照
- 回答要点:在 MySQL 中,布尔表达式的结果为
-
追问:如果我要对一个字符串列
position排序,同时希望NULL和空字符串''都排在最后,该怎么写?- 回答要点:这需要一个更精细的
CASE表达式来创建三个排序组:有效字符串、空字符串、NULL。这个sql1ORDER BY2 CASE3 WHEN position IS NULL THEN 2 -- NULL 值组4 WHEN position = '' THEN 1 -- 空字符串组5 ELSE 0 -- 其他有效字符串组6 END ASC,7 position ASC;CASE表达式创建了一个优先级:有效字符串 (0) > 空字符串 (1) >NULL(2)。先按这个优先级升序,即可把空字符串和NULL都排到最后,并且NULL在空字符串之后。
- 回答要点:这需要一个更精细的
-
追问:
NULLS FIRST/LAST和CASE写法,在有索引的情况下,哪个性能更好?为什么?- 回答要点:通常情况下,
NULLS FIRST/LAST的性能更好。NULLS FIRST/LAST是数据库的原生语法。查询优化器被设计为能够直接理解这个指令。如果排序列上有索引,优化器可以直接利用索引进行顺序或逆序扫描,NULL值在 B+ 树索引中通常位于一端,数据库可以高效地处理它们,避免额外的排序操作。CASE表达式 对优化器来说是一个黑盒函数。虽然现代优化器越来越智能,但很多时候它无法将ORDER BY CASE WHEN col IS NULL...优化为对col列索引的直接使用。这很可能导致数据库放弃使用索引进行排序,转而执行“filesort”:即读取所有满足WHERE条件的行,然后在内存或临时文件中进行排序。这个过程的代价远高于直接利用索引。- 结论:
NULLS FIRST/LAST不仅可读性好,而且给了优化器最大的优化空间,是性能首选。CASE表达式是功能强大的兼容性后备方案,但在性能敏感的场景下要用EXPLAIN仔细评估其对索引使用的影响。
- 回答要点:通常情况下,