12.2 Dimitri Fontaine访谈

我认识Dimitri已经20年了。他是一位经验丰富的PostgreSQL主要贡献者,在2ndQuadrant公司(http://2ndquadrant.com/en/)工作,并在pgsql-hackers的邮件列表上与其他数据库大牛辩论。我们彼此分享了很多开源的体验,并且他非常热心的回答了很多在处理数据库时你应该了解的问题。

阅读 ‧ 电子书库

广告:个人专属 VPN,独立 IP,无限流量,多机房切换,还可以屏蔽广告和恶意软件,每月最低仅 5 美元

对用RDBMS作为存储后端的开发人员你有什么建议吗?有什么是他们需要了解的吗?

这是一个很好的问题,因为它让我有更多机会专门澄清一些非常错误的假设。如果你觉得这个问题有意义的话,那么现在你真的有必要看一下我的回答。

让我们从一些无聊的部分开始:RDBMS代表Relational DataBase Management System(关系型数据库系统)。它们是在20世纪70年代发明的,用来解决一些那个时代的每个开发人员都会遇到的常见问题,并且RDBMS实现的主要服务并不是数据存储,因为人们已经知道如何实现数据的存储了。

RDBMS提供的主要服务如下。

 
  • 并发:可以执行多个并发的线程对数据进行读写,RDBMS能够正确地进行处理。这是你需要RDBMS提供的主要功能。
  • 并发语义:讨论RDBMS中有关并发行为的细节必然涉及高级规范中的原子性(Atomicity)和隔离性(Isolation),它们可能是ACID(Atomicity,Consistency,Isolation,Durability)中最关键的部分。原子性是指在开始(BEGIN)一个事务和其结束之前(不管是执行COMMIT还是ROLLBACK)的这段时间内,任何系统中的其他并发行为都无法获知你在干什么,不管是什么的一种性质。当使用一个合适的包含数据定义语言(Data Definition Language,即DDL,如CREATE TABLEALTER TABLE)的RDBMS时,隔离性是指在系统中你自己的事务内允许你看到的其他并发行为。SQL标准中定义了四个级别的隔离,在事务隔离文档(http://www.postgresql. org/docs/9.2/static/transaction-iso.html)中有所描述。

RDBMS承担了对数据的全部责任。所以它允许开发人员描述自己的一致性规则,并且会在关键的时候对这些规则进行检查,例如,当事务提交时或者语句的边界,取决于约束声明的可延迟性(deferability)。

对于数据的第一个约束是关于其期望的输入输出格式的,即使用合适的数据类型。合适的RDBMS知道更多关于如何处理文本数值日期格式,并能恰当地处理实际出现在当今日历中的日期(罗马儒略历4到目前还不算特别大,除非处理历史日期,否则你可能需要格里高里历5)。

但是数据类型不仅仅与输入输出格式有关,它们还允许实现行为和某种程度的多态性,因为我们期望的基本相等测试是针对特定数据类型的。我们不会以同样的方式比较文本和数字,比较日期和IP地址,比较点框和线条,比较布尔和圆形,比较UUID和XML,比较数组和范围,等等。

保护数据还意味着合适的RDBMS的唯一选择是要能主动拒绝不满足一致性的规则的数据,首当其冲的就是你已经选择的数据类型。如果你觉得处理类似0000-00-00这样在日历中根本不存在的日期可以的话,那么你需要重新考虑一下。

保证一致性的其他有关约束的表现方式还包括CHECK约束,NOT NULL约束和约束触发器,后者通常被称为外键。所有这些都可以作为数据类型定义和行为在用户层面的扩展,主要区别是可以选择DEFER检查这些约束从每条语句结束到当前事务结束。

RDBMS的关系特性主要体现在对数据的建模,以及保证所有在同一个关系中的元组共享通用的规则集,即结构和约束。当执行时,即表示我们正在强制使用适当的显式模式来处理数据。

令数据工作在适当模式上的过程被称为规范化(normalization),并且可以在设计中实现许多有些细微不同的范式(Normal Form)。但是有时也会需要规范化过程无法提供的灵活性。常见的办法是先规范化数据模式,然后反过来看如何进行反规范化(denormalization)以获得需要的灵活性。你可能会碰巧发现你并不需要额外的灵活性。

当你发现确实需要更多的灵活性时,PostgreSQL为初学者提供了一些反规范化的选择:组合类型(composite type)、记录(record)、数组(array)、hstore、json或XML。

反规范化有一个很重要的缺点,就是我们接下来要讨论的查询语言(Query Language),它被设计为处理而非规范化数据。当然,PostgreSQL已经对查询语言进行了扩展,当在使用组合类型、数组或hstore,甚至最近发布的json时,支持尽可能多地非规范化数据。

RDBMS对数据非常了解并能在需要的情况下帮助实现非常细粒度的安全模型。访问模式被控制在关系和列层面,并且PostgreSQL还实现了SECURITY DEFINER存储过程,可以对敏感数据提供非常受限的访问,很像在使用suid程序。

RDBMS可以使用结构化查询语言对数据进行访问,结构化语言在20世纪80年代已经成为了事实上的标准并且目前由一个专门的委员会负责管理。对于PostgreSQL,每年的每个主要发布版本都有大量的扩展被加入,以支持极为丰富的DSL语言。所有查询规划和优化的工作都由RDBMS来完成,以便你可以专注于声明式的查询,即只需要描述对于你所拥有的数据想要什么样的结果。

这也是在这里要对NoSQL多加注意的原因,因为大部分这些新兴的产品实际上移除的不光是结构化查询语言,还包括你已经掌握并期望包含的很多其他基础的东西。

我的建议是开发人员要记住存储后端和RDBMS间的区别。它们是非常不同的服务,如果需要的只是存储后端的话,也许应该考虑避免用RDBMS。

但是大多数情况下,你真正需要的是一个完全成熟的RDBMS。这种情况下,最好的选择是PostgreSQL。去读一下它的文档,看看它提供的数据类型、操作符、函数、特性和扩展的清单,以及在博客上看一些使用示例。

然后考虑一下在你的开发中将PostgreSQL作为一个工具来利用,并将其包含在应用程序的架构中。你需要实现的部分服务在RDBMS层面已经给予了最好的支持,而且PostgreSQL擅长成为整个实现中最值得信赖的部分。

用或不用ORM的最好方式各是什么?

SQL代表Structured Query Language(结构化查询语言)并且其对于PostgreSQL已经被证明是图灵完备的。它的实现和优化都相当有分量。

由于ORM代表Object Relational Mapper(对象关系映射器),其思想是你能够处理一对一的映射,即数据库关系和类,以及数据库元组和对象(或者说类实例)。

即使对于PostgreSQL这样种已经实现了强静态类型的RDBMS,关系定义也是动态建立的,每一次查询结果都是一个新关系。每一次子查询的结果也是一个新关系,而且可能只存在于这个子查询期间。每一个JOIN,无论是INNER或者OUTER,都将动态生成一个新关系以处理JOIN

作为一个直接结果,很容易明白ORM能完成的最好的工作就是所谓的CRUD应用,即创建(Create)、读取(Read)、更新(Update)、删除(Delete)。读取部分会受限制,只能对单个表做非常简单的SELECT查询。如果比较较大的输出列表,可以测量出提取额外的列和必要的列之间的查询性能的区别。现在,如果ORM在它的投影(或输出列表)中包含所有的已知列,那么它将强制你的RDBMS在发送前提取额外的数据(并解压缩),如果在RDBMS和应用程序之间使用SSL的话可能还需要再次压缩。而且,还要考虑到网络带宽的使用并记得我们正在测量的是基于主键毫秒级的查询。

所以,任何从RDBMS中提取但最终没有使用的列,都是对宝贵资源的严重浪费,是可扩展性的第一杀手。

即使在ORM能够只获取你请求的数据,接下来你也必须以某种方式管理在每种情况下要显示的具体列,并避免使用会自动计算字段列表的简单抽象的魔术方法。

CRUD查询的其余部分是简单的INSERTUPDATEDELETE。首先,当使用高级的RDBMS(如PostgreSQL)时所有这些命令都接受连接(join)和子查询。而且仍需提及的是,比如PostgreSQL实现了RETURNING子句,允许返回给客户端任何刚编辑过的数据,如默认值(对于代理键通常是序列号)和其他在RDBMS(一般通过BEFORE <action>触发器)上自动计算的值。但你的ORM能够意识到这些吗?什么语法可以从中受益呢?通常情况下,一个关系或者是一个表( 调用一个返回集合的函数的结果),或者是任意查询的结果。常见的做法是使用ORM构造已定义的表和其他模型类(或其他辅助模块)之间的关系映射

如果从总体上考虑SQL的整个语义的话,那么关系映射实际上应该能够将任意查询映射到一个类。因此可能需要为每个运行的查询建立一个新类。

充分智能编译器(Sufficiently Smart Compiler)的传说同样适用于ORM。关于这个传说的更多细节,可以读一下James Hague的On Being Sufficiently Smarthttp://prog21.dadgum. com/40.html)。

其思想应用到我们这个场景下就是你相信ORM能比你写出更高效的SQL查询,认为即使你没有给出足够的信息也能精准地给出你想要的数据集。

有时候SQL确实会变得相当复杂。但是你不太可能通过一个自己无法控制的SQL生成器的API让其变得更简单。

在讨论了所有典型的ORM之后,也需要说一下其他的选择。

将SQL查询构造为字符串会难以扩展。你会想要组合几个限制条件WHERE子句)并且动态地添加一些连接(join)到查询中,以便可以有选择地获取更细节的数据等。

我现在的想法是,你真正想要的工具可能并不是ORM,而是一种通过编程接口更好地组合SQL查询的方式。

名为Postmodern(http://marijnhaverbeke.nl/postmodern/)的PostgreSQL驱动针对这个问题提出了几乎类似的抽象,它是一个结合了S-SQL(http://marijnhaverbeke.nl/postmodern/s-sql.html)方案的Common Lisp库。当然,Lisp借助其本身能够很容易地开发可组合的组件。

实际上有两种场景可以放心地使用ORM,只要你愿意接受下面的条件:你需要尽快将ORM使用的代码移出代码库。

 
  • 上市时机。当真的需要尽可能快地占领市场份额的时候,唯一的办法就是尽快地发布应用和想法的第一个版本。如果你的团队比手写SQL更擅长ORM的使用的话,那就全力去做。但是你必须意识到,一旦你的应用取得成功,要解决的可扩展性问题之一就是你的ORM生成的糟糕的查询,并且ORM的使用已经把你逼到了死角并做了一些糟糕的代码设计决定。但如果你到了这个地步的话,那么你应该已经足够成功到花一些重构的钱去删除那些对ORM的依赖,对吧?
  • CRUD应用。真正要处理的只是一次编辑一个元组,并且不关心性能问题。例如,基本的管理应用界面。

对于Python开发,选用PostgreSQL与选用其他数据库相比有什么优缺点吗?

下面是我作为开发人员选择PostgreSQL的主要原因。

 
  • 社区支持:PostgreSQL社区非常欢迎新用户,而且通常都会花时间充分理解你的问题以给出最好的可能答案。邮件列表仍然是与社区沟通的最好方式。参考PostgreSQL邮件列表(http://www.postgresql.org/list/)以便了解更多细节。
  • 数据完整性和持久性:任何发送给PostgreSQL的数据都在其定义中都是安全(存储)的并能在其后再次获取。
  • 数据类型、函数、操作符、数组和范围:PostgreSQL有着非常丰富的数据类型集合,它们非常有用,并且拥有大量用于处理这些数据类型的操作符和函数。甚至可以使用数组JSON数据类型进行反规范化,而且仍然可以通过包含连接(join)的高级查询操作它们。例如,你知道正则表达式操作符~吗?还有函数regexp_split_to_array``和``regexp_split_to_table
  • 计划器和优化器:你得尝试推进你所知道的关于它们的限制,以便了解它们到底有多复杂和强大。我已经多次看到只为提升几毫秒而完善成的两三页长的查询。
  • 事务性的DDL:几乎可以回滚(ROLLBACK)任何命令。现在就试试看,只需要对你的数据库打开psql shell并输入BEGIN; DROP TABLE foo; ROLLBACK;(将其中的foo替换成你本地实例中存在的表的名字)。太妙了,对吗?
  • INSERT INTO ... RETURNING:可以从INSERT语句中直接返回任何东西,如一个递增序列的id值。与执行一次SELECT语句相比,你省去了一次网络通信并且用同一个协议和工具就得到了结果。
  • WITH (DELETE FROM ... RETURNING *) INSERT INTO ... SELECT:Postgre- SQL在查询中支持常用表表达式(Common Table Expression),被称为WITH查询,而且幸好它支持RETURNING子句,因此它还支持DML命令。那简直太牛了,是吧?
  • 窗口函数CREATE AGGREGATE:如果你不知道窗口函数是什么,那么去读一下Post-greSQL手册或者我博客中的Understanding Window Functionshttp://tapoueh.org/blog/-Window-Functions)一文。然后你将意识到PostgreSQL允许你使用任何已经存在的聚合(aggregate)作为窗口函数,而且允许你在SQL中动态地定义新的聚合。
  • PL/Python(以及其他语言,如C、SQL、Javascript或Lua):你可以在服务器上运行你自己的代码,就在数据所在的位置,以便你无需通过网络获取数据进行处理再在查询中将其发回去执行下一级的JOIN。不管怎样,你可以完全在服务上执行。
  • 特定索引(GiST、GIN、SP-GiST、partial&functional):你知不知道可以从PostgreSQL中创建Python函数去处理你的数据,并索引函数调用的结果?以便当你发出一个包含了调用该函数的WHERE子句的查询时,它只从查询中以该数据被调用一次,然后就会直接匹配索引的内容。PostgreSQL对非排序数据类型实现了索引框架,如二维类型(rangesgeometry等)和容器数据类型。许多场景已经默认支持了,这要多谢PostgreSQL的扩展系统。可以看看扩展支持模块(Additional Supplied Modules,http://www.postgresql.org/docs/9.3/static/contrib.html)和PostgreSQL扩展网络([http:// pgxn.org/](http:// pgxn.org/))。
  • 扩展:这些扩展包括hstore,一个包含灵活索引的完全成熟的键值存储;ltree,用于索引嵌套的标签;pg_trgm,作为一个穷人的全文搜索方案支持正则表达式搜索的索引和非锚定LIKE查询;ip4r,用于在一定范围内快速搜索一个IP地址,以及更多其他扩展。
  • 外部数据封装器:外部数据封装器是实现了SQL/MED标准(Management of External Data)的一组完整扩展。其思想是将一个连接驱动嵌入PostgreSQL服务器中,并将其通过CREATE SERVER命令暴露出来。PostgreSQL给外部数据封装器的作者提供了一个API,允许他们实现对远程数据的读写,以及where子句的叠加,以获得更高效的连接(joining)能力。你甚至可以使用PostgreSQL的高级SQL功能操作那些通过其他技术维护的数据。
  • LISTEN/NOTIFY:PostgreSQL实现了一个名为LISTEN/NOTIFY的异步服务器到客户端的协议。当有些有意思的事情发生时,应用程序可能会收到来自服务器的主动推送的消息,例如更新某些数据时。NOTIFY命令接收一个有效载荷以便可以在对象被删除或更新时通知你的缓存应用相应的对象ID。当然,只有在事务成功提交后通知才会发生。
  • COPY流协议:PostgreSQL实现了一个流协议,并用它实现了全集成的复制方案。现在,可以很容易地在应用程序中使用它,并能带来极大的性能提升。在需要一次处理十几行的时候,有时会在这之前针对一个临时表使用COPY,然后执行一个单独的语句连接这张临时表。PostgreSQL知道如何在所有数据的修改语句(insertupdatedelete)中对这些表进行连接,并且批处理操作通常会更快。

1如果犹豫不决的话,选PostgreSQL(http://www.postgresql.org/)。
2另一种选择是使用HTTP/1.1中定义的Transfer-Encoding: chunked。
3能够同其他RDBMS服务器兼容,如MySQL。
4 Julian calendar,参见http://en.wikipedia.org/wiki/Julian_calendar
5 Gregorian calendar,参见http://en.wikipedia.org/wiki/Gregorian_calendar。——译者注