DeepSeek总结的欢迎来到 ORDER BY 丛林
来源 https://boringsql.com/posts/order-by-jungle/
欢迎来到 ORDER BY 丛林
作者:Radim Marek
日期:2026-05-15
阅读时间:11 分钟
目录
SQL 很有趣,一点也不枯燥。Markus Winand 的最新文章《Order by 已经走过了漫长的道路》让我开启了一段相当精彩的旅程。
首先,创建一个名为nums的表,包含一个整数列和四行数据:
CREATETABLEnums(aint);INSERTINTOnumsVALUES(0),(1),(2),(3);试着猜猜下面两个查询返回什么结果。
SELECT-aASaFROMnumsORDERBYa;SELECT-aASaFROMnumsORDERBY-a;我们大多数人会猜测它们返回相同的行,但顺序不同。实际的答案是,它们以完全相同的顺序返回完全相同的行。按照同样的逻辑,你可能会期望
SELECTaAScFROMnumsORDERBY-c;做完全相同的事情。但它没有。它会报错说列 “c” 不存在,尽管别名就在语句中。欢迎来到 ORDER BY 丛林。
名称和表达式不是一回事
如果你问大多数开发人员 ORDER BY 是如何工作的,他们会说“你在那里放一个列名,它就会对行进行排序”。在 99% 的查询中,这正是发生的事情。人们根据created_at或id排序,然后继续做其他事情。
严格来说,如果你算上ORDER BY 1,其实有三种。位置引用是另一回事,不在本文讨论范围之内。但ORDER BY接受两种不同类型的东西:
SELECTcreated_at,user_idFROMeventsORDERBYcreated_at;SELECTcreated_at,user_idFROMeventsORDERBYdate(created_at);两者都感觉很自然。但没人告诉你的是,它们在解析器中走的是完全不同的代码路径。不同的作用域规则,不同的查找方式,不同的错误消息。第一个查看你的 SELECT 列表。第二个查看你的 FROM 子句。它们从不查看同一个地方。
相同的结果,两种不同的排序
再看第一个查询。
SELECT-aASaFROMnumsORDERBYa;你写了ORDER BY a。一个裸标识符,没有任何修饰。Postgres 走名称路径。它在 SELECT 列表中扫描名为a的东西,找到别名为-a AS a的列,并按它的输出值排序。取反后的值是 -3, -2, -1, 0,升序是 -3, -2, -1, 0。这就是输出的结果。
现在看它的孪生兄弟。
SELECT-aASaFROMnumsORDERBY-a;你写了ORDER BY -a。这不再是一个标识符。它是一个表达式:围绕列引用的单目减号。解析器根本不会尝试相同的逻辑。
相反,它会切换到表达式路径,其中它知道的唯一a是nums表中的列,并对输入值取反后排序。算术上的巧合是,这两个查询最终得到了相同的行顺序。相同的输出,完全不同的逻辑。如果你不相信这只是巧合,可以将 SELECT 列表中的取反去掉,但保留 ORDER BY 中的:
SELECTaAScFROMnumsORDERBY-a;c---3210(4rows)ORDER BY -a是一个表达式,所以它按-input_a升序排序,也就是input_a降序。别名c从未被使用。结果与c是什么毫无关系。
而ORDER BY -c现在就很明显了。-c是一个表达式,所以解析器在 FROM 中查找列c,找不到,然后报错。别名存在,但在这条代码路径无法看到的作用域中。
标识符之上,或环绕其周围
一旦规则清晰了(裸标识符命中 SELECT 列表,其他任何东西命中表),其余的惊喜也就随之而来。
SELECT'hello'ASxFROMnumsORDERBYx::text;-- 错误: 列 "x" 不存在类型转换算作表达式并将查找推送到表,这大概并不令人惊讶。
令人惊讶的可能是这个:
SELECTaAScFROMnumsORDERBYcDESCNULLSFIRST;这将按预期工作。DESC和NULLS FIRST都是排序子句本身的一部分,而不是排序表达式的一部分。它们在解析树中位于标识符之上,因此它们从不触及标识符。解析器仍然看到一个裸的c,走快速路径,找到别名,按它排序,然后在解析出的键之上应用“降序,空值优先”。
但排序规则(Collation)就不能这么说了。
SELECT'A'::textASxFROMnumsORDERBYxCOLLATE"C";-- 错误: 列 "x" 不存在这是一个非常糟糕的情况。COLLATE可能看起来和排序修饰符一样,但它不是。它在解析树中包裹了表达式。
括号是一个特例。
SELECT-aASaFROMnumsORDERBY(a);-- 有效,按别名排序Postgres 在裸标识符检查之前会折叠多余的括号,所以(a)仍然是裸的a。这种不对称性以最大化混淆的方式存在着:COLLATE对人是“仍然是一个名称”,对解析器是“一个表达式”;而(a)对人是“一个表达式”,对解析器“仍然是一个名称”。你在这里同时得到了两种错误的直觉。
一元加号。+a和a计算出的值相同,但它们解析成的节点不同。
SELECT-aASaFROMnumsORDERBYa;SELECT-aASaFROMnumsORDERBY+a;一个你甚至不会想到的加号,会改变哪些行以什么顺序输出。
解析器将列引用存储为名称部分的列表:未限定时为一部分,添加表或模式后为两个或更多部分。快速路径仅在列表长度为一时触发。
最后,模式和表限定的引用。ORDER BY nums.a看起来像一个标识符,但它不是。
SELECT-aASaFROMnumsORDERBYa;SELECT-aASaFROMnumsORDERBYnums.a;第一个查询输出 -3, -2, -1, 0,第二个查询输出 0, -1, -2, -3。结果不同!
不是你想象的那个名字的别名
有一个问题曾花掉我一个下午的时间。一旦 ORM 或生成的视图为你声明了别名,就很容易遇到。SQLAlchemy、Hibernate、jOOQ 以及大多数代码生成器会引用任何不是纯小写的标识符。两个查询,除了一个中的别名被引号括起来之外,完全相同。两个不同的结果集。
SELECT-aASAFROMnumsORDERBYa;-- 按别名排序 (-3,-2,-1,0)SELECT-aAS"A"FROMnumsORDERBYa;-- 按输入排序 (0,-1,-2,-3)裸标识符检查使用strcmp比较名称。未引用的A折叠为小写a并匹配。引用的"A"保留大小写,保持为A,并且与 ORDER BY 中的小写a不匹配。查找失败,解析器回退到表达式路径,表达式路径在nums中找到列a,查询成功运行,但执行的操作与你的意图不同。
GROUP BY 首先检查相反的作用域
GROUP BY和ORDER BY都接受裸标识符,并且两者都可以通过两种方式解析它:到表列或到 SELECT 列表别名。区别在于它们检查的顺序:
ORDER BY a首先查看 SELECT 列表,然后查看表。GROUP BY a首先查看表,然后查看 SELECT 列表。
对于大多数查询,这无关紧要。两个子句最终会选择相同的东西,因为没有东西被遮蔽。当别名与基列同名但值不同时,意外就会发生:
SELECTa/2ASa,count(*)FROMnumsGROUPBYaORDERBYa;现在两个子句对a的含义产生了分歧。GROUP BY a选择输入列(四个不同的值,四个组,每行一组)。ORDER BY a选择别名,即a/2。结果有四行,因为分组是基于比投影更细粒度的键:
a|count---+-------0|10|11|11|1两行a/2 = 0(来自输入 0 和 1),两行a/2 = 1(来自输入 2 和 3)。重复是真实的。同一个标识符在同一个查询的两个相邻子句中表示两个不同的列。
窗口 ORDER BY 甚至不假装
这一点会让人困扰,因为它看起来不像一个不同的子句。
SELECTa,-aASneg,row_number()OVER(ORDERBYneg)FROMnums;-- 错误: 列 "neg" 不存在OVER (ORDER BY ...)完全是一个不同的解析路径。它根本不检查目标列表,只检查 FROM 作用域。裸名称快速路径在这里根本不存在。
SELECTa,-aASneg,row_number()OVER(ORDERBY-a)FROMnums;-- 这个有效同一个查询中的两个 ORDER BY 子句,两种不同的作用域规则。
UNION ORDER BY 仅限名称
当 ORDER BY 跟在 UNION 后面时,两条路径都没有完全打开。
-- 有效(SELECTaFROMnums)UNIONALL(SELECT9)ORDERBYa;-- 错误(SELECTaFROMnums)UNIONALL(SELECT9)ORDERBY-a;-- 错误(SELECTaFROMnums)UNIONALL(SELECT9)ORDERBYaCOLLATE"C";错误信息异常有用:
只能使用结果列名,不能使用表达式或函数。提示:将表达式/函数添加到每个 SELECT 中,或者将 UNION 移到 FROM 子句中。
集合操作没有单一的 FROM 作用域可以回退,因此表达式路径完全关闭。要么是裸名称,要么什么都没有。
源代码中的接缝
完全披露:在我让 Claude Code 帮助我追踪实际的解析树之前,这一节我写错了三次。整晚沉迷于 ORDER BY 而睡眠不足是另一种合理的解释。
打开src/backend/parser/parse_clause.c并找到findTargetlistEntrySQL92。它有四十行注释,两个if块,以及一个最终的return。SQL92 的两种解析规则首先被尝试;SQL99 是后备方案。
第一块:裸名称路径。入口是一个恰好包含一个名称部分的 ColumnRef 节点,并且该部分必须是一个字符串标识符(不是
*,*也是 ColumnRef 但带有 A_Star 字段)。如果节点通过,该函数会遍历目标列表,寻找一个非resjunk的、其resname等于该标识符的条目。循环会越过第一个匹配项以检测歧义:相同的表达式没问题(这就是SELECT a, a FROM nums ORDER BY a有效的原因),不同的表达式会报错。如果是唯一匹配,则返回。如果循环没有找到任何东西,该块不会返回。控制权向下传递。这就是前面文章中引用别名意外背后的情况:
AS "A"存储resname = "A",ORDER BY a查找resname = "a",strcmp失败,函数继续执行,好像没有应用 SQL92 快速路径一样。GROUP BY是这个块内部的一个小例外。名称首先会针对 FROM 作用域进行测试,如果在那里命中,则跳过目标列表循环。这就是GROUP BY最终优先选择输入列的方式。第二块:位置路径。入口是
IsA(node, A_Const)。非整数常量会立即报错(“ORDER BY 中的非整数常量 …”),这捕获了ORDER BY NULL、ORDER BY 'a'、ORDER BY TRUE。整数被用作进入非resjunk目标列表的基于 1 的位置;超出范围的值会报错“位置 %d 不在选择列表中”。第二块从不向下传递。1 和 -1 都会作为整数
A_Const到达这里。语法中的doNegate在函数运行之前将-Iconst折叠成一个单一的整数常量,因此ORDER BY 1和ORDER BY -1走相同的代码,只有整数值(以及位置查找的结果)不同。向下传递。任何未被上述情况捕获的内容都会到达最后一行:
/* * Otherwise, we have an expression, so process it per SQL99 rules. */returnfindTargetlistEntrySQL99(pstate,node,tlist,exprKind);这就是接缝。SQL92 在两种狭窄的形状下成功:一个具有匹配别名的裸标识符,或者一个范围内的正整数。其他一切,包括别名查找未找到任何东西的裸标识符,都变成了针对 FROM 解析的 SQL99 表达式。
一个有用的变通方法
如果你想在 ORDER BY 的表达式内部使用别名,可移植的技巧是将查询包装在一个子查询中:
SELECT*FROM(SELECT-aASxFROMnums)sORDERBYx+0;现在x是外部查询的 FROM 作用域中的一个真实列。表达式路径找到了它。接缝被移开了。
从概念上讲,这就是当你直接写ORDER BY x + 0时,你希望引擎为你做的事情。然而,SQL-99 标准实际上并没有要求这样做,而且 Postgres(以及 SQL Server)明确记录了 ORDER BY 表达式内部不支持别名。所以你需要手动完成。
枯燥的要点
大多数时候,这些都无关紧要。你按你刚刚选择的列排序,别名和输入列具有相同的名称和相同的值,两条解析器路径给出相同的结果。接缝是不可见的。
当别名和输入列在表达式、值、大小写或任何包裹在标识符周围的东西上不一致时,解析器会根据一条比大多数在职程序员都年长的规则,静默地选择一个或另一个。
存在两个解析器。裸名称路径是 SQL-92,表达式路径是 SQL-99,它们是在 1990 年代末拼接在一起的。它们仍然对你的标识符存在于哪个作用域存在分歧,而知道触发了哪一个就能告诉你该使用哪个作用域。
如果在阅读完这篇文章后,你仍然需要停下来思考一分钟才能预测
SELECT-aASaFROMnumsORDERBYaCOLLATE"C";会做什么,那是正确的反应。这意味着你已经有了心智模型。
开头的谜题查询来自 Jamie Brandon 在 Lobsters 帖子上的评论,该帖子讨论了 Markus Winand 在 modern-sql.com 上发表的 ORDER BY 历史。本文的其余内容是该评论没有给出的解释。这两篇文章本身都值得一读。
