SQLite ORDER BY 时 NULL 为什么总排在最前面
SQLite ORDER BY 时 NULL 为什么总排在最前面
一张表里混进了几条 NULL,ORDER BY 一跑,NULL 全挤到了最前面。改成 DESC,NULL 又跑到了最后面。数据没错,SQL 没报错,排序结果却跟直觉对不上。这篇笔记把这个现象复现一遍,定位到 SQLite 的排序规则,再给出两种改法,全部用本地 :memory: 数据库验证,不需要任何现成数据。
现象:NULL 的位置跟着 ASC/DESC 走
建一张最小的表,插几行带 NULL 的数据:
CREATE TABLE t_score(name TEXT, score INTEGER);
INSERT INTO t_score VALUES ('a', 90), ('b', NULL), ('c', 70), ('d', NULL), ('e', 85);
跑默认升序:
SELECT name, score FROM t_score ORDER BY score ASC;
结果是:
b | NULL
d | NULL
c | 70
e | 85
a | 90
前两行是两个 NULL,后面 70、85、90 按数值升序排好。NULL 稳稳排在所有数值前面,数值之间按正常数值顺序排。
再跑降序:
SELECT name, score FROM t_score ORDER BY score DESC;
结果变成:
a | 90
e | 85
c | 70
b | NULL
d | NULL
NULL 从最前面移到了最后面。换一张表、换一列,规律不变:ASC 时 NULL 在前,DESC 时 NULL 在后。NULL 的位置不是随机的,它跟着排序方向走。
判据就定在这里:同一条 ORDER BY,只改 ASC/DESC,NULL 从一端移到另一端,且数值之间的相对顺序不受 NULL 行干扰。这个现象可以稳定复现,就不是数据问题,也不是客户端显示问题。
判据:先确认是不是 NULL 在捣乱
碰到排序结果异常,先不要改 SQL 逻辑。跑三条语句做区分:
SELECT COUNT(*) FROM t_score WHERE score IS NULL;
SELECT name, score FROM t_score ORDER BY score ASC LIMIT 3;
SELECT name, score FROM t_score ORDER BY score DESC LIMIT 3;
第一条告诉你表里到底有几条 NULL。第二条和第三条对照看:升序前几行是不是 NULL,降序最后几行是不是 NULL。如果两边都对上,问题就锁定在 NULL 的排序位置上,跟索引、跟字符集、跟客户端都没关系。
还有一个容易混淆的情况:空字符串 ‘’ 不是 NULL,0 也不是 NULL。空字符串参与排序时按文本规则走,0 按数值规则走,只有真正的 NULL 才走这条“最小值”规则。判据里加一条就能区分:
SELECT name, score, score IS NULL AS is_null FROM t_score ORDER BY score ASC;
is_null 为 1 的行就是 NULL 行。看到它们集中在升序开头,结论就成立了。
根因:SQLite 把 NULL 当成最小值
SQLite 的排序规则写得很明确:不同类型的值按固定顺序排,NULL < 数字 < 文本 < BLOB。NULL 不是“没有值所以排哪里都行”,在排序比较里它就是最小的那一档,比任何数字都小,比空字符串也小。
ASC 是从小到大排,最小值自然在最前面,NULL 就在最前面。DESC 是从大到小排,最小值自然在最后面,NULL 就在最后面。这个行为跟 MySQL 默认把 NULL 当最小值、PostgreSQL 默认 ASC 时 NULL 在最后面都不一样。换数据库迁移 SQL 时,排序结果变了,根子往往就在这里:每个数据库对 NULL 的默认位置规定不同,SQLite 选的是“NULL 最小”。
理解了这一条,前面那个现象就解释通了:不是 ORDER BY 坏了,也不是数据插错了,是 SQLite 按“NULL 最小”这条规则在执行。NULL 行之间没有大小之分,它们的相对顺序不保证,b 在 d 前面只是这次执行的结果,换一次查询、加一个索引,NULL 行内部顺序可能变化,但 NULL 整体在前或在后这个位置不会变。
修法:两种写法,按业务语义选
业务上常见的需求有两种,写法不一样,选错会把结果改错。
第一种:NULL 本来就是“还没打分”,希望它排在最后,不管升序降序。写法是把 NULL 单独拎出来做第一排序键:
SELECT name, score FROM t_score ORDER BY score IS NULL, score ASC;
score IS NULL 在 NULL 行上是 1,在非 NULL 行上是 0。先按这个值升序,非 NULL 行(0)排前面,NULL 行(1)排后面,再按 score 排数值。这样 ASC 时 NULL 在最后,DESC 时把第二键改成 DESC 即可:
SELECT name, score FROM t_score ORDER BY score IS NULL, score DESC;
第二种:NULL 要当成一个具体数值处理,比如缺考记 0 分参与排名。写法是用 COALESCE 给 NULL 一个替补值:
SELECT name, score, COALESCE(score, 0) AS sort_score
FROM t_score ORDER BY sort_score ASC;
这条语句里 NULL 行按 0 参与排序,会排在 70、85、90 前面,但不再是“因为最小所以在前”,而是“因为替补值是 0”。两种写法结果看起来接近,语义完全不同:第一种是“NULL 排最后”,第二种是“NULL 按 0 算”。业务说“没打分的不参与排名”,用第一种;业务说“缺考记 0”,用第二种。不要混用。
还有一种写法值得提醒:ORDER BY score ASC NULLS LAST。这是 PostgreSQL、Oracle 的语法,SQLite 不支持,贴过去会直接报语法错误。SQLite 里实现 NULLS LAST 靠的就是上面 IS NULL 做排序键的写法,没有现成关键字。
验证:三组对照,确认改法生效
用同一张表跑三组查询,对照输出:
-- A:默认行为,ASC 时 NULL 在前
SELECT name, score FROM t_score ORDER BY score ASC;
-- B:NULL 排最后,ASC
SELECT name, score FROM t_score ORDER BY score IS NULL, score ASC;
-- C:NULL 按 0 算,ASC
SELECT name, score, COALESCE(score, 0) AS s FROM t_score ORDER BY s ASC;
预期结果:A 组前两行是 b、d 两个 NULL;B 组前三行是数值行(85、70、90 按数值升序应为 70、85、90),最后两行是 NULL;C 组第一行是 NULL 行(按 0 算),后面跟 70、85、90。这里要留心 B 组的数值顺序:70 在 85 前面,85 在 90 前面,如果看到 85 在 70 前面,说明第二排序键没写对,或者数据本身插错了,回头检查 INSERT 语句。
本地复现的完整命令,用 sqlite3 命令行一次跑完:
sqlite3 :memory: "CREATE TABLE t_score(name TEXT, score INTEGER);
INSERT INTO t_score VALUES ('a', 90), ('b', NULL), ('c', 70), ('d', NULL), ('e', 85);
SELECT 'ASC默认', name, score FROM t_score ORDER BY score ASC;
SELECT 'NULL最后', name, score FROM t_score ORDER BY score IS NULL, score ASC;
SELECT '按0算', name, COALESCE(score, 0) FROM t_score ORDER BY 3 ASC;"
输出里能同时看到三组结果:默认组 NULL 在前,NULL 最后组 NULL 在后,按 0 算组 NULL 行按 0 出现。三组对上,说明修法生效,问题闭环。
碰到 SQLite 排序结果里 NULL 位置异常,先跑 COUNT + IS NULL 确认,再对照 ASC/DESC 两端,最后按业务语义选 IS NULL 排序键还是 COALESCE。规则本身不复杂,复杂的是把它当成数据错误去修别的地方。
- 点赞
- 收藏
- 关注作者
评论(0)