当前位置: 首页 > news >正文

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_atid排序,然后继续做其他事情。

严格来说,如果你算上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。这不再是一个标识符。它是一个表达式:围绕列引用的单目减号。解析器根本不会尝试相同的逻辑。

相反,它会切换到表达式路径,其中它知道的唯一anums表中的列,并对输入值取反后排序。算术上的巧合是,这两个查询最终得到了相同的行顺序。相同的输出,完全不同的逻辑。如果你不相信这只是巧合,可以将 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;

这将按预期工作。DESCNULLS FIRST都是排序子句本身的一部分,而不是排序表达式的一部分。它们在解析树中位于标识符之上,因此它们从不触及标识符。解析器仍然看到一个裸的c,走快速路径,找到别名,按它排序,然后在解析出的键之上应用“降序,空值优先”。

但排序规则(Collation)就不能这么说了。

SELECT'A'::textASxFROMnumsORDERBYxCOLLATE"C";-- 错误: 列 "x" 不存在

这是一个非常糟糕的情况。COLLATE可能看起来和排序修饰符一样,但它不是。它在解析树中包裹了表达式。

括号是一个特例。

SELECT-aASaFROMnumsORDERBY(a);-- 有效,按别名排序

Postgres 在裸标识符检查之前会折叠多余的括号,所以(a)仍然是裸的a。这种不对称性以最大化混淆的方式存在着:COLLATE对人是“仍然是一个名称”,对解析器是“一个表达式”;而(a)对人是“一个表达式”,对解析器“仍然是一个名称”。你在这里同时得到了两种错误的直觉。

一元加号。+aa计算出的值相同,但它们解析成的节点不同。

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 BYORDER 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 NULLORDER BY 'a'ORDER BY TRUE。整数被用作进入非resjunk目标列表的基于 1 的位置;超出范围的值会报错“位置 %d 不在选择列表中”。第二块从不向下传递。

    1 和 -1 都会作为整数A_Const到达这里。语法中的doNegate在函数运行之前将-Iconst折叠成一个单一的整数常量,因此ORDER BY 1ORDER 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 历史。本文的其余内容是该评论没有给出的解释。这两篇文章本身都值得一读。

http://www.jsqmd.com/news/823993/

相关文章:

  • Windows Server 2022 数据中心版安装避坑指南:从ISO下载到桌面体验的完整流程
  • 告别盗版与广告:Office 2021官方纯净部署实战指南
  • Notemd Pro:基于Web技术栈的开源个人知识管理应用深度解析
  • AMD Vitis嵌入式开发实战:从异构计算到FPGA加速全流程解析
  • 3步掌握智能票务助手:告别手动抢票的终极方案
  • 告别手动填坑:用SSC工具+Excel快速搞定LAN9252 EtherCAT从站XML配置(附64点IO实例)
  • 面试鸭:一站式面试题库解决方案,助你轻松备战技术面试
  • 实测taotoken多模型聚合端点的响应延迟与稳定性表现
  • 服务网格流量管理:智能控制微服务间通信
  • 如何快速清理Windows驱动存储:Driver Store Explorer完整使用指南
  • 从BST到RBT:深入解析三大树结构的性能抉择与应用场景
  • AI IDE CLI:为AI编程助手打造的轻量级本地开发环境
  • 用Python复现数学建模国赛B题‘穿越沙漠’:手把手教你写最优路径规划算法
  • AI驱动数字营销平台架构解析:从工作流引擎到品牌个性化
  • 3D模型格式转换终极方案:用stltostp轻松实现STL到STEP的专业转换
  • 体验Taotoken Token Plan套餐为长期每日大赛带来的成本优势
  • 猫抓插件:告别网页下载限制,一键获取所有在线媒体资源
  • 不止Keil5:VSCode+GCC也能玩转GD32单片机?手把手教你搭建轻量级开发环境
  • 从零到自动化:手把手教你用nRF Connect搭建个人BLE设备测试流水线
  • SQL 中 OR 与 UNION ALL选择指南
  • 防火墙知识--安全策略故障排查
  • 【NI-DAQmx实战】巧用DAQ助手,三步构建高效数据采集任务
  • 伊的家护肤老师是否可靠?专业资质与团队规模奠定可靠基础 - 品牌企业推荐师(官方)
  • 电路设计效率革命:Draw.io电子工程库的专业绘图方案
  • 表空间(Tablespace)管理
  • 5分钟快速上手GSE:魔兽世界智能技能循环终极指南
  • 如何评估机器翻译质量?COMET框架的实战指南
  • 从PLINK到CMplot:三步绘制高颜值SNP密度图
  • TI毫米波雷达IWR1642原始数据采集避坑指南:DCA1000配置、IQ顺序与帧大小限制
  • 首驱电动车和小牛哪个好?售后体验和智能化全面怎么比 - 品牌企业推荐师(官方)