在现代关系查询语言中我想要的特性
摘要
这篇文章针对现代关系查询语言,讨论了其期望的功能,批评了SQL的不足,并基于函数式编程、更好的语法和用户定义类型提出了改进建议。
<p><a href="https://lobste.rs/s/p5lkok/things_i_want_modern_relational_query">评论</a></p>
查看缓存全文
缓存时间: 2026/08/19 06:28
# 我对现代关系查询语言的期待 - sporks.space
来源:https://sporks.space/2026/08/19/things-i-want-in-a-modern-relational-query-language/
*这是一篇多年前的旧草稿。最近关于 Acadia(https://acadia.engineering/blog/rethinking-database-programming)等新型查询语言的讨论促使我重新审视、修订并发布此文。*
我认为 NoSQL 兴起的最大原因之一在于:SQL 虽因背后的设计思想而成为强大语言,但其常见实现方式却往往笨拙且古老。一种从 SQL 中学习的语言能让关系型数据对程序员更友好。我将尝试结合现实场景中遇到的问题,探讨更优的查询语言如何提供帮助。非常期待能听到更多改进方案。
就我个人背景而言,主要使用 MySQL 和 Db2 开发关系数据库,但也曾较为深入地使用 SQLite、SQL Server、Oracle 和 Postgres(按熟悉程度递减)。
## 更优的语法
我个人对美学并不苛求,但许多程序员在意。程序员就像幼儿,只想要通心粉奶酪而拒绝西兰花。基于 PL/I 的语法是 1970 年代 IBM 的选择,在今天恐怕难以流行。基于大众需求,这类语言很可能在语法上借鉴 C 或 Python 的风格,或许还会融入 ML 或 Prolog 的影响(正如 Rust 所示)。
更好的语法应带来更强大的解析器。我尤其反感 MySQL 的解析器——除非遇到像 `DELIMITER` 这类语法上的荒谬错误,它从不明确指出问题所在。不过传统实现中确实存在更优的 SQL 解析器——Oracle 的错误提示就出奇地清晰,会明确告知预期内容。
文中示例仅为演示;我不执着于任何特定语法,也不强制规定必须采用何种风格。这些示例的灵感很可能来源于 F#(ML 家族)、Erlang(类 Prolog)和 Elixir(融合 Erlang 与 Ruby 特性)。
## 对函数式编程友好的语言
SQL 的 4GL 特性——通过*描述*数据获取方式而非手动循环处理——是其最强大的武器。这非常接近诸多函数式编程范式,如惰性求值——向 Haskell 致敬。遗憾的是,大多数 SQL 方言的标准库在这方面表现薄弱,因其为 1980 年代的面向过程程序而优化。多数 SQL 方言最终都支持了存储过程,这本质上是……过程式的;这与 SQL 的声明式特性背道而驰。这种设计最终反映在用户的 SQL 代码中——他们模仿语言和标准库提供的简易风格,导致大量可变状态处理(游标……)和过程式逻辑而非函数式设计。默认设置至关重要。
## 更透明的查询规划器
虽然 4GL 的优势在于编译器和优化器能高效处理大多数代码,但用户很容易无意中写出性能低下的查询。除非你是 SQL 优化专家,否则规划器可能晦涩难懂。(再次点名 MySQL 的"explain"工具何其糟糕。)尽管这不严格属于编程语言理论范畴,但它是当前 SQL 实现的弱点,也是计算机科学家们已深入研究的领域。
## 更强的用户自定义类型
虽然部分关系数据库提供了*域*概念来指定用户自定义数据类型(这是 SQL 规范的可选部分),但其功能往往受限(通常只是范围或校验的语法糖)。Postgres 似乎是唯一支持该特性的系统(https://www.postgresql.org/docs/current/sql-createdomain.html);Oracle 最近才获得支持(虽然看起来比 Postgres 更灵活 https://blogs.oracle.com/coretec/less-coding-with-sql-domains-in-23c)。遗憾的是,我尚未深入实践两者的具体实现。然而,Codd 的**关系模型**(关系数据库的奠基著作)涵盖了域的概念。考虑到 Postgres 源自 Ingres(基于 QUEL 语言,而 QUEL 比 SQL 更贴近 Codd 对关系数据库的构想),Postgres 遵循这一设计也合情合理。
## 和类型、可辨识联合与模式匹配
展示现代函数式编程技术在此领域应用的典范之一,是这个返回栈帧信息的函数(https://www.ibm.com/support/knowledgecenter/en/ssw_ibm_i_73/rzajq/rzajqudfstackinfo.htm)。背景说明:此处提及的 IBM i 操作系统在"服务(https://www.ibm.com/support/pages/ibm-i-services-sql)"框架下提供了众多 SQL 函数用于系统管理。这对转型为系统管理员的 DBA 在调试时非常有用,但实现方式颇为笨拙:存在本质上互斥的"列组",由此产生大量可空字段,以及实际代表枚举的字符串字段。
部分问题源于糟糕的模式设计(或许也因必须在*单个*表中返回所有数据——支持返回多个表也将是个有趣方向);字符串枚举可通过外键约束指向枚举表来解决。但其余问题确实源于语言表达能力的局限。
基于这个思路,我尝试提出更优示例,使查询更简洁且不易出错:
```
// 为简化大幅省略内容;例如位移或其他枚举情况,以及临时定义枚举(也可在类型外声明)
// 每种帧类型虽相似但不等同,具有不同语义或限定条件
type MachineInterfaceInfo =
{
ActivationGroup: long;
ASP: long;
Library: string;
}
// 供缺乏上下文的读者了解:IBM i 支持多种程序模型:
// - Java 程序:运行时为系统提供特殊洞见
// - OPM 程序:旧版托管运行时程序 ABI
// - ILE 程序:新版托管运行时程序 ABI
// - AIX 程序:通过系统调用模拟实现
// - LIC:IBM i 内核
// 系统可为所有这些程序类型生成堆栈跟踪;某些程序调用栈可能包含每种类型的帧条目
type FrameType =
// 继承另一记录类型的字段
| ILE { MachineInterfaceInfo | ServiceProgram: string; Module: string; }
| OPM { MachineInterfaceInfo | Program: string; }
| AIX { Bitness: enum(32 | 64); LibArchive: Option(string); Module: string, Syscall: bool; }
| Java { MethodType: enum(DirectExecution | Glue | Interp | JIT | MMI); ClassName: string; Signature: Option(string); }
table Frame =
{
ThreadID: long;
FrameType: FrameType;
Function: Option(string);
}
function StackInfo(JobID: string) : Frame;
// 使用模式匹配进行筛选的类 SQL 查询
select Function from StackInfo("1234/JOB/5678") where AIX { Bitness: 64 } = FrameType;
// 此查询将返回 ILE 和 OPM 类型的 FrameType
select Function from StackInfo("1234/JOB/5678") where MachineInterfaceInfo { Library: "QSYS" } = FrameType;
select Function from StackInfo("1234/JOB/5678") where AIX { LibArchive: "libc.a" } = FrameType;
select Function from StackInfo("1234/JOB/5678") where AIX { LibArchive: None } = FrameType;
// 使用模式匹配打印信息的函数
function FrameFullySpecifiedProgramName(frame : Frame) : string =
match frame.FrameInfo with
| OPM { Program: program } -> program
| ILE { ServiceProgram: srvpgm, Module: module } -> "#{srvpgm}/#{module}"
| AIX { LibArchive: None, Module: module } -> module
| AIX { LibArchive: lib, Module: module } -> "#{lib}(#{module})"
| Java { ClassName: class } -> class
// 必须匹配所有可能类型,或用 _ 丢弃
| _ -> "?"
// 使用模式匹配重载与解构的函数
function FrameJavaFunctionDef(frame : Frame { Java { Signature: None } = .FrameInfo }) : string =
"#{frame.Function}()"
function FrameJavaFunctionDef(frame : Frame { Java { Signature: signature } = .FrameInfo }) : string =
"#{frame.Function}(#{signature})"
// 调用非 Java 帧时将报错,因无匹配模式
```
若能将互斥列集合合并,可视化呈现也会容易得多。若能将它们转换为大列中的子列按行展示,或按类型显示不同字符串,便无需频繁左右滚动。
## 支持多类型匹配的外键
假设我有"软件"、"版本"和"下载"(类似 WEMI 的层级结构)三张表,每张都可能有图片,因此有"图片"表。(因图片本身含元数据,它们是表而非上述表的字段。)通常,我们需要为每种关系创建多对多关联表,如"软件图片"、"版本图片"等。这似乎是无谓的重复,若能采用多对多表并在外键上实现可辨识联合,将更为高效:
```
table ObjectPictures =
{
// 外键类型默认与其关联对象类型相同
PictureID: key relates to (Picture.PictureID);
ObjectID: key relates to (Software.SoftwareID | Version.VersionID | Download.DownloadID);
}
insert into ObjectPictures (PictureID, ObjectID) values (0x1234, DownloadID { 0x1234 });
```
相似文章
重新思考数据库编程
本文探讨了新的视角,挑战了数据库编程中的传统方法,并提出了提高效率和设计的创新建议。
设计查询系统
Arya Dradjica 撰写的一篇详细博客文章,描述了为 Rust 编译器 Krabby 设计查询系统的过程。文章解释了为什么基于拉取的架构优于基于推送的架构,并概述了期望的功能特性。
为非技术分析师设计自定义查询语言
作者详细介绍了为非技术分析师设计一种自定义查询语言的过程,用于过滤车辆维护数据,并概述了用户需求、数据模式以及具体用例。
查询语言中的求值顺序与非终止性
一篇博客文章,讨论了类似λFS的函数式关系查询语言中的求值顺序与非终止性,并引用了在FLOPS 2026上发表的关于有限函数式编程的论文。
反对基于查询的编译器
一篇技术博客文章批评了基于查询的编译器,认为其有效性受限于源语言的依赖结构,尤其是雪崩效应——变更可能广泛传播,使得增量更新往往和完全重建一样昂贵。