Skip to main content

第 9 章 语言(Languages)

原文:Readings in Database Systems, Fifth Edition (2015),Chapter 9: Languages,Introduced by Joe Hellerstein。原书文本采用 CC BY-NC-SA 4.0 许可;本译文按同一许可发布。

导读:Joe Hellerstein

选读论文

  • Joachim W. Schmidt. “Some High Level Language Constructs for Data of Type Relation.” ACM Transactions on Database Systems, 2(3), 1977, 247-261.
  • Arvind Arasu, Shivnath Babu, Jennifer Widom. “The CQL Continuous Query Language: Semantic Foundations and Query Execution.” The VLDB Journal, 15(2), 2006, 121-142.
  • Peter Alvaro, Neil Conway, Joseph M. Hellerstein, William R. Marczak. “Consistency Analysis in Bloom: A CALM and Collected Approach.” CIDR, 2011.

读数据库论文时,你可能会以为典型的数据库用户是数据分析师、业务决策者或 IT 人员。实践中,大多数数据库用户其实是软件工程师,他们构建由数据库支撑的应用,而这些应用会被栈上更高层的人继续使用。虽然 SQL 最初是面向非技术用户设计的,但除非是在编写数据库支撑的应用,否则人们很少会直接通过 SQL 这样的语言与数据库交互。

那么,如果数据库系统大多只是软件开发的 API,它们给程序员提供了什么?像大多数优秀软件一样,数据库系统提供了强大的抽象。其中有两个尤其突出:

  1. 事务模型给程序员提供了一个抽象:一台单进程、顺序执行、不会在任务中途失败的机器。这把程序员从一座巨大的复杂性悬崖边拉开,也就是现代计算中固有的并行性。如今一个计算机机架就有数千个核心,分布在数十台可以独立失败的机器上并行运行。然而应用程序员仍然可以若无其事地编写顺序代码,仿佛还在 1965 年,把一叠穿孔卡片装进大型机,让它们从头到尾逐个执行。

  2. 像 SQL 这样的声明式查询语言,为程序员提供了操作数据集合的抽象。众所周知,声明式语言让程序员不用思考如何访问数据项,而是专注于要返回哪些数据项。这种数据独立性还让应用程序员免受底层数据库组织变化的影响,也让数据库管理员不必介入应用的设计与维护。

这些抽象随着时间推移究竟有多有用?今天又有多有用?

  1. 作为一种编程构造,可串行化事务影响巨大。程序员很容易用 BEGINCOMMIT/ROLLBACK 把代码括起来。不幸的是,正如我们在第 6 章讨论过的,事务很昂贵,而且常常会被折中。“放松”的事务语义会打破用户眼中的顺序抽象,让应用逻辑暴露在竞争条件和/或不可预测异常的可能性之下。如果应用开发者想处理这些问题,就必须自己管理并发、故障和分布式状态的复杂性。面对缺少事务的情况,一种常见回应是追求“最终一致性” [154],这也在弱隔离一章中讨论过。但正如第 6 章所说,这仍然把所有正确性负担转移给应用开发者。在我看来,这种局面是现代软件开发中的一场重大危机。

  2. 声明式查询语言同样很成功,显然比它之前的导航式语言前进了一步;后者会导致意大利面条式代码,而每当数据库被重新组织时,这些代码都需要重写。不幸的是,查询语言与程序员通常使用的命令式语言差别很大。查询语言消费并产生简单的无序“集合类型”(集合、关系、流);编程语言通常执行有序指令,且往往操作复杂的结构化数据类型(树、哈希表等)。数据库应用程序员被迫跨越程序与数据库查询之间所谓的“阻抗失配”。自关系数据库早期以来,这一直是数据库程序员的麻烦。

数据库语言嵌入:Pascal/R

本节第一篇论文展示了处理第二个问题的经典例子:帮助命令式程序员应对阻抗失配。论文首先为一些操作做定义,而我们现在(四十多年后!)可能会把它们看作熟悉的集合类型操作:Python 中的“dictionary”类型,Java 或 Ruby 中的“map”类型,等等。随后,论文耐心地带我们考察各种语言构造的可能性和陷阱,这些构造似乎几十年来反复出现在应用中。一个核心主题是希望区分枚举(用于生成输出)和量化(用于检查性质);如果你显式表达后者,它往往可以被优化。最后,论文为关系类型提出了一个声明式、类似 SQL 的子语言,并将它嵌入 Pascal。结果相当自然,和今天一些较好的接口并不遥远。

虽然这种方法今天看来很自然,但这个主题花了几十年才获得大众关注。在此过程中,ODBC 和 JDBC 这样的数据库“连接性”API 作为 C/C++ 与 Java 的权宜之计出现:它们允许用户把查询推给 DBMS 并迭代结果,但类型系统仍然彼此分离,从 SQL 类型桥接到宿主语言类型也很不愉快。像 Pascal/R 这类思想最好的现代演化,也许是 Microsoft 的 LINQ 库;它提供嵌入语言的集合类型和函数,使应用开发者可以在多种后端数据库以及其他集合(XML 文档、电子表格等)上编写类似查询的代码。我们在第 5 章的 DryadLINQ 论文中已经看到了一点 LINQ 语法。

在 21 世纪头十年,社交媒体、在线论坛、交互式聊天、照片共享和产品目录等 Web 应用,一再基于关系数据库后端被实现和重新实现。用于 Web 编程的现代脚本语言比 Pascal 方便一些,并且通常包含不错的集合类型。在这种环境中,应用开发者最终看到了代码中的重复模式,并把它们固化为现在所谓的对象关系映射(ORM)。Ruby on Rails 起初是最有影响力的 ORM 之一,不过现在已经有许多其他选择。每种流行应用编程语言至少都有一个 ORM,而且它们在功能和理念上各有变化。感兴趣的读者可以参考 Wikipedia 的 “List of object-relational mapping software” 页面。

ORM 为 Web 程序员做了几件便利的事。首先,它们提供了语言原生的原语,用来处理很像 Pascal/R 中那样的集合。其次,它们可以让内存中语言对象的更新透明地反映到数据库支撑的状态中。它们通常提供某种语言原生语法,用来表达实体、关系、键和外键等熟悉的数据库设计概念。最后,包括 Rails 在内的一些 ORM 还提供了很好用的工具,用于追踪数据库模式如何随应用代码变化而演化(在 Rails 术语中称为 “migrations”)。

这是数据库研究社区和工业界都应该更加关注的领域:这些人正是我们的用户!ORM 和数据库之间存在一些令人惊讶、有时甚至令人不安的脱节 [19]。例如 Rails 的作者 David Heinemeier Hansson(“DHH”)是一位性格鲜明的人物,他相信“有主见的软件”(当然,是反映他自己观点的软件)。有人引用过他的说法,大意是:

我不想让我的数据库变聪明!……我认为存储过程和约束是可憎而鲁莽的连贯性破坏者。不,数据库先生,你不能拿走我的业务逻辑。你的过程式野心不会结果;你得从我冰冷的面向对象之手中撬走那套逻辑……我只想要一层聪明:我的领域模型。

这种不愿信任 DBMS 的态度,会在 Rails 应用中导致许多问题。基于 ORM 编写的应用通常很慢,ORM 本身并不会花太多力气优化查询生成方式。相反,Rails 程序员往往需要学习以“不同”的方式编程,才能鼓励 Rails 生成高效 SQL;这类似 Pascal/R 论文中的讨论,他们需要学会避免循环和逐表迭代。一个 Rails 应用的典型演化过程是:先天真地写出来,观察到性能很慢,研究它生成的 SQL 日志,然后重写应用以说服 ORM 生成“更好”的 SQL。Cheung 及其合作者最近的工作探索了这样一种想法:程序合成技术能否自动生成这些优化 [38]。这是一个有趣方向,时间会告诉我们它能自动消除多少复杂性。数据库与应用之间的分离也可能对正确性产生负面影响。例如,Bailis 最近展示了 [19],大量现有开源 Rails 应用由于在应用中(而不是数据库中)不恰当地执行约束,很容易受到完整性违规的影响。

尽管存在一些盲点,ORM 总体上仍然是数据库支撑应用可编程性的一次重要实践跃迁,也验证了早在 Pascal/R 那里就已经出现的思想。有些好思想需要时间才会流行。

流查询:CQL

我们的第二篇 CQL 论文属于另一种语言工作:它是一篇查询语言设计论文。它提出了一种新的声明式查询语言,用于流这一数据模型。论文有几个有趣之处。

首先,它是一个干净、可读、相对现代的查询语言设计范例。每隔几年,就会有一群人带着又一个数据模型和查询语言出现:例子包括对象与 OQL、XML 与 XQuery,或者 RDF 与 SPARQL。这些尝试大多从一种断言开始:某个数据模型 X “改变了一切”;随后展示一门新的查询语言,它往往看起来既熟悉,又和 SQL 奇怪地不同。CQL 是一个令人耳目一新的语言设计例子,因为它反其道而行之:它强调,如果通过正确的视角看待流式数据,实际上几乎没有什么东西发生了改变。CQL 只对 SQL 做了足够少的演化,以隔离查询“静止”表与“移动”流之间的关键差异。这让我们清晰理解,当你必须谈论流时,语义上真正不同的东西是什么;许多当下其他流式语言都比 CQL 更加临时拼凑,也更凌乱。

除了是深思熟虑的查询语言设计范例之外,这篇论文还代表了一个在数据库文献中曾受到大量关注、在实践中仍然很有吸引力的研究领域。21 世纪初第一代流式数据研究系统 [3, 120, 118, 36],无论作为开源项目,还是作为从这些系统中成长出来的各类创业公司,都没有获得重大采用。然而,近年来流查询这一主题又重新在工业界获得兴趣,SparkStreaming、Storm 和 Heron 等开源系统得到采用,Google 等公司也主张连续数据流是现代服务的新现实 [8]。我们也许还会看到流查询系统占据比当前金融服务小众领域更大的位置。

CQL 的另一个有趣之处是,流位于数据库和“事件”之间的某种中间地带。数据库存储和检索集合类型;事件系统传输和处理离散事件。一旦你把事件看作数据,事件编程和流编程就会显得非常相似。考虑到事件编程在某些领域是一种广泛使用的编程模型(例如用户界面中的 Javascript,分布式系统中的 Erlang),Javascript 这样的事件编程语言与数据流系统之间的阻抗失配应该相对较小。这个方向的一个有趣例子是 Rx(Reactive Extensions)语言,它是 LINQ 的流式扩展,让事件流编程感觉像是在编写函数式查询计划;或者用它的作者 Erik Meijer 的话说,“你的鼠标就是一个数据库” [114]。

不使用事务来编写正确应用:Bloom

第三篇关于 Bloom 的论文连接了上面许多要点;它在应用层有一个关系式状态模型,也有一种与 CQL 流相关的网络通道概念。但它的主要目标,是帮助程序员处理本章导读开头提到的第一个抽象的丧失,也就是我称为重大危机的那个抽象。现代开发者面对的一个大问题是:如果不使用事务或其他昂贵机制来控制操作顺序,你能否为自己的程序找到一个正确的分布式实现?

Bloom 对这个问题的回答,是给程序员一门“无序”的编程语言:一种不鼓励他们意外依赖顺序的语言。Bloom 的默认数据结构是关系;它的基本编程构造是可以以任意顺序运行的逻辑规则。简而言之,它是一门类似关系查询语言的通用语言。出于与 SQL 查询可以在不改变输出的情况下被优化和并行化相同的原因,简单 Bloom 程序拥有一个定义良好、独立于执行顺序的(一致)结果。这个直觉的例外,来自那些“非单调”的 Bloom 代码行,也就是测试某种会随着时间推移在真和假之间振荡的性质(例如 “NOT EXISTS x” 或 “HAVING COUNT() = x”)。这些规则对执行顺序和消息顺序敏感,需要由协调机制“保护”起来。

CALM 定理将这个概念形式化,并明确回答了上面的问题:当且仅当程序规范是单调的,你才可以为程序找到一致的、分布式的、无协调的实现 [84, 14]。Bloom 论文还说明了编译器如何在实践中使用 CALM,定位 Bloom 程序中需要协调的位置。在程序员注解的帮助下,CALM 分析也可以应用于 Storm 等系统中的数据流语言 [12]。文献 [13] 给出了这一领域理论结果的综述。

围绕避免协调,近期出现了大量相关语言工作:一些论文提出使用结合、交换、幂等的操作 [83, 142, 42];这些操作天然是单调的。另一组工作考察替代性的正确性标准,例如只确保数据库状态上的特定不变量 [20],或者使用替代性程序分析,在不实现传统读写并发控制的情况下交付可串行化结果 [137]。这个领域仍然很新;不同论文有不同模型(例如,有些包含事务边界,有些没有),而且往往并不认同“consistency”或“coordination”的定义。(CALM 把一致性定义为全局确定性结果,把协调定义为无论数据如何分区或复制都必需的消息传递 [14]。)这里需要更多清晰性和更多思想:如果程序员不能拥有事务,那么他们就需要应用开发层面的帮助。

Bloom 也是数据库研究中一个反复出现主题的例子:通用声明式语言,也就是“逻辑编程”。Datalog 是标准例子,并且在数据库研究中有漫长而有争议的历史。Datalog 是 20 世纪 80 年代数据库理论学家喜爱的主题,却遭到当时系统研究者的猛烈反弹,被认为在实践中无关紧要 [152]。较近时期,它又得到数据库和其他应用领域中一些(更年轻的)研究者关注 [74];例如,Nicira 的软件定义网络栈(后来被 VMWare 以十亿美元量级收购)就使用一种 Datalog 语言来表示网络转发状态 [97]。在使用声明式子语言访问数据库状态,到像 Bloom 这样非常激进地用声明式编程指定应用逻辑之间,存在一个光谱。时间会告诉我们,对基础设施、应用、Web 客户端和移动设备等不同上下文中的程序员来说,这条声明式与命令式的边界会如何移动。