关于数据库:常见的数据库-schema-变更错误

5次阅读

共计 11687 个字符,预计需要花费 30 分钟才能阅读完成。

rake db:migrate — 这是任何 Ruby 开发者都晓得的一个命令。然而咱们在同一个 rake(耙)上踩了多少次?

在文章鲜为人知的 PostgreSQL 个性中,@be_haki 形容了 18 个许多人不晓得的 Postgres 个性。我很喜爱这篇文章,它启发我写了「反性能」– 在可能是利用开发中危险最大的畛域工作时,每个人都应该防止的事件 — 所谓的「schema 迁徙」。

这是我在关系型数据库畛域中最喜爱的话题之一。咱们都记得 MongoDB 是如何带着两个明确的信息进入舞台的:「网络规模」(让咱们领有开箱即用的分片)和「无 schema」(让咱们防止设计 schema,容许充沛的灵活性)。在我看来,这两个流行语都是过于简单化了,然而如果你有审查和部署关系型数据库的 schema 变更的教训,你可能会了解扩大 schema 变更过程中的艰难水平、危险和苦楚。我的集体问题是:在我本人公司应用 Postgres 的 17 年多工夫里,以及在为 GitLab、Chewy、Miro 等其余公司提供征询时,设计 / 审查 / 部署了 1000 多个迁徙我的项目。在这里,我将分享我所学到的货色,形容我所犯的或察看到的一些谬误 — 这样下次你可能就会防止这些谬误。

此外,帮忙人们防止此类谬误的强烈欲望促使我创造了数据库实验室引擎 — 一种用于薄型克隆数据库的技术,对于开发和测试来说是必不可少的。有了它,你能够在 10 秒内克隆一个 10 TiB 的数据库,测试 schema 变更,并在部署前理解危险。本文探讨的大多数状况都能够通过这样的测试轻松检测进去,而且能够在 CI/CD 流水线中主动实现。

像平常一样,我将专一于 OLTP 用例(挪动和网络应用),对于这些用例,超过 1 秒的查问执行通常被认为是太慢了。这里探讨的一些案例在流动少的小型数据库中很难留神到。但我很有信念,当你的数据库规模增长到 ~10 TiB,其负载达到 ~105-106 个事务 / 秒时,你会遇到其中的大部分状况(当然,有些状况会在更早的时候呈现 — 除非刻意阻止)。

我倡议你浏览 GitLab 的优良文档 — 他们的迁徙格调指南充斥了智慧,是由那些有教训的人写的,他们以齐全自动化的形式向大量的实例(包含 GitLab.com 自身)部署了许多 Postgres schema 变更。

我还激励大家观看 PGCon-2022 — Postgres 的重要会议之一;这次,它又在网上举办了。5 月 26 日星期四,我将发表两个演讲,其中一个叫做「常见的数据库 schema 变更谬误」,你能够在这里 找到 PPT。如果你错过了,不必放心 — @DLangille 从 2006 年开始组织这个会议(谢谢你,Dan!),承诺在几周内公布讲座视频。

目录

  • 术语
  • 三种类型的数据库迁徙谬误
  • 案例 1. Schema 不匹配
  • 案例 2. 滥用 IF [NOT] EXISTS
  • 案例 3. 遇到 statement_timeout(语句超时)
  • 案例 4. 无限大的变更
  • 案例 5. 取得一个独占锁 + 在事务中期待
  • 案例 6. 一个有 DDL + 大量 DML 的事务
  • 案例 7. 长时间期待获取独占锁 ⇒ 阻断别人
  • 案例 8. 不小心创立了一个 FK
  • 案例 9. 不小心删除了一个 FK
  • 案例 10. 不小心减少了一个 CHECK 约束条件
  • 案例 11. 不小心增加了 NOT NULL
  • 案例 12. 不小心扭转了列的数据类型
  • 案例 13. 大意的 CREATE INDEX
  • 案例 14. 大意的 DROP INDEX
  • 案例 15. 重命名对象
  • 案例 16. 增加一个带有 DEFAULT 的列
  • 案例 17. CREATE INDEX CONCURRENTLY 的遗留问题
  • 案例 18. 为大表增加 4 字节的整数主键
  • 倡议

术语

术语「DB 迁徙」可能会引起混同;它常常被用来形容从一个数据库系统切换到另一个零碎的工作,挪动数据库,并尽量减少可能的负面影响(如长时间的停机)。

在这篇文章中,我将议论这个术语的第二个含意 — 具备以下个性的数据库 schema 变更:

  • 「增量的」:变更是分步骤进行的;
  • 「可逆的」:有可能「撤销」任何扭转,回到 schema 的原始状态(和数据;在某些状况下,这可能是艰难的或不可能的);
  • 「可版本化的」:应用某种版本控制系统(如 Git)。
    我更喜爱应用调整后的术语,「DB schema 迁徙」。然而,咱们须要记住,许多 schema 的变更意味着数据的扭转 — 例如,将一个列的数据类型从整数改为文本,须要对整个表进行重写,这在重载的大型数据库中是一项非同小可的工作。

利用 DBA — 一个负责 DB schema 设计、开发和部署变更、查问性能优化等工作的数据库工程师,而「基础设施 DBA」则负责数据库配置、复制、备份、全局配置。@be_haki https://twitter.com/be_haki 在 利用 DBA 的一些 SQL 技巧中解释了「利用 DBA」这一术语。

最初,在咱们的小术语列表中,还有一些常见的可疑份子:

  • DML – 数据库操作语言(SELECT / INSERT / UPDATE / DELETE 等等)
  • DDL – 数据定义语言(CREATE …, ALTER …, DROP …)

三类数据库迁徙谬误

我把 DB schema 迁徙的谬误分为三大类:

  1. 并发相干的谬误。这是最大的一类,通常决定了一个利用 DBA 教训的重要局部。一些例子(目前跳过细节;咱们很快就会议论它们):

    • 未能取得锁
    • 一次性更新了太多的行
    • 取得了一个排他性的锁,并使事务长时间凋谢
  2. 与步骤的正确性无关的谬误 — 逻辑问题。例子:

    • 意外的 Schema 偏差
    • Schema / 利用程序代码不匹配
    • 意外的数据
  3. 杂项 — 与某些特定数据库性能的实现或特定数据库的配置无关的谬误,例如:

    • 遇到了 statement_timeout
    • 在能够增长的表中应用 4 字节的整数主键
    • 漠视 VACUUM 行为和臃肿危险

案例 1. Schema 不匹配

让咱们从一个根本的例子开始。假如咱们须要部署上面的 DDL:

create table t1 ();

在咱们开发和测试时,它运行良好。然而起初,在一些测试 /QA 或暂存环境中测试时失败了,或者 — 在最坏的状况下 — 在生产上的部署尝试中失败了:

ERROR:  relation "t1" already exists

呈现这种问题的起因可能是不同的。例如,该表可能是通过毁坏工作流程(例如,手动)创立的。为了解决这个问题,咱们应该考察该表是如何被创立的,以及为什么没有遵循这个过程,而后咱们须要找到一种办法来建设一个良好的工作流程,以防止这种状况。

可怜的是,人们常常抉择另一种形式来「解决」它 — 这就把咱们引向了第二种状况。

案例 2. 滥用 IF [NOT] EXISTS

察看到相似上述的 schema 不匹配谬误,可能会导致「放弃」那种修复形式:工程师们往往不会去寻找谬误的根本原因,而是抉择自觉地修补他们的代码。对于下面的例子,它能够是这样的:
create table if not exists t1();

如果这段代码不是用于基准测试或测试脚本,而是用于定义一些利用 schema,这种办法通常是个坏主意。它用逻辑覆盖了问题,减少了一些反常的危险。这种异常情况的一个显著例子是:一个现有的表与咱们要创立的表有不同的构造。在我的例子中,我应用了一组「空」列(在事实中,总是有一些列 — Postgres 创立了暗藏的零碎列,如 xmin、xmax 和 ctid,你能够读 Postgres 文档,5.5. 零碎列,所以每一行总是有几列;试试:insert into t1 select; select ctid, xmin, xmax from t1;

我常常察看到这种办法,可能在我工作的其余每个工程团队中都有。在 拥护 Postgres DDL 中的 IF NOT EXISTS / IF EXISTS 的三个案例 中对这个问题进行了详细分析。

如果你应用的是 DB schema 构造迁徙工具,如 Sqitch、Liquibase、Flyway,或嵌入你的框架中的工具(Ruby on Rails、Django、Yii 等都有),你很可能会先在 CI/CD 流程中测试 schema 构造迁徙。如果你开始测试链 DO-UNDO-DO(利用变更,复原它,再从新利用),它能够帮忙检测一些不心愿应用 IF [NOT] EXISTS 的状况。当然,放弃所有环境中的 schema 都是最新的,并尊重所有察看到的谬误,不漠视它们,不抉择诸如 IF [NOT] EXISTS 这样的「变通」门路,能够说是良好的工程实际。

案例 3. 遇到 statement_timeout

如果测试环境没有大表,测试程序不成熟,这个就很常见:

ERROR: canceling statement due to statement timeout

即便生产环境和非生产环境都应用雷同的 statement_timeout 设置,表越小,查问的执行速度就越快。这很容易导致只在生产环境中达到超时的状况。

我强烈建议在大量的数据上测试所有的变动,这样在开发 - 测试 - 部署的管道中就能更早地察看到这样的问题。这里最无力的办法是在管道的晚期应用全尺寸数据库的薄克隆,最好是在开发期间。查看咱们的数据库实验室引擎,如果你有问题,请通知咱们(Twitter @Database_Lab)。

案例 4. 无限大的变更

一个针对太多行的 UPDATE 或 DELETE 是一个坏主意,大家都晓得。但为什么呢?
一些例子:

test=# explain (buffers, analyze) update t1
        set val = replace(val, '0159', 'OiSg');


                                               QUERY PLAN
--------------------------------------------------------------------------------------------------------
 Update on t1  (cost=0.00..189165.00 rows=10000000 width=42) (actual time=76024.507..76024.508 rows=0 loops=1)
   Buffers: shared hit=60154265 read=91606 dirtied=183191 written=198198
   ->  Seq Scan on t1  (cost=0.00..189165.00 rows=10000000 width=42) (actual time=0.367..2227.103 rows=10000000 loops=1)
         Buffers: shared read=64165 written=37703
 Planning:
   Buffers: shared hit=17 read=1 dirtied=1
 Planning Time: 0.497 ms
 Execution Time: 76024.546 ms
(8 rows)

Time: 76030.399 ms (01:16.030)

可能烦扰生产的潜在问题:

  • 在一个事务中批改太多的行(在这里,咱们有一个单查问事务),意味着这些即将被锁定批改,直到咱们的事务实现。这可能会影响其余事务,可能会好转用户体验。例如,如果一些用户试图批改其中一个被锁定的行,他们的批改尝试可能须要很长时间。
  • 如果查看指针没有失去很好的调整(例如,max_wal_size 的值是默认的,1GB),在这样一个大规模的操作中,检查点可能会十分频繁地呈现。因为 full_page_writes 被关上(默认),这将导致 WAL 数据的适度生成。
  • 此外,如果磁盘零碎不够弱小,检查点产生的 IO 可能会使磁盘的写入能力饱和,导致总体性能降落。
  • 如果咱们的大量操作是基于一些索引和数据批改以随机程序产生在页面上,屡次从新拜访一个页面,在未调整的查看指针和频繁的检查点下,一个缓冲区可能会通过多个脏 - 清周期,意味着咱们有多余的写操作。
  • 最初,咱们在这里可能有两种类型的 清理 / 收缩 问题。首先,如果咱们在一个事务中用 UPDATE 或 DELETE 扭转了很多图元,就会产生大量的死图元。即便主动真空性能很快清理了它们,这样大量的死图元也很有可能间接转化为收缩,导致额定的磁盘耗费和潜在的性能降落。第二,在漫长的事务过程中,主动真空零碎无奈清理任何在咱们的事务过程中成为死的表的死图元 – 直到这个事务进行。

该怎么做呢?

  • 思考把工作分成几批,每一批都是一个独自的事务。如果你是在 OLTP 背景下工作(挪动或网络应用),应该确定批处理的大小,以便任何批处理的预期不会超过 1 秒。要理解为什么我举荐 1 秒作为批处理的软门槛,请阅读文章什么是 SQL 慢查问?
  • 留神 VACUUMing(真空)- 调整主动真空和 / 或思考在解决肯定数量的批处理后应用显式 VACUUM 调用。
    最初,作为一个额定的保护措施,调整查看指针,这样即便产生了大规模的变动,咱们的数据库的负面影响也不会那么重大。我举荐浏览 Tomáš Vondra 的调整检查点的基础知识。

案例 5. 在事务中取得一个 独占锁 + 期待

在后面的案例中,咱们涉及了长期持有独占锁的问题。这些锁能够是被锁定的行(通过 UPDATE 或 DELETE 隐式或通过 SELECT ... FOR UPDATE 显式)或数据库对象(例如:在事务块内胜利的 ALTER TABLE 会锁定该表并放弃该锁直到事务完结)。如果你须要理解更多对于 Postgres 中锁的信息,请浏览 Marco Slot 写的文章 PostgreSQL 是个好货色,除了它阻塞的时候:理解锁。

一个对于锁的个别问题的形象例子:

begin;
alter table t1 add column c123 int8;
-- do something inside or outside of the database (or do nothing)
commit;

在取得锁之后,坐在事务外面的起因可能有所不同。然而,有时它什么也不是 – 在一个凋谢的事务和取得的锁中简略地期待。这是最令人讨厌的起因,它能够迅速导致各种性能甚至局部停机:一个表的独占锁甚至阻断了这个表的 SELECT。

注意事项
记住:在一个事务中取得的任何锁都会被保留到这个事务的最初。只有当事务完结时,才会通过 COMMIT 或 ROLLBACK 开释它。

每当咱们取得一个独占锁时,咱们应该思考尽快实现事务。

案例 6. 一个有 DDL + 大量 DML 的事务

这个案例是前一个案例的一个子案例。我独自形容它是因为它能够被认为是一种常见的反模式,在开发 DB 迁徙时很容易遇到。上面是它的伪代码:

begin;
alter table t1 add column c123 int8;
copy ... -- load a lot of data, taking some time
commit;

如果 DML 步骤须要大量的工夫,就像咱们曾经探讨过的那样,在前一个步骤(DDL)上取得的锁也会被长期保留。这可能导致性能降落或局部停机。
要遵循的根本规定:

  • DML 永远不应该在 DDL 之后进行,除非它们都解决一些新创建的表。
  • 通常理智的做法是将 DDL 和 DML 流动分成不同的事务 / 迁徙步骤。
  • 最初,请记住,大规模的变动应该分批进行。每个批次都是一个独自的事务 — 所以如果你遵循这个规定,并且在 CI/CD 管道中测试变动时应用了大量的数据,那么你应该不会遇到这种状况。

案例 7. 长时间期待获取独占锁 ⇒ 阻塞别人

这个问题可能产生在大多数不小心部署的 ALTER 命令中 — 然而对于小型的、负载不重的数据库来说,这个机会是相当小的,所以这个问题可能在很长一段时间内都不会被留神到,直到有一天它以一种俊俏的形式呈现,引发了诸如「咱们怎么能忍耐这些呢?」这样的问题(我和几个团队经验了这个过程,它总是相当令人难堪)。

咱们曾经探讨过,当一个独家锁被获取,而后它被持有太长时间会产生什么。但如果咱们不能取得它呢?

这种状况可能会产生,在重载的大型数据库中,这是很常见的。例如,这种状况可能会产生,因为主动真空零碎正在解决咱们试图批改的表,而它没有产生 — 通常状况下,它会产生,但在事务 ID 缠绕预防模式下运行时就不会。这种模式被 Postgres 认为是一种重大的状态,必须尽快解决,所以惯例的主动真空中断工作以容许 DDL 胜利的逻辑在这里不起作用。在这种状况下,通常,最好只是期待。

但这还不是这个案例中最蹩脚的局部。真正蹩脚的是,在咱们期待获取锁的时候,如果咱们的超时设置(statement_timeout 和 lock_timeout)被设置为 0(默认)或相当大(>>1s),咱们就会阻塞对这个表的所有查问,甚至 SELECT。我在零提早的 Postgres schema 迁徙须要这个:lock_timeout 和 retries 一文中谈到了这个非凡的问题。

这里该怎么做呢?对于所有的(!)DB 迁徙,除了那些创立全新的 DB 对象或者应用 CREATE/DROP INDEX CONCURRENTLY 的迁徙(在上面探讨),你应该有低 lock_timeout 的重试逻辑,正如我在文章中形容的那样。这是一个每个人都须要领有的根本机制 — 我想在某个时候,无论是 Postgres 还是风行的 DB schema 迁徙工具都会实现它,所以利用 DBA 的世界会变得更好。

案例 8. 不小心创立一个 FK

在案例 5 中,咱们曾经探讨了一个由胜利的 DDL 获取独占锁和同一事务中的一些操作(或不足这些操作)组成的事务。但有时,一个单语句事务 — 一个 DDL — 能够把获取锁和一些减少操作工夫的工作联合起来,导致相似的成果。这项工作能够是浏览或数据批改;它继续的工夫越长,操作的工夫就越长,阻断其余会话的危险就越大。

咱们将探讨几个具备这样性质的案例 — 因为须要读取或批改一些数据,一个 DDL 操作的持续时间被缩短了。这些案例很类似,但我想逐个辨认它们,因为每一个案例都有轻微的差异。

这个系列的第一个案例是在两个现有的大而忙碌的表上创立一个外键:

alter table orders add constraint fk_orders_customers foreign key (customer_id) references customers (id);

这里咱们能够有两个咱们曾经探讨过的问题:

  1. 两个表的元数据须要调整,所以咱们须要两个锁 — 如果一个锁取得了,然而第二个锁没有取得,而咱们又在期待它,咱们就会遇到阻塞问题(对两个表都是如此!)。
  2. 当一个外键被引入时,Postgres 须要为援用表中应用的每个值查看该值在被援用表中是否存在。这可能须要一些工夫 — 而在这段时间里,锁将被保留。

为了防止这些问题:

  • 应用两步办法:首先,用 not valid 选项定义外键,而后,在一个独自的事务中,运行 alter table ... validate constraint ...;
  • 当第一个 ALTER 的时候,不要遗记咱们下面探讨的重试逻辑。留神,须要两个表级的独占锁。

案例 9. 不小心删除一个 FK

当须要删除一个 FK 时,除了不须要进行数据查看外,还必须采纳与后面的状况相似的思考。所以,当删除一个 FK 时,咱们须要取得两个表级的独占锁,具备低 lock_timeout 的重试逻辑能够使咱们免于阻塞问题的危险。

案例 10. 不小心增加了一个 CHECK 束缚

CHECK 束缚是一个弱小的、真正有用的机制。我十分喜爱它们,因为它们能够帮忙咱们定义一个严格的数据模型,在这个模型中,次要的查看都是在数据库方面实现的,所以咱们有一个牢靠的高数据质量保证。
增加 CHECK 束缚的问题与增加外键束缚十分类似 — 但它更简略,因为咱们只须要解决一个表(可怜的是,你不能在 CHECK 束缚中援用其余表)。当咱们在一个大表上增加这样的束缚时,须要进行全表扫描以确保没有违反束缚的状况。这须要工夫,在此期间,咱们有一个局部的停机工夫 — 不可能对表进行查问。(还记得 DDL + 大量数据变动的状况吗?这里咱们有一个子案例)。
侥幸的是,CHECKs 反对与咱们看到的 FKs 雷同的办法:首先,咱们通过增加 not valid 选项来定义这个束缚。接下来,在一个独自的事务中,咱们执行验证:alter table ... validate constraint ...;
放弃这样的束缚并不意味着任何危险(只管,咱们依然不应该遗记在运行 ALTER 命令时应用低 lock_timeout 的重试逻辑)。

案例 11. 不小心增加 NOT NULL

这是我最喜爱的案例之一。它十分乏味,而且常常被忽视,因为在中小型表中,它的负面影响能够不被留神到。然而在一个有比如说 10 亿行的表上,这种状况会导致局部停机。
当咱们须要禁止列 col1 中的 NULL 时,有两种风行的办法:

  1. 应用一个带有表达式的 CHECK 束缚:alter table ... add constraint ... (col1 is not null)
  2. 应用一个「惯例」的 NOT NULL 束缚:alter table ... alter column c1 set not null
    后者的问题是,与 CHECK 束缚不同,惯例 NOT NULL 的定义不能以「在线形式」进行,分两步进行,正如咱们看到的 FK 和 CHECK。

能够说,咱们总是应用 CHECKs 吧。批准 – 这些办法在语义上是雷同的。然而,有一种重要的状况,即只有惯例的 NOT NULL 才实用 — 当咱们在一个有大量数据的现有表上定义(或从新定义)一个主键时。在这种状况下,咱们必须在主键定义中应用的所有列上设置 NOT NULL,否则咱们会忽然进行全表扫描,以隐含的形式装置 NOT NULL 束缚。

如何解决这个问题?这取决于 Postgres 的版本:

  • 在 Postgres 11 之前,没有「官网」的办法来防止局部停机。惟一的办法是确保没有值违反束缚,并明确编辑系统目录,当然,这并不举荐。
  • 从 Postgres 11 开始,如果 NOT NULL 必须装置在一个新的列上(当咱们议论 PK 定义时,经常出现这种状况),咱们能够应用一个不错的技巧:

    • 首先,增加一列 not null default -1(思考到该列是 int8 类型的;在这里咱们受害于 Postgres 11 中引入的一个平凡的优化 — 疾速创立具备默认值的列;咱们的 NOT NULL 是主动引入和执行的,因为所有现有的记录在新的列中失去了 -1,所以没有 NULL 值存在)。
    • 而后用数值回填所有现有的行。
    • 最初,删除 DEFAULT -- NOT NULL 束缚将保留在其地位上。
  • 最初,在 Postgres 12 中,另一项平凡的优化使得在任何列上以齐全「在线」的形式引入一个惯例的、传统的 NOT NULL 成为可能。须要做的是:首先,创立一个带有(… is not null)表达式的 CHECK 束缚。接下来,定义一个惯例的 NOT NULL 束缚 — 因为新的优化,强制扫描将被跳过,因为当初 Postgres 明确没有 NULL 的存在,这要感激 CHECK 束缚。最初,CHECK 束缚能够被放弃,因为它对于咱们的惯例 NOT NULL 束缚来说是多余的。

案例 12. 不小心扭转了列的数据类型

咱们不能简略地扭转一个列的数据类型而不思考阻塞的问题。在大多数状况下,当你收回一个简略的 alter table t1 alter column c2 type int8; 时,你有可能失去一个残缺的表重写。

该怎么做呢?创立一个新的列,定义一个触发器来镜像旧列的值,回填(分批进行,管制死的图元和收缩),而后将你的应用程序切换到应用新的列,在齐全切换时丢掉旧的列。

案例 13. 大意的 CREATE INDEX

这是一个广为人知的事实 — 你不应该在 OLTP 背景下应用 CREATE INDEX,除非它是一个全新表的索引,还没有人在应用。
每个人都应该应用 CREATE INDEX CONCURRENTLY。尽管,有一些注意事项须要记住:

  • 它的速度大概是一般 CREATE INDEX 的两倍
  • 不能在事务块中应用
  • 如果它失败了(如果你建设的是惟一索引,机会不是 0),会给表留下一个有效的索引定义,所以:

    • 部署零碎必须筹备好重试创立索引
    • 失败后,须要进行清理

案例 14. 大意的 DROP INDEX

与 CREATE INDEX 不同,DROP INDEX 的惟一问题是,它可能导致锁的获取问题(见案例 7)。尽管对于 ALTER 来说,没有什么能够用来与长期期待或失败的锁获取相干的问题,但对于 DROP INDEX 来说,Postgres 有 DROP INDEX CONCURRENTLY。这看起来是不均衡的,然而可能能够解释为,与 ALTER 相比,从新创立索引可能是更常常须要的(另外,REINDEX CONCURRENTLY 是在 Postgres 12 中增加的)。

案例 15. 重命名对象

在一个接管大量 SQL 流量的大型数据库中,重命名一个表或一个列可能成为一项非同小可的工作。
重命名看起来并不是一项艰巨的工作 — 直到咱们看一下利用程序代码如何与数据库一起工作,以及如何在两端部署变动。PostgreSQL 的 DDL 反对事务。(嗯,除了 CREATE INDEX CONCURRENTLY。以及咱们须要分批解决的事实。以及防止长时间的独占锁。还有咱们曾经探讨过的所有其余花里胡哨的性能 …… ) 现实状况下,利用程序代码的部署 — 在咱们领有的所有节点上,可能是成千盈百个节点 — 应该产生在同一个事务中,所以当重命名提交时,所有利用节点曾经有了新版本的代码。
当然,这是不可能的。因而,当重命名时,咱们须要找到一种办法来防止利用程序代码和数据库 schema 之间的不统一 – 否则,用户将在相当长的一段时间内收到谬误。
一种办法能够是:先部署利用变动,调整代码以了解新旧(尚未部署)schema 版本。而后部署 DB 的变动。最初,部署另一个应用程序的代码变动(清理)。
另一种办法是更多的数据变动密集型,但一旦适当的自动化,它可能更容易为开发人员所应用。它相似于案例 12(扭转列的数据类型)中曾经形容过的内容:

  • 创立一个新的列(有一个新的名字)
  • 定义一个触发器来镜像旧列的值
  • 回填(分批进行,管制死的图元和臃肿)
  • 将你的应用程序切换到应用新的列
  • 当齐全切换时,抛弃旧列

案例 16. 增加一个带有 DEFAULT 的列

正如咱们曾经提到的,在 Postgres 11 之前,用默认值增加列是一项非同小可的工作,而且数据变化很大(默认状况下,意味着要重写整个表)。如果你错过了这个性能,请浏览 Postgres 11 中的一个缺失环节:用默认值疾速创立列,作者是 @brandur。
这是一个完满的例子,阐明长期以来苦楚的变动类型能够齐全自动化,因而 DB 模式变动的开发和部署变得简略和无风险。

案例 17. CREATE INDEX CONCURRENTLY 的遗留问题

正如咱们在案例 13 中曾经探讨过的,一个失败的 CREATE INDEX CONCURRENTLY 会留下一个有效的索引。如果迁徙脚本没有预料到这一点,全自动的重试就会被阻止,所以须要人工干预。为了使重试齐全自动化,在运行 CREATE INDEX CONCURRENTLY 之前,咱们应该查看 pg_indexes:

test=# select indexrelid, indexrelid::regclass as indexname, indisvalid
from pg_index
where not indisvalid and indexrelid::regclass::text = 'mytable_title_idx';

 indexrelid |     indexname     | indisvalid
------------+-------------------+------------
      26401 | mytable_title_idx | f
(1 row)

如果你应用的框架激励创立具备不可预测名称的索引,那么这里就会变得复杂 — 通常状况下,对名称进行管制会更好,使清理工作的施行变得简单明了。

案例 18. 大表的 4 字节整数主键

这是一个很大的话题,值得独自写一篇文章。在大多数状况下,在定义一个新的表时,应用 int4 的 PK 是没有意义的 — 这里的好消息是,大多数风行的框架,如 Rails,Django 曾经转而应用 int8。我集体倡议始终应用 int8,即便你当初不指望你的表会增长 — 如果我的项目胜利,状况可能会扭转。
对于那些依然偏向于在代用 PK 中应用 int4 的人,我有一个问题。思考一个有 10 亿行的表,有两列 — 一个整数和一个工夫戳。你是否会看到该表的两个版本(id int4, ts timestamptz)和(id int8, ts timestamptz)在大小上的差别。答案可能会让你感到诧异(在这种状况下,请浏览列式俄罗斯方块。

倡议

除了为每个具体案例提供的倡议外,这里还有一些一般性的倡议,没有具体的程序:

  • 测试、测试、测试。在测试中应用实在的数据量。正如曾经提到的,数据库实验室引擎 DLE 对它十分有用。
  • 在测试时,留神独占锁的持有工夫。看看 DLE 的组件 DB Migration Checker,它能够帮忙你在 CI/CD 管道中主动进行这种测试。
  • 对于扩大的锁剖析,参考我的博客文章中对于锁树剖析的片段。
  • 为部署建设更好的自动化。有很多很好的自动化例子,这些助手库能够防止在 DB 迁徙部署期间(和之后)的停机工夫和性能问题。GitLab 的 migration_helpers.rb 就是这样一套辅助工具的榜样。
  • 向别人学习并分享你的常识 如果你有其余想法,能够在下面的列表中提及,请给我发邮件(nik@postgres.ai)或在 Twitter 上分割我:@samokhvalov;我很乐意探讨这个问题。

    本文由博客一文多发平台 OpenWrite 公布!

正文完
 0