我希望现代关系查询语言具备的那些东西
文章摘要
这篇文章是作者 cb 压了好几年的旧草稿,因为最近关于 Acadia 等新查询语言的讨论重新翻出来修订发表。他的核心论点开门见山:NoSQL 之所以兴起,很大一部分原因在于——SQL 背后的思想本身很强大,但它的实现方式往往笨拙而陈旧。一门从 SQL 汲取教训的语言,可以让关系型数据对程序员来说好用得多。他自称的背景是 MySQL 和 Db2 为主,SQLite、SQL Server、Oracle、Postgres 也都在真实项目里用到过(熟悉度递减)。
第一件事是语法。作者说自己对美学并不挑剔,但很多人挑剔——「程序员就像小孩,只想吃通心粉不想吃西兰花」。SQL 的语法源自 PL/I,那是 1970 年代 IBM 的选择,放到今天恐怕过不了关。他猜想一门新语言多半会带上 C 或 Python 的观感,或许再掺一些 ML 和 Prolog 的影响(就像 Rust 那样)。与语法一同该改进的是解析器:他特别厌恶 MySQL 的解析器,因为它从不告诉你问题出在哪里、是什么,除非是 DELIMITER 那种语法荒诞剧;相比之下 Oracle 在报错时会告诉你它期待什么,做得出奇地好。文中的示例语法他强调只是示意,灵感来自 F#、Erlang 和 Elixir。
第二件是让函数式风格真正被支持。SQL 最强大的武器正是它的第四代语言特质——你描述想要什么数据,而不是手工循环去取,这与惰性求值等函数式范式非常接近。但绝大多数 SQL 方言的标准库在这方面相当贫瘠,是为 1980 年代的过程式程序优化的;最后各家都加上了存储过程,而存储过程本质上是过程式的,与 SQL 的声明式本性背道而驰。结果反映在用户代码里:人们模仿语言和标准库让他们最容易写的风格,于是大量代码在摆弄可变状态(游标之类)和过程,而不是函数。作者的结论是一句朴素的道理:默认值很重要。
第三件是不那么不透明的查询规划器。4GL 的强大依赖于编译器和优化器,但你很容易不小心写出一个昂贵得多的查询,而规划器的输出对非 SQL 优化专家来说往往像天书(他再次点名 MySQL 的 EXPLAIN 工具有多糟)。这一点虽然不严格属于编程语言理论范畴,但确实是现有实现的短板,而计算机科学界对此其实已经积累了很多认识。
第四件是更好的用户自定义类型。一些数据库提供 domain 概念(也是 SQL 规范的可选部分)用于定义用户自定义数据类型,但能力通常很有限,多半只是范围或检查约束的语法糖。他发现 Postgres 大概是唯一像样支持的,Oracle 是最近才加上(看起来可能比 Postgres 更灵活)。他指出 domain 是 Codd 的《The Relational Model》里就讲过的东西——那是关系数据库的奠基文本;考虑到 Postgres 的血统来自 Ingres、Ingres 又基于 QUEL、而 QUEL 比 SQL 更接近 Codd 的原始构想,Postgres 走到这一步是说得通的。
第五件也是全文篇幅最长的一节:和类型、可辨识联合与模式匹配。他举的例子是 IBM i 系统里一个返回调用栈帧信息的 SQL 函数(IBM i 在「Services」这一伞形概念下提供了大量系统管理用的 SQL 函数)。这个函数对于身兼系统管理员的 DBA 在排障时非常有用,却也非常笨拙:本质上存在若干组互斥的列,因此产生大量可空字段,还有一堆实际上是枚举却用字符串表示的列。部分问题是纯粹的模式设计问题(也可能是因为结果必须放进单张表里——他顺带提到「能返回多张表」也是个有趣的方向),字符串枚举可以用外键指向一张充当枚举的表来修;但另一部分确实源于实现语言的表达力不足。
他随后给出了一段示意代码:先定义一个共享字段的记录类型 MachineInterfaceInfo,然后把 FrameType 定义成一个和类型,包含 ILE、OPM、AIX、Java 等几个分支——分别对应 IBM i 支持的多种程序模型(Java 程序、旧的 OPM 程序 ABI、新的 ILE 程序 ABI、通过系统调用模拟运行的 AIX 程序、以及内核 LIC),而一次调用栈里可能同时出现每种类型的栈帧。这个和类型支持从别的记录类型继承字段、内联定义枚举(如 32/64 位)、以及 Option 类型表示可空。有了它,查询就可以直接在 WHERE 子句里做模式匹配:筛出 64 位的 AIX 帧、筛出库名为 QSYS 的机器接口帧(会同时返回 ILE 和 OPM)、筛出 LibArchive 为某值或为 None 的帧。函数体内同样能用 match 表达式按分支解构并格式化程序全名,而且必须穷尽所有可能分支,否则要显式用通配符丢弃。他还展示了基于模式匹配的函数重载:两个同名函数分别匹配「Java 帧且签名为空」和「Java 帧且签名存在」,用非 Java 帧调用会直接报错,因为没有模式能匹配。作者补了一个很实际的好处:把互斥的列组折叠起来之后,可视化也会容易得多——不用再左右横向滚动,可以把它们做成大列里按行显示的子列,或者按类型用不同方式渲染字符串。
最后一件是能匹配多种类型的外键。他举了自己博客上写过的一个 WEMI 式层级:Software、Version、Download 三张表,每张都可能关联图片,而图片因为自带元数据所以是一张 Picture 表而非一个列。常规做法是为每种关系建一张多对多表——SoftwarePicture、VersionPicture 等等——他认为这是毫无意义的重复。他设想的替代方案是让多对多表的外键本身就是一个可辨识联合:ObjectID 可以关联到 Software.SoftwareID、Version.VersionID 或 Download.DownloadID 中的任意一个,插入时用构造器标明具体是哪一种,查询时既可以按类型解构取出,也可以按带类型的值精确过滤。
HN 评论精华
这条帖子拿到 115 分,但只有 13 条评论,讨论规模远小于分数所暗示的热度。更值得注意的是讨论的走向:几乎没有人逐条评价作者提出的具体特性(和类型、模式匹配、多态外键这些),而是迅速集体转向了一个更宏观也更悲观的元问题——在 LLM 时代,改良 SQL 语法这件事还有没有意义。此外原文博客自己的评论区反而出现了几条更对口的技术回应。
- scythmic_waves 的评论被顶到最高,也定下了整场讨论的基调。他先推荐了 scattered-thoughts 那篇经典的《against-sql》(结尾同样附了一份愿望清单,与本文最相似),然后给出自己的判断:SQL 还会统治很久,因为替换它的工程量因数据库本身的固有复杂性而极其巨大。而「LLM 让情况更糟,因为它们特别擅长把散文翻译成 SQL。既然 SQL 对程序员有多烦人已经不那么重要了,SQL 会越来越像汇编——一种主要由计算机来写的东西,因为人类直接对付它太复杂。」他最后点出这里的深刻反讽:SQL 当初的设计初衷恰恰是要读起来像散文,也就是要让人类容易使用。
- a2ff6eeb0 表达了同样的立场,但更不留情面:「这个话题放在十年前会很有意思,但今天 AI 全都会 SQL,我自己已经很久没手写过了。」他还给出了一条实际的技术理由:既然训练数据的数量看起来主导了 AI 的表现,而 AI 目前又不能把使用新工具的经验内化,那么偏离训练集就是个坏主意。他的结尾类比很尖锐:「纠结一门语言的语法和语义,感觉有点像在争论汇编该用 AT&T 还是 Intel 语法。」
- tmoertel 给出了最务实的一条反对意见,角度是社会性而非技术性:替代查询语言的问题在于,那些最懂如何写查询、最懂自家业务底层关系域的人,全都是 SQL 专家。引入别的东西,意味着你最天然的用户群必须离开一个他们已经用得很好的工具,这是一桩很难做成的生意。「所以在终极查询语言被造出来之前,我要带管道语法的 SQL(SQL with pipes)就行——这好卖,而且足以消灭我对 SQL 90% 的抱怨。」
- esafak 一句话点出了执行层面的死结:数据库工程师必须自己成为他们想看到的改变,去为新查询语言添加支持。
- BrenBarn 贡献了唯一一条真正提出新方向的评论:他想要的查询语言根本不该是一门语言,而应该是某种可以用机器可读格式传给引擎的组件规范。「SQL 最怪的地方在于它必须用 SQL 写。」如果引擎接受的是查询的 JSON 表示(类似 AST,稍作归约),那么每种编程语言都能用自己最地道的语法去构造查询。他指出现在 ORM 干的就是这件事,但异常痛苦,因为 ORM 相当于一个转译器,而目标语言又自由散漫又混乱——「所有 ORM 都只是在拼字符串」。如果原生查询格式本身更结构化,面向程序员的那一层就能玩出多得多的花样。
- mikewarot 提出了一个和原文完全不同、但他惦记了 15 年的诉求:实时 SQL——允许一个查询变成对数据库的订阅,之后所有更新以增量形式推送给监听中的客户端。他说自己用 SQL 那些年,无数次是把同一个查询反复跑来跑去,只为拿到并处理那点差异,「从一开始就那样工作岂不是高效得多?」他附上了自己 2011 年写的 livesql.org。这条引出了整场讨论里信息密度最高的一串回复:mike_hearn 指出 Oracle 早有此功能,叫「continuous query notification」,可以让驱动在结果变化时回调、通知存储过程或投递到消息队列,通知里带增量信息;但主要限制是能被实时监控的查询只是全部查询的一个子集,它更像是「用 SQL 选出要监视的数据库单元格」,而不是把变更沿任意查询计划传播(比如它处理不了
SELECT COUNT(*))。DenisM 补充 Snowflake 有能建在视图上的 STREAM 和 Dynamic Tables,SQL Server 有 Query Notification,也可以读 Debezium 等 CDC——但那更接近表变更而非查询结果变更。gregw2 再补 BigQuery 的 continuous queries,以及 Postgres 可以用触发器加 NOTIFY 把变更推下游,他的感慨是「大多数数据库在这个功能上都远落后于 Oracle,而它实在太有用了」。 - 若干替代方案被简短抛出:mcc1ane 贴了 EdgeDB/Gel 那篇《we can do better than SQL》,bri-holt 推荐了专为 LLM 设计的极致紧凑关系查询语言 memelang。justanothersnek 说自己 SQL 和 dataframe API 库并用,算是两全其美:SQL 负责高层的大规模聚合,dataframe 库负责那些写成 SQL 会极其冗长或难以调试的部分。
- bastawhiz 的评论和内容无关但值得一提,是一条关于可读性的元吐槽:代码块没有语法高亮他能忍,代码块自动换行他也能忍,但两者加上长注释同时出现,就彻底变成噪声了——不再有任何有用的视觉信号来指示该怎么读,在手机上这些代码块根本无法解析。
- 补充一点 HN 之外的信息:原文博客的评论区反而出现了更对口的技术回应。James K. Lowden 说他为这个问题纠结了 20 年,还在一次 Ingres 大会上做过演讲,他的答案叫 coddb——语法是 Scheme,表是 Scheme 变量,关系运算符是 Scheme 函数,没有查询优化器,取而代之的是程序员拥有完全控制权和一份契约:每个运算符都有基于输入的确定性能。「这让数据库编程去神秘化,把它拉回到跟……编程同一水平线上。」Barney Keene 说他正在做一个叫 Rad 的关系数据库,它对外暴露的不是 SQL 而是一个类似 LLVM 的 IR,让你可以用任何语言去「前端化」它——ORM 不必再拼字符串,而是操作一个接近逻辑查询计划的节点图。此外 Julian Hyde 指出 Morel 已经实现了「能匹配多种类型的外键」以及作者提到的大部分特性,AgentM 推荐了 Project:M36,还有人建议作者看看 Datalog。