Markus Winand 是数据库专家,著有《SQL Performance Explained》一书。2013 年 Uber 从 MySQL 迁移到 PostgreSQL,而在 2016 年 8 月,Uber 工程博客发文称他们从 PostgreSQL 迁回 MySQL。当时在业内引发热议,Markus 写下了这篇文章来回应。
  你可能听说了 Uber 从 PostgreSQL 迁移到 MySQL 。这对于他们来说,或许是一个好的迁移,但是这未必适合你。且听我说来。
  2016 年 8 月,Uber 发表了一篇名为《为什么 Uber 工程从 PostgreSQL 迁移到了 MySQL》的文章。我并没有立即阅读原文,因为我的内心深处告诉我应该做一些本地的改进。在如此做的过程中,我的邮箱塞满了问题,比如“难道 PostgreSQL 真的有这么差劲吗?”我知道  PostgreSQL 并没有如此差劲,因此这些邮件使我想知道,原文到底写了些什么鬼玩意。这是一篇企图理解 Uber 的文章。
  在我看来,Uber 的文章基本上在说他们发现 MySQL 比 PostgreSQL 更适合他们的环境。然而,原文在传递这一消息时却表现的非常差劲。举例来说,原文并没有写“  PostgreSQL 在 update-heavy 的使用场景下有一些局限”,而是写到“它对于写入(write)来说是一个非常低效的架构”。当你没有 updata-heavy 的使用场景时,不必担心 Uber 的文章中描述的问题。
  在本文中,我将会解释:①为什么我认为 Uber 的文章并不能作为我们在选择数据库的一个普适性建议,②为什么 MySQL 仍旧适用于 Uber,③为什么这样的成功迁移案例可能会引起更多的问题,而不是扩展了数据库。
  在更新方面的问题
  Uber 的文章所出现的第一个问题是, 在更新表中的行时,PostgreSQL 通常需要更新表上的所有索引,原文对这方面做了大篇幅的描述,但细节又不够充分。另一方面,对于具有 InnoDB 的 MySQL 来说,只需要更新那些包含有更新列的索引。在更新改变了非索引列之时(原文中的「Write Amplification」部分),PostgreSQL 的方法需要占用更多的磁盘 IO。如果这对 Uber 来说是一个大问题,那这些更新对于他们来说,或许是其整个工作量中的很大一部分。
  然而,我有一些猜测,是 Uber 文章并没有提到的。原文并没有提及 PostgreSQL 的 Heap-Only-Tuples( HOT )。从 PostgreSQL 源码来看,HOT 对于一些特殊场景非常有用,即“当一个元组进行了重复更新时,不需要对它的索引列进行更新”。在这个场景中,如果新的行版本能够和之前的版本存储在同一个页面中,PostgreSQL 能够做出一些不需要触及任何索引的更新。后一种情况可以使用  fillfactor 来进行调谐。假设 Uber 的工程认为 HOT 不能够解决他们的问题,是因为这些频繁的更新影响了不止一个索引列。
  这个假设也在文章中的下列语句得到了支持:“如果我们有一张定义了十几个索引的表,那么只覆盖了一个指标的领域的更新必须涉及到整整12个指标来反映新的行中的 ctid ”。它明确地指出“只覆盖了一个指标,”只有一个索引这是一种极端情况,否则 PostgreSQL 的 HOT 是可以解决这一问题的。
  【旁注:我比较关心他们所拥有的索引的数量能否减少— index redesign  是我的一个挑战。无论如何,对于那些使用的很少但在使用时又非常重要的索引来说,这是完全有可能的。】
  看起来他们似乎运行着许多更新,这些更新改变了不止一个索引列,但这和表中所有索引相比来说,这算不上啥。如果这是一个主要的用例,那么原文建议用 MySQL 来取代 PostgreSQL 能够说得通了。
  关于 Select 的问题
  还有一个关于他们用例的声明引起了我的注意:原文解释了 MySQL/InnoDB 使用了 clustered indexed ,并且承认了“这种设计意味着在做二次关键值查找时和 Postgres 相比 InnoDB 的优势并不明显”。关于这个问题(the clustered index penalty),我之前写过在 SQL Server 环境下的文章。
  他们写到 clustered index penalty 的优势很小,这引发了我的兴趣。在我看来,当你运行了很多使用二次索引的查找时,这个优势是很大的。如果对于他们来说这总种优势很小,那这可能说明这些索引使用得很少。这将意味着他们在大多数情况下使用了 primary key 来进行查找(那么不需要付出 clustered index penalty 了)。注意我写的是“查找”而不是“选择”。这其中的原因在于 clustered index penalty 会影响具有 where 字句的声明,而不仅仅是选择。这也意味着高频率的更新大多基于primary key。
  后,还有关于查询的问题:他们没有提到PostgreSQL在  index-only scans 方面的限制。尤其是在更新比较多的数据库,安装 PostgreSQ L的 index-only scans  没有多大用处。我甚至说过这是影响我大多数客户端的问题。我在 2011 年已经在博客上写过这个问题。2012 年, PostgreSQL 9.2 在 index-only scans 获得了一些有限的支持(这些工作大多服务于静态数据)。2014 年我甚至在 PgCon 提出了我关注的一个方面。然而, Uber 并没有抱怨这个问题。选择速度对于他们而言并不是问题。我猜测查询速度有运行在  replicas 上的选择进行解决(如下所示),并且可能被大多数 primary key 所限制。
  如今,他们的使用案例看起来似乎更加适合一个键/值存储。并且 Innodb 是一个很坚实和流行的键/值存储。这里甚至有一些包将 InnoDB 和一些(非常有限的)的前端 SQL 绑定:我认为 MySQL 和 MariaDB 是非常流行的两个。但是认真地说,如果你需要一个键/值存储并且恰巧想运行一个简单的 SQL 查询, MySQL (或者 MariaDB )是合理的选择。我猜测这至少比任何随机的 NoSQL 键/值存储要好,它们往往只提供一些更加有限的 SQL-ish 查询语句作为开始。Uber ,从另一个方面来说仅仅在 InnoDB 和 MySQL 上层建立了他们自己的东西(“Schemaless”)。
  关于索引再平衡(Rebalancing)
  关于原文谈索引,我后再说一个方面:它在 B-tree 索引语句中使用了“再平衡”这个词。它甚至链接到了维基百科上词条 “Rebalancing after deletion”。不幸的是,维基百科词条解释并不普遍适用于数据库索引,因为维基百科上所描述的算法维持了每个节点至少是 half-full 的要求。为了提高并发性, PostgreSQL 使用了Lehman, Yao variation of B-trees,其升高了要求并且因此允许稀疏索引。作为一个旁节点,PostgreSQL 仍旧移除索引中的空白页(参见“ Indexing Internals” 幻灯片第 15 页)。然而,这仅仅只是一个次要问题。
  真正使我担心的是这句话:“B-tree 有核心方面,必须周期性再索引(rebalancing)…”,看到这里,我想声明这不是一个每天运行的周期性的过程。索引平衡是和每一次单独的索引变化一同维护的(或许更严重,对吗?)。但原文继续写到“…当子树移动到新的 no-disk 位置时,这些 rabalancing 操作会彻底改变树的结构”。如果你现在认为“rebalancing”包含了大量的数据移动,那你误解了它。
  一个 B-tree 中的重要操作是结点分裂。正如你所猜想的那样,结点分裂发生在一个结点不能主持属于这个结点的一个新条目。给你一个近似数,这可能在 100 次插入中发生一次。结点分裂分配新的结点,将一般的条目移动到新的结点并将新结点和它的前、后以及父结点连接起来。这是 Lehman , Yao 节省了大量锁定的地方。在某些情况下,新的结点不能直接加入到父结点中,因为父结点没有足够的空间来让新结点加入。在这种情况下,父结点进行分裂并重复所有的过程。
  在糟糕的情况下,分裂泡沫会到达根结点,它也会进行分裂,并且会在其之上产生一个新的根结点。在这种情况下,一个 B-tree 将会增加深度。请注意,根结点的分裂有效转变了整棵树,并且因此保持了平衡。然而,这并不包括许大量数据的移动。在差情况下,它可能会在每个曾经(level)上影响到三个节点和一个新的根结点。更确切地说:根结点分裂的差情况,可能在一个十亿的插入中发生大约 5 次。在另一种情况下,它不需要遍历整棵树。总的来说,索引的维护不是“周期性的”,甚至不会发生的很频繁,并且永远不会改变整棵树的结构。至少在物理存储单元不会这样。