(王垠 yinwang.org 版权所有,未经许可,请勿转载)
在之前的一些博文里(比如这篇),我多次提到过关系式数据库和 SQL 的问题。我觉得它们是制造了问题又自己来解决,而且永远没法解决好。可是由于时间原因,一直没有来得及解释我的观点,以至于很多人不理解我在说什么,还以为是信口开河。所以现在有了点闲暇时间,我就把这里面的细节稍微说一下。也许你会发现,得出这些结论所需要的背景知识,比你想象的多得多。
SQL 与 Prolog 的渊源
在我批评 SQL 带来的问题时,总是避免不了有人批驳说:“SQL 是描述性的语言。你只告诉它 What,而不是告诉它 How。”我发现每一次有人批驳我的观点,总是拿一些我多年前就听腻了,看透了的“广告词”,而现在这同样的事又发生在 SQL 身上。他们没有发现,我不但能实现 SQL,而且已经实现过比 SQL 强大很多的语言,所以我其实早已看透了所有这些语言的实质。
Prolog 与人工智能的没落
可以说,“只告诉它 What,而不是告诉它 How”,只是一个不切实际的妄想,而且它并不是 SQL 首创的想法。在 SQL 诞生两年以前,有人发明了 Prolog,著名的“逻辑式语言运动”的先锋。Prolog 使用了与 SQL 非常类似的广告词,声称自己能够一劳永逸的解决人工智能和自动编程的问题,这样人们不需要写程序,只需要告诉电脑“想要什么”,然后电脑就能自动生成算法,自动生成代码来解决这问题。
呵呵,世界上总是有很多这种类似“减肥药”的东西,每一个都声称自己是“不需运动,不需节食,一个星期瘦 20 斤!”然而由于人类的智力和经验参差不齐,总会有人上当。Prolog 当年的风头之大,以至于它被日本政府采用并且大力推广,作为他们所谓的“第五代计算机”的编程语言。可惜的是,减肥药毕竟是减肥药,科学道理决定了 Prolog 必定失败,以及“人工智能冬天”(AI winter)的到来。
为什么 Prolog 会失败呢?这是因为 Prolog 虽然“终究”有可能自动解决某些问题,然而由于它的算法复杂度太高,所以没法在我们有生之年完成。说白了,Prolog 采用的“计算”方式就是“穷举法”。为了得到用户“描述”的问题的答案,而不需要用户指定具体的数据结构和算法,Prolog 必须对非常大的图状解空间进行完全的遍历(Prolog 采用深度优先搜索)。而这种解空间的“状态”数量往往是指数增长,这就决定了 Prolog 虽然“最终”可能找到问题的答案,却很有可能在地球毁灭之前都没法完成它的搜索。而且由于 Prolog 无法表达真正意义上的“逻辑否”操作,所以对于很多问题它永远无法得到正确的答案(这是一个非常深入的问题,30 多年的研究,仍然没有结果)。
过于具体的细节我不想在这里解释,你只需要明白的是我并不是在信口开河。在 Indiana 的日子里,我学会并且重新实现了一种与 Prolog 类似的逻辑式语言叫做 miniKanren,它也就是 Dan Friedman 的新书《The Reasoned Schemer》的主题。虽然 miniKanren 比起 Prolog 更加优雅,而且在搜索算法上有所改进(广度优先而非深度优先),它本质上采用的搜索方式也是一样的:穷举法。所以在很多时候它的效率很低,用法不灵活。像 Prolog 一样,miniKanren 并不能用来解决很多实际的问题。
只举一个例子,在 IU 的时候总有一些人喜欢用 miniKanren 来实现类似 Haskell 的 Hindley-Milner 类型系统。最基本的基于 unification 的类型推导,miniKanren 确实能做到,然而如果遇到一些必要的扩展,比如 let-polymorphism,你就需要对 miniKanren 语言本身进行扩展。也就是说,你不再是用 miniKanren 实现你的算法,而是用 Scheme 把你的算法加到 miniKanren 里面,然后再利用这个你已经实现的“新特性”来“实现”你的算法。于是你就发现,其实 miniKanren 本身并没有足够的表达力表示完整的 Hindley-Milner 类型推导算法。这就是为什么虽然我很感谢 miniKanren 教会了我逻辑编程的原理,然而我实现过的所有强大的类型系统,全都是用最普通的过程式或者函数式语言。
从 Prolog 到 SQL
Prolog (miniKanren)的性能问题在 SQL 里面得到了部分的缓解,这是因为 SQL 对于基本的数据结构进行了“索引”。比如,对于基本的算数操作 x < 10,它能够通过对索引(B树)的查找来进行“优化”,从而避免了对 x 所有可能的值(一个非常大的空间)进行完全的遍历。
然而 SQL 的表达力也受到比 Prolog 更大的限制。很多 Prolog 可以表达的问题,SQL 没法表示。所以后来你就发现,SQL 其实只能用于非常简单的,适合会计等人员使用的查询操作。一旦遇到程序员需要的,稍微复杂一点的数据结构,它就会引起诸多的性能问题。
更要命的是,这种性能问题的来源是根本性的,不可解决的,而不只是因为某些数据库的 SQL 编译器不够“智能”。很多人不理解这一点,总是辩论说“我们为何需要 Java 而不是写汇编,也就是我们为何需要 SQL。”然而,把 Java 编译成高效的汇编,和把 SQL 编译成高效的汇编,是两种本质上不同的问题。前者可以比较容易的解决,而后者是不可能的(除了非常个别的情况)。
我只举一个例子来说明这个问题。如果你需要迅速地在地图上找到一个点附近的城市,SQL 无法自动在平面点集上建造像 KD-tree 那样的数据结构。这是很显然的,因为 SQL 根本就不知道你的数据所表示的是平面上的点集,也不理解平面几何的公理和定理。跟 B-tree 类似,知道什么时候需要这种特殊的索引结构,需要非常多的潜在数学知识(比如高等平面几何),所以你肯定需要手动的建立这种数据结构。你发现了吗,你其实已经失去了所谓的“描述性”语言带来的好处,因为你完全可以用最普通的语言,加上一些构造 B-tree, KD-tree 的“库代码”,来实现你所需要的所有复杂查询操作。你的 SQL 代码并不会比直接的过程式代码更加清晰和简洁。再加上 SQL 本身的很多设计失误,你就发现使用 SQL 数据库其实比自己手工实现这些数据结构还要痛苦。你学会 SQL 是为了避免编程,结果你不得不做比编程还要苦逼的工作,美其名曰,“SQL performance tuning”。
到这里也许有人仍然会说,这只是因为现在的 SQL 编译器不够智能,总有一天我们能够制造出能够“自动发明”像 B-tree, KD-tree 这样索引结构的“优化算法”。我对此持非常不乐观的态度。首先你要意识到,哪怕最基本的数学知识,也是经过了人类几千年的实践和研究才得到的。计算机虽然越来越快,它却缺乏对于世界最直接的观察和探索能力,所以在相当长的时间内,计算机是根本不可能自动“想到”这些数学和算法问题的,就不要谈解决它们。其次,即使计算机有一天长了脚可以走路,有了眼睛可以看见东西,有了“自由意志”,可以自己去观察世界,它却不一定能够比人类快很多的发现并且解决“人类关心的数学问题”。最后,我们需要在有生之年解决这些迫切的问题,我们无法等待几十年几百年,就为了让计算机自己想出像 KD-tree 一类众所皆知的数据结构。
这就是为什么你几乎总是需要手动指定索引的原因,而且这种索引需要数据库“内部支持”。你一次又一次的希望 SQL 能够自动为你生成高效的索引和算法,却一次又一次的失望,也就是这个原因。当然,你永远可以使用所谓的 stored procedure 来扩展你的数据库,然而这就像是我的 IU 同学们用 miniKanren 来实现 HM 类型系统的方式——他们总是先使用一种过程式语言(Scheme)来添加这种描述性语言的“相关特性”,然后欢呼:“哇,miniKanren 解决了这个问题!”而其实呢,还不如直接使用过程式语言来得直接和容易。
关系式模型的实质
每当我批评 SQL,就有人说我其实不理解关系式模型,说关系式模型本身并没有问题。所以现在我就来分析一下什么是关系式模型的实质。
我想很多有经验的数据库使用者都理解,关系式模型的每一个“关系”或者“行”(row),其实表示的不过是一个普通语言里的“结构”(比如 C 的 struct)。一个表(table),其实不过是一个装满结构的数组。每一个 join,其实就是沿着行里的“指针”进行“寻址”,找到它所指向的东西。当然,这些操作都是基于“集合”的,但这并不妨碍你使用普通的语言(比如 C 或者 Java)来完成这种操作,它们都可以通过很简单的“库代码”来完成。
所以,关系式模型所能表达的东西,绝对不会超过普通过程式语言所用的数据结构,然而关系式模型却有过程式数据结构所不具有的局限性。由于经典的关系“行”只能有固定的宽度,所以导致了你没法在结构里面放进任何“变长”的东西。比如,如果你有一个变长的数组需要放进结构,你就需要把它单独拿出来,旋转 90 度,做成另外一个表,然后在原来的表里用一些“key”指向它们。这通常被叫做 normalization。这种方法虽然可行,然而我不得不说这是一个“变通”。它的存在是为了绕过关系式模型的这一无须有的限制,它终究导致了关系式数据库的繁琐。
NoSQL 的革命和终结
上面的这一系列问题,终究引发了所谓 NoSQL 的诞生。然而我并不觉得有很多 NoSQL 数据库的设计者们看到了以上我所看到的问题,所以他们的设计并没有完全摆脱关系式模型以及 SQL 带来的思维枷锁。
最早试图冲破关系式模型和 SQL 限制的一种数据库叫做“列模式数据库”(column-based database),其代表包括 Vertica 等新兴产品。这种数据库其实就是针对了我刚刚提到的,关系式模型无法保存可变长度数组的问题。这些列模式数据库所谓的“压缩”,其实不过是在“行结构”里面增加了对“数组”的表示和实现。很显然,每一个数组需要一个字段来表示它的长度,剩下的空间用来依次保存每一个元素。这也就是你在这些列模式数据库的设计里所看到的。
最新的一些 NoSQL 数据库,比如 Neo4j, MongoDB 等,部分的针对了 SQL 的表达力问题。Neo4j 设计了自己的查询语言 Cypher,MongoDB 使用普通的 JavaScript 来对数据进行查询。所以到现在看来,数据库的主要问题已经转移到了语言设计的问题。
只要你有一个好的程序语言,你就可以发送这种语言的代码到“数据库服务器”,这个服务器可以远程执行你的代码,调用服务器上的“库代码”对数据进行索引,查询和重构,然后返回代码指定的结果。如果你看清了 SQL 的实质,就会发现这样的“过程式设计”其实并不会损失 SQL 的“描述性语言”的表达能力。反而由于过程式语言使用的简单性,直接性和普遍性,使得开发效率有很大提高。
然而,NoSQL 的终极问题在于,设计他们的人并不是经过了专业的程序语言设计训练的。我经历过好些 NoSQL 数据库之后发现,它们的查询语言,要么没有逃脱 SQL 的圈套(比如 Neo4j 的 Cypher),要么就没有逃脱普通程序语言的圈套(比如 MongoDB 的 JavaScript)。而且由于具体的实现质量以及商业动机,这些数据库往往有各种各样恼人的问题。这是必然的现象。因为这些数据库公司靠的就是咨询和服务作为收入,如果他们把这些数据库高质量又开源的实现,没有烦人的问题,谁会给他们付费呢?
所以,这些 NoSQL 数据库问题的存在,也许并不是因为人们都很笨,而是因为世界的经济体制仍然是资本主义,大家都需要骗钱糊口,大家都舍不得给“小费”。
总结
说了这么多,其实主要的只有几点:
SQL,Prolog 等所谓“描述性语言”的价值被大大的高估了。使用它们的人往往有“避免编程”的心理,结果不得不做比编程还要痛苦的工作:数据库查询优化。数据库完全可以使用普通的程序语言(Java,Scheme 等)的“远程执行”来进行查询,而不需要专门的查询语言。这在某种程度上就是 NoSQL 数据库的实质和发展方向。关系式模型严重束缚了人们的思想,其本质并不如普通的数据结构简单和高效。
对这个问题有兴趣的人,可以参考我的一篇相关的英文博客,以及这篇《一种新的操作系统的设计》