ORDER BY 的两条暗道:你以为排的是别名,其实排的是原列

2026-05-15 20 预计阅读时间:1 分钟
来源:postgr.es AI 摘要 原文链接

免责声明:本文为 AI 摘要整理,建议结合原文阅读。摘要可能省略上下文、版本差异或边界条件,不作为官方说明。

预计阅读时间:13 分钟

写 SQL 的人每天都在用 ORDER BY,绝大多数时候它表现得像一条直线——写个列名,结果就按那列排。但一旦别名和原列在表达式、大小写或修饰符上产生分歧,Postgres 会悄悄切换到另一条解析路径,而你毫不知情。

先看一道谜题。建一张四行小表:

CREATE TABLE nums (a int);
INSERT INTO nums VALUES (0), (1), (2), (3);

猜猜这两条查询的输出顺序:

SELECT -a AS a FROM nums ORDER BY a;
SELECT -a AS a FROM nums ORDER BY -a;

直觉会说:结果集相同,只是排序依据不同,所以行顺序应该不一样。实际答案是——完全相同的行,完全相同的顺序。再往下看:

SELECT a AS c FROM nums ORDER BY -c;

你可能会觉得这和上面第二条等价。但它直接报错:column "c" does not exist。别名 c 明明就在语句里,解析器却说找不到。

这就是 ORDER BY 的丛林。下面把地图画出来。

两条路径,两个世界

ORDER BY 接受的东西看起来只有一种——"排序依据"。但解析器把它们分成了两条完全不同的路:

  • 名字路径(SQL-92):裸标识符,比如 ORDER BY created_at。解析器先扫描 SELECT 列表,找同名别名,用别名的输出值排序。
  • 表达式路径(SQL-99):任何不是裸标识符的东西,比如 ORDER BY date(created_at)。解析器只看 FROM 子句的表列,别名根本不在视野里。

两条路径从不看同一个地方。名字路径看 SELECT 列表;表达式路径看 FROM 表。这就是所有意外的根源。

同样的输出,不同的逻辑

回到第一条谜题:

SELECT -a AS a FROM nums ORDER BY a;

ORDER BY a 是裸标识符,走名字路径。解析器在 SELECT 列表里找到别名 a(实际值是 -a),按别名的输出值排序。-3, -2, -1, 0 升序,结果就是 -3, -2, -1, 0

第二条:

SELECT -a AS a FROM nums ORDER BY -a;

ORDER BY -a 是表达式(一元负号 + 列引用),走表达式路径。解析器只认 nums.a,对输入值取负后排序。输入值 0,1,2,3 取负得 0,-1,-2,-3,升序排列同样是 -3,-2,-1,0

输出碰巧一样,逻辑完全不同。SELECT 列表里的负号去掉,只保留 ORDER BY 里的,差异就暴露了:

SELECT a AS c FROM nums ORDER BY -a;

结果:

 c
---
 3
 2
 1
 0

ORDER BY -a 走表达式路径,按 -input_a 升序排,等于 input_a 降序。别名 c 从未被参考。而 ORDER BY -c 直接报错——表达式路径只看 FROM,表里没有列 c

修饰符的陷阱:哪些算"裸",哪些不算

知道规则后,下面的现象就都能解释了,但直觉仍然会出错。

类型转换是表达式:

SELECT 'hello' AS x FROM nums ORDER BY x::text;
-- ERROR: column "x" does not exist

加了 ::textx 就不再是裸标识符,走表达式路径,找不到表列 x

DESC / NULLS FIRST 不是表达式:

SELECT a AS c FROM nums ORDER BY c DESC NULLS FIRST;
-- 正常工作,按别名 c 降序、nulls first 排序

DESCNULLS FIRST 是排序修饰符,挂在解析树的上层,不改变 c 本身的身份。解析器仍然看到裸标识符 c,走名字路径找到别名,再在结果上应用修饰符。

COLLATE 是表达式——这是最坑的:

SELECT 'A'::text AS x FROM nums ORDER BY x COLLATE "C";
-- ERROR: column "x" does not exist

COLLATE 在解析树上包裹了 x,把它从裸标识符变成了表达式。人类觉得它和 DESC 一样是排序修饰符,解析器说它是表达式的一部分。

括号是特例:

SELECT -a AS a FROM nums ORDER BY (a);
-- 正常工作,按别名排序

Postgres 在做裸标识符检查之前会先折叠冗余括号,所以 (a) 仍然是裸 a。这造成了一种不对称:COLLATE 对人来说是"名字上的修饰",对解析器是表达式;(a) 对人来说是"表达式",对解析器是名字。两种错误直觉同时存在。

一元正号:

SELECT -a AS a FROM nums ORDER BY +a;

+aa 的值相同,但解析节点不同。一元正号让标识符变成了表达式,走表达式路径,按原列 nums.a 排序。输出顺序和 ORDER BY a(走名字路径按别名排)不一样。一个你根本不会注意的 + 号,改变了结果行的顺序。

表名限定:

SELECT -a AS a FROM nums ORDER BY nums.a;

nums.a 看起来是标识符,但解析后是两个名字部分(表名 + 列名),裸标识符快速路径只对单部分名字生效。于是走表达式路径,按 nums.a 原列排序。两条看似等价的 ORDER BY,输出顺序截然不同:

-- ORDER BY a      → 按别名 -a 排序:-3, -2, -1, 0
-- ORDER BY nums.a → 按原列 a 排序:0, -1, -2, -3

大小写与引号:ORM 常踩的坑

ORM 和代码生成器(SQLAlchemy、Hibernate、jOOQ)会给不是纯小写的别名加引号。两条查询只差一个引号,结果集顺序不同:

SELECT -a AS A FROM nums ORDER BY a;   -- 按别名排序:-3,-2,-1,0
SELECT -a AS "A" FROM nums ORDER BY a; -- 按原列排序:0,-1,-2,-3

原因:裸标识符检查用 strcmp 做比较。不加引号的 A 被 Postgres 折叠为小写 a,和 ORDER BY a 匹配,走名字路径。加引号的 "A" 保留原大小写,和 a 不匹配,名字路径找不到,落入表达式路径,按原列排序。查询成功执行,但排序逻辑和你预期的完全不同。

GROUP BY 走反方向

GROUP BYORDER BY 都接受裸标识符,都能解析到表列或别名——但检查顺序相反

  • ORDER BY a:先看 SELECT 列表,再看 FROM
  • GROUP BY a:先看 FROM,再看 SELECT 列表。

当别名和原列同名但值不同时,两条 clauses 对同一个标识符的理解就不一样了:

SELECT a/2 AS a, count(*) FROM nums GROUP BY a ORDER BY a;

GROUP BY a 先看 FROM,选原列 nums.a(四个不同值,四组,每组一行)。ORDER BY a 先看 SELECT 列表,选别名 a/2。结果:

 a | count
---+-------
 0 |     1
 0 |     1
 1 |     1
 1 |     1

同一个 aGROUP BY 指原列,ORDER BY 指别名。四行里有重复的 a 值,因为分组粒度比投影粒度更细。

窗口函数和 UNION:更窄的视野

窗口 ORDER BY 完全不看 SELECT 列表:

SELECT a, -a AS neg, row_number() OVER (ORDER BY neg) FROM nums;
-- ERROR: column "neg" does not exist

OVER (ORDER BY ...) 是独立的解析路径,只看 FROM 作用域,裸名字快速路径根本不存在。必须写成:

SELECT a, -a AS neg, row_number() OVER (ORDER BY -a) FROM nums;

同一条查询里两个 ORDER BY,两套作用域规则。

UNION 后的 ORDER BY 只允许裸名字:

(SELECT a FROM nums) UNION ALL (SELECT 9) ORDER BY a;       -- OK
(SELECT a FROM nums) UNION ALL (SELECT 9) ORDER BY -a;      -- ERROR
(SELECT a FROM nums) UNION ALL (SELECT 9) ORDER BY a COLLATE "C"; -- ERROR

集合操作没有统一的 FROM 作用域,表达式路径完全关闭。只能用裸名字引用结果列。

源码里的接缝

打开 src/backend/parser/parse_clause.c,找到 findTargetlistEntrySQL92。四十行注释,两个 if 块,一个 return

第一个块——裸名字路径:检查 ColumnRef 是否恰好只有一个名字部分且为字符串标识符。通过后遍历 SELECT 列表找匹配的 resnameGROUP BY 在这个块里有个小例外:先查 FROM 作用域,命中则跳过目标列表循环。没找到匹配别名时,这个块不返回,控制权落入下一个阶段。

第二个块——位置路径:A_Const 整数常量作为 1-based 位置索引。ORDER BY -1 在语法层面就被折叠成单个整数常量,和 ORDER BY 1 走同一条路。这个块不会落入下一阶段。

落入点——一切没被上面捕获的东西:

/* Otherwise, we have an expression, so process it per SQL99 rules. */
return findTargetlistEntrySQL99(pstate, node, tlist, exprKind);

这就是接缝。SQL-92 只在两种窄形状上成功:匹配到别名的裸标识符,或范围内的正整数位置。其余一切——包括没匹配到别名的裸标识符——都变成 SQL-99 表达式,对 FROM 作用域解析。

实操:让别名在表达式里可见

想在 ORDER BY 表达式里引用别名,标准做法是把查询包进子查询,让别名变成外层的真实列:

-- 直接写 ORDER BY x+0 会报错(x 是别名,表达式路径看不到)
SELECT * FROM (
    SELECT -a AS x FROM nums
) s
ORDER BY x + 0;

现在 xFROM 作用域里的真实列,表达式路径能找到它。这本质上就是你想让引擎替你做的事,但 SQL-99 标准不要求引擎这么做,Postgres 和 SQL Server 都明确文档化了"别名不能出现在 ORDER BY 表达式里"的限制。所以你得手动搬。

检查清单

日常写 SQL 时,记住这几条就够了:

  1. 裸列名走别名,其余走原列——ORDER BY aORDER BY -a 可能指不同的东西。
  2. 别名和原列同名同值时两条路径结果一致——这是大多数查询不出问题的原因,也是意外发生时最难排查的原因。
  3. ORM 生成的引号别名可能让排序悄悄切换路径——检查生成的 SQL,看别名是否被加了引号。
  4. COLLATE 是表达式,DESC 不是——需要排序修饰符时,先确认它会不会改变路径。
  5. 窗口 ORDER BYUNION ORDER BY 有各自的作用域限制——别指望它们能看 SELECT 列表。
  6. GROUP BYORDER BY 对同名标识符的解析顺序相反——别名遮蔽原列时,两个 clauses 可能指不同的列。

读完这篇文章,如果你面对 SELECT -a AS a FROM nums ORDER BY a COLLATE "C" 还需要停顿一下才能判断结果——那是正确的反应。说明你已经有了正确的心理模型:COLLATEa 变成表达式,走 FROM 路径,按原列排序。两条路径的接缝在 1990 年代末缝合,至今仍在默默分歧。


相关推荐