用扩展/收缩模式实现零停机架构变更
通过扩展/收缩模式、安全回填、兼容发布、验证和回滚,规划并完成零停机架构变更。

为什么架构变更会导致故障
当应用版本、后台工作进程和数据库不再对哪些结构和值有效达成一致时,架构变更就会引发故障。问题可能很明显,例如每个请求都报错,也可能逐渐出现,例如查询延迟上升、写入失败、副本延迟,以及需要重新执行的一批任务。
生产环境的部署很少能一次替换所有进程。滚动发布会让旧版和新版应用实例同时运行。长期运行的工作进程可能数小时都保留旧构建,移动客户端可能数月仍在使用,报表或集成任务也可能绕过主应用直接使用表。它们共用同一个数据库。
常见故障包括:
- 新代码在创建列的迁移完成前就写入该列。
- 旧代码读取了后续版本重命名或删除的表或列。
- 表重写、回填或建索引占用大量 I/O 和 CPU,拖慢正常流量。
- 架构命令等待锁,后面的请求不断堆积。
- 新约束拒绝了尚未升级进程发出的写入。
真正危险的往往是获取锁,而不是命令标称的执行时间。一条很快的 ALTER TABLE 可能在长事务后面等待。等待期间,后续查询可能排在待处理的架构锁后面,让一次小迁移演变成全应用停滞。
要做到零停机,每个过渡中的数据库状态都必须能被仍可能运行的每个应用版本使用。先添加兼容结构,再有节奏地迁移流量和数据,直到最后一个旧路径使用方消失后,才移除旧路径。
对于有实时流量、滚动部署、严格可用性目标或恢复代价高的系统,这样做很有必要。数据库安静的小型内部工具,经过测试的维护窗口可能更合适。应根据故障成本和迁移的运维复杂度来决定。
用简单的话解释扩展/收缩
扩展/收缩模式把一次不兼容变更转成一系列兼容发布。数据库会暂时支持两种表示,代码和数据从旧表示逐步迁移到新表示。
流程分为三部分:
- 扩展:添加列、表、索引或约束,不删除当前代码所需的任何内容。
- 过渡:部署兼容代码,迁移历史数据,并把读写引导到新表示。
- 收缩:验证确认旧对象无人使用后,删除旧代码和数据库对象。
假设 PostgreSQL 表把人的姓名存储在 full_name 中,应用需要单独的 first_name 和 last_name 字段。扩展阶段添加可空列,同时保留 full_name。兼容版本会在过渡期写入所需的两种表示。回填会拆分已有值,并为无法可靠拆分的姓名制定明确规则。新字段足够完整后,读取才会迁移过去。之后的收缩阶段再删除 full_name。
这个顺序适合滚动部署,因为旧构建仍能找到 full_name,新构建则能找到全部三列。它也保留了应用回滚路径。新版本表现异常时,之前的构建仍可运行,因为它依赖的架构尚未被删除。
数据库回滚和应用回滚不同。数据已经转换后,再反向迁移可能丢失信息或恢复过期值。过渡期间,更适合把应用流量切回已知表示,同时保留新增的数据库对象。等事故稳定后再修正向前迁移。
这套模式不代表每项变更都需要双写。只有新代码使用的可选列,可能只需一次新增迁移和一次部署。重命名、表示方式变更、拆表和必填字段变更通常需要更多阶段,因为两个应用版本否则无法安全共用架构。
选择步骤前先给变更分类
迁移计划应匹配操作实际的锁、重写、兼容性和数据转换风险。把每条 ALTER TABLE 都当成同一类操作,要么流程过重,要么发布不安全。
新增变更通常最简单。可空列、独立表,或通过在线方式创建的索引,通常可在应用代码使用前引入。命令仍需要锁,因此应在接近生产环境的表和事务负载上测试其行为。
破坏性变更包括删除或重命名列、缩窄类型、替换表和增加更严格的约束。这些变更会使现有代码的某项假设失效。应把它们放在收缩阶段,等代码引用和外部使用方都移除后再做。
改变数据的操作需要单独评估。转换时间戳、规范化电话号码、合并记录或拆分自由文本,都可能丢失信息。回填开始前,要明确如何处理无效值和有歧义的值。如果转换不可逆,应保留源数据,直到结果通过业务层面的检查。
实用的预检应回答五个问题:
- 每条语句要请求什么锁,可能等待或持有多久?
- 操作会重写表、产生大量 WAL,还是增加副本延迟?
- 哪些应用、任务、报表和变更数据捕获使用受影响对象?
- 当前版本和计划版本能否在每个过渡状态下运行?
- 什么信号会暂停操作,暂停后会精确保留什么状态?
要在数据量和分布都真实的环境中运行完全相同的迁移。一张有一千行整齐数据的测试表,无法说明包含数亿行、宽元组、死行、倾斜值和长事务的生产表会怎样。
在 PostgreSQL 中安全扩展
安全的 PostgreSQL 扩展会使用短暂的元数据变更、有限的锁等待,以及数据库要求的独立在线操作。部署依赖新结构的代码前,先添加该结构。
添加没有默认值的可空列通常是很短的元数据操作:
BEGIN;
SET LOCAL lock_timeout = '2s';
SET LOCAL statement_timeout = '30s';
ALTER TABLE customers
ADD COLUMN phone_e164 text;
COMMIT;
超时设置可防止发布在未结束事务后面无限等待。如果无法迅速获得锁,让迁移失败,检查阻塞方,并在更安全的时机重试。不要在紧密循环中自动重试,因为反复请求锁会持续干扰生产流量。
现代 PostgreSQL 版本可添加带常量默认值的列,而无需立即把值写入所有已有行。这项优化不代表任何默认值都无害。易变表达式可能需要重写表,ALTER TABLE 仍需短暂的 ACCESS EXCLUSIVE 锁。应根据已部署的 PostgreSQL 版本和确切表达式确认行为,不要只依赖一般规则。
普通 CREATE INDEX 可能阻塞写入。表必须保持可写时,使用并发创建:
CREATE INDEX CONCURRENTLY idx_customers_phone_e164
ON customers (phone_e164);
CREATE INDEX CONCURRENTLY 不能在事务块中运行。它耗时更长、额外工作更多,也可能等待较早的事务,但普通插入、更新和删除可继续进行。它仍会消耗 CPU、I/O 和 WAL,运行期间请监控数据库延迟和副本。
失败的并发构建可能留下无效索引。重试前检查索引状态,再有意识地删除或重建无效对象。会把每个迁移文件包进事务的工具,需要为并发索引操作提供受支持的非事务模式。
新建表通常比原地转换更容易引入。对于一对多或多对多关系,可添加目标表及其索引,同时保留源列。等新写入、历史数据、读取和下游使用方都迁移后,再删除源对象。
类型变更要格外谨慎。有些只改元数据,有些会重写每一行,或持有过久的限制性锁。对于高风险转换,添加目标类型的列,分批填充,切换应用访问,最后再删除原列。这样团队还能记录转换失败,而不是让一次大规模 ALTER COLUMN TYPE 要么全部成功,要么全部失败。
部署保持兼容的代码
兼容的应用代码能处理过渡值缺失,且不会在同一次发布中依赖破坏性迁移。数据库扩展必须在第一个应用实例开始使用新对象前完成。
当两种表示都要保持最新时,双写很有用。尽可能在同一个数据库事务中完成两次写入。异步的第二次写入可能在第一次成功后失败,造成不一致,后续读取可能暴露这个问题。
双写逻辑还需要明确唯一权威。如果 phone_e164 从 phone 派生,就要规定两者都提供时以哪个输入为准,并在 API 处理器、工作进程、导入和管理工具中采用同样的规范化方式。否则两条看似正确的代码路径会存下不同结果。
读取应晚于写入迁移。新写入同时填充两种形式、回填处理历史行期间,读取继续使用既有字段。验证后,部署优先使用新字段的读取路径,并只在明确的回退规则下使用旧值。要衡量回退使用量。无声存在的回退会永久掩盖不完整数据。
典型发布顺序如下:
- 发布 1:添加新数据库对象,不改变应用行为。
- 发布 2:写入过渡表示,继续沿用既有读取。
- 发布 3:回填和一致性检查通过后切换读取。
- 发布 4:回滚条件过期后,停止维护旧表示。
- 发布 5:删除旧代码引用,之后再清理数据库。
把公开 API 合约与物理架构变更分开。数据库列改名,不需要立即改 Web、移动端或集成响应中的字段名。通过独立的兼容策略变更这些合约,尤其在客户端无法随服务器升级时。
盘点所有写入方。HTTP 处理器只是变更来源之一。队列消费者、定时任务、导入脚本、数据修复工具、数据库触发器和直接管理操作,都可能继续产生旧形状的数据。可行时给数据库连接标注应用名称,并记录过渡路径的使用情况,让遗漏进程暴露出来。
长期运行的进程可能通过预处理语句、缓存元数据或对象关系映射层保留过时假设。收缩前测试滚动重启和连接池行为。一个近期没有流量的进程,仍可能在罕见任务首次运行时失败。
回填数据,不压垮数据库
安全的回填使用小而可恢复的批次,并在生产环境健康状况恶化时减速。只有在线写入方已能维护新表示后,才开始回填。
根据耗时和数据库影响选择批次,不要套用固定行数。一千行窄数据可能几毫秒完成,而包含大值或昂贵转换的一千行可能产生显著 I/O。先保守设置,目标是让事务在数秒内完成。批次之间提交,避免锁和旧行版本在一个事务中积累。
PostgreSQL 不支持在普通 UPDATE 上直接使用 ORDER BY 和 LIMIT。可先在公用表表达式中选出一批行,再更新这些行:
WITH batch AS (
SELECT id
FROM my_table
WHERE id > $1
AND new_col IS NULL
ORDER BY id
LIMIT 1000
)
UPDATE my_table AS target
SET new_col = transform_expression(target.old_col)
FROM batch
WHERE target.id = batch.id
AND target.new_col IS NULL
RETURNING target.id;
应用把已完成的最大 id 记录为游标。条件更新使重复运行具备幂等性,因此提交后崩溃也不会破坏已处理的行。记录进度时要确保游标不会越过尚未提交的批次。
递增 id 游标可避免反复扫描表开头,但不会发现游标之后才被修正的行,也不会发现插入到游标之前的行。最后应扫描所有剩余 NULL 值进行补漏。如果标识符无序,或行可能在符合条件与不符合条件之间变化,应使用工作表或其他明确检查点,而不要假设一次向前扫描就已完成。
多个工作进程可借助 FOR UPDATE SKIP LOCKED 认领行,但并行会增加写入压力,也让进度跟踪更复杂。不要把被跳过的行和永久前进的游标混用。对并行工作进程而言,已认领标识符的队列或反复扫描符合条件的行更安全。
根据生产指标限速,例如查询延迟、活跃连接数、锁等待、WAL 生成量、副本回放延迟和死行增长。超过阈值时暂停,再从检查点恢复。固定休眠容易实现,但由数据库反馈驱动的方式更能适应流量变化。
避免修改每一行,只有需要处理的行才更新。可按新字段、源状态或迁移标记过滤。如果转换昂贵,并且一致性允许,可在更新事务外计算,再发出短小的条件写入。记录被拒绝值的数量和样本,而不是悄悄编造数据。
每次更新后,自动清理和副本都需要消化这些工作。回填可能在主库顺利完成,但副本大幅落后,或表膨胀影响后续查询。限速不仅要考虑批次的即时耗时,也要考虑这些延后成本。
验证数据和生产流量
只有数据检查、应用遥测和依赖证据都表明新路径已成为权威,迁移才可以进入收缩阶段。仅凭任务完成计数器无法证明正确性。
先检查完整性和一致性。PostgreSQL 的 IS DISTINCT FROM 可显式处理 NULL 并比较值,与任一边为 NULL 就产生未知结果的 <> 不同:
SELECT count(*)
FROM customers
WHERE normalize_phone(phone) IS DISTINCT FROM phone_e164;
不要在繁忙的大型表上反复执行未建索引的全表计数。可进行一次受控验证,限制标识符范围、抽样,或使用遍历全表的临时验证流程。合适方法取决于出错成本和数据库可用余量。
验证应涵盖:
- 需要新字段的行中不存在意外缺失值。
- 新值符合约定转换,包括格式错误和空输入。
- 历史数据处理结束后,新行和更新仍保持一致。
- 回退读取已达到计划阈值,对服务器控制的流量通常应为零。
- 错误率、查询延迟、锁和副本延迟都在发布限制内。
还要比较业务结果,而不仅是列。如果迁移改变价格、权限、账户状态或标识符,应验证用户依赖的总量和不变量。两列可能在技术上匹配,却都编码了错误的业务规则。
清理前观察一个完整运行周期。正确的时长取决于真实系统行为,不是固定一周。它可能需要覆盖月末处理、低频计费任务、延迟的队列重试,或旧移动客户端的最长存活期。记录每个使用方已迁移的证据。
应用架构允许时,可对读取切换进行金丝雀发布。让少量流量走新读取路径,对比结果,再逐步扩大。回滚动作要简单:把读取重定向到既有表示,不要反向回填。
数据准备好后再加约束
只有所有写入方都遵守规则、已有数据已验证后,约束才应变得严格。在扩展阶段强制 NOT NULL、检查约束或外键,可能阻塞流量,或拒绝旧进程写入。
PostgreSQL 可把检查约束添加为 NOT VALID,这样会对新增或修改的行执行规则,却不会立即扫描所有历史行。回填后再单独验证:
ALTER TABLE customers
ADD CONSTRAINT customers_phone_e164_present
CHECK (phone_e164 IS NOT NULL) NOT VALID;
ALTER TABLE customers
VALIDATE CONSTRAINT customers_phone_e164_present;
验证成功后,受支持的 PostgreSQL 版本可在将列设为 NOT NULL 时利用这一证明,避免再次全表扫描。最终修改仍需较强的表锁,因此应设置有限锁超时和重试计划:
ALTER TABLE customers
ALTER COLUMN phone_e164 SET NOT NULL;
ALTER TABLE customers
DROP CONSTRAINT customers_phone_e164_present;
临时检查约束如果仍有价值可以保留,但保留等价约束只会增加系统目录负担,不会改变规则。
外键也可采用类似的 NOT VALID 和 VALIDATE CONSTRAINT 流程。创建约束后会检查新写入,历史数据验证则留到之后。如果删除或更新被引用关系时可能造成昂贵扫描,应有意识地添加支持索引。
应用验证应先于数据库强制执行,但不能替代数据库约束。代码能给用户更清晰的错误,数据库则保护所有路径写入的数据。发布期间监控约束冲突,以发现依赖审计遗漏的写入方。
安全收缩旧路径
收缩阶段应先移除应用依赖,再移除数据库对象。遥测和验证确认新路径已成为权威后,可通过独立发布进行清理。
先停止读取旧字段并删除回退逻辑。再停用旧字段写入,观察生产环境足够长的时间以捕获罕见路径。移除涉及旧表示的功能开关、触发器、兼容视图、修复脚本和定时任务。搜索导出的源代码和迁移代码,也要检查主仓库外的报表、集成查询和变更数据捕获配置。
安全的清理顺序是:
- 删除回退读取,并确认遥测中不再出现。
- 停止旧写入并删除同步代码。
- 从所有可部署版本中移除应用引用。
- 使用合适的在线方式删除废弃索引和约束。
- 在后续数据库发布中删除旧列或旧表。
删除 PostgreSQL 列主要是系统目录变更,但仍需 ACCESS EXCLUSIVE 锁。因此短语句也可能在长事务后面等待,并阻塞后续工作。设置锁超时,事先检查长事务,并选择风险较低的时段尝试。
废弃索引若不能接受阻塞写入,请使用 DROP INDEX CONCURRENTLY。和并发创建一样,它不能在事务块内运行,迁移工具必须处理其限制。
不要在同一次发布中同时清理代码和物理删除对象。分开后,已清理的应用仍可对着包含未使用对象的数据库运行。出现应用问题时,仍可回滚,而无需重建架构或还原数据。
删除表前,检查序列、视图、函数、授权、触发器、复制发布和外部查询的归属。生产迁移中不要把 CASCADE 当捷径,因为它可能删除本不在计划内的依赖。
处理回滚和失败步骤
回滚计划应为每个阶段定义安全动作,而不是依赖一套通用的向下迁移。新增对象、数据迁移、读取切换和删除,恢复特性各不相同。
扩展阶段无法获得锁时,保持应用不变,解决阻塞事务后再重试。并发索引构建失败时,检查是否留下无效索引,清理该对象后再尝试。
回填造成负载时,暂停它。已提交且具备幂等性的批次可以保留。降低批次大小或速率,解决昂贵转换后从检查点恢复。撤销数百万条正确更新通常只会增加风险,无助于生产环境恢复。
新读取路径返回错误结果时,把读取切回旧表示,同时保留新数据以便诊断。只有确认正确时才继续双写。写入方本身有问题时,先禁用它或回滚应用,再修复受影响行。
收缩之后,恢复可能需要还原数据,而不只是部署旧构建。明确不可逆点。按系统恢复策略创建所需备份或快照,发布前测试恢复;存储成本允许时,在约定保留期内保留旧对象。
架构命令可能具备事务性,但外部影响并不总在其中。并发索引操作、队列消息、缓存变更和应用部署不会共享一个原子事务。运行手册应描述每种部分失败后的可观察状态,以及能从该状态安全继续执行的命令。
避免常见迁移陷阱
大多数失败的零停机迁移,要么过早强制新状态,要么遗漏了旧状态的某个使用方。审批前应明确审查以下陷阱:
- 旧应用实例仍可能省略字段时就添加
NOT NULL。 - 在一个事务中执行大规模回填,锁和行版本保留过久。
- 把列重命名当作新增变更,尽管旧代码仍使用原名。
- 所有写入路径和历史行尚未填充新表示时就切换读取。
- 把部署成功当成报表、工作进程、副本和集成都兼容的证明。
另一个隐蔽问题来自双向同步。触发器把 old_col 复制到 new_col,应用代码又把 new_col 写回 old_col。规范化差异或触发器顺序可能形成循环、覆盖有意设置的值,或让归属不清。优先采用单向同步,并记录每次发布期间哪种表示是权威。
默认值可能掩盖写入方未更新的问题。新必填列若收到空泛或通用默认值,旧代码看似兼容,实际却存入语义无效的数据。缺失本身具有诊断价值时,先采用可空过渡,等每个写入方都能提供有意义的值后再强制真正规则。
功能开关本身不能让不兼容的架构命令变安全。禁用的代码路径仍可能被旧进程加载、预处理或运行。数据库对象必须保留到没有任何活跃或可部署版本引用它为止。
迁移归属同样重要。指定一个人或团队负责整个过渡直至收缩,包括验证和删除日期。否则临时列、开关和同步任务可能残留数月,让之后每次变更的成本都升高。
不停机替换电话列
把 customers.phone 替换为规范化的 customers.phone_e164,需要新增列、明确转换策略、兼容代码、受控回填、读取切换和延后清理。转换策略必须先于 SQL,因为并非每个已存值都能自动规范化。
先给已有值分类。知道所需国家信息的有效号码可以转换。空白值可变为 NULL。有歧义或格式错误的号码应进入异常报告,不应猜测。还要决定产品是否要求每位客户都有电话号码,这决定之后是否适合 NOT NULL。
使用短锁超时添加列:
BEGIN;
SET LOCAL lock_timeout = '2s';
ALTER TABLE customers
ADD COLUMN phone_e164 text;
COMMIT;
部署能规范化新输入、并在同一事务中写入 phone 和 phone_e164 的代码。开始时继续从 phone 读取。更新所有写入方,包括账户导入、支持工具、工作任务,以及创建客户测试数据的测试代码。
在短事务中回填符合条件的行。记录最后处理的标识符、已转换数量、跳过数量和每类失败原因。根据生产延迟和副本延迟限制任务速率。向前扫描完成后,再扫描符合条件的 NULL 值,捕获并发插入或重启后遗漏的行。
使用与应用相同的规范化规则进行一致性检查,再人工抽查国际区号、分机、空值、重复联系人记录和旧导入数据。行数能证明覆盖范围,却不能证明电话号码正确。
部署读取路径:有 phone_e164 时返回它,仅在已记录的例外下使用 phone。监控回退使用量和规范化错误。解决剩余例外,不要让回退变成永久行为。
新字段成为权威后,移除回退并停止写入 phone。在合适的运行周期内观察罕见任务和集成流量。只有产品规则确实要求时,再添加已验证的约束。
最后删除对 phone 的代码引用。单独删除它的索引或约束,再在后续迁移中以有限锁等待删除该列。在删除前的任何时点,如果读取切换失败,都可回滚应用行为,因为两个列仍然可用。
这个例子还揭示了架构机制无法解决的领域问题:拆分或规范化人工输入的数据并不总能无损完成。迁移计划必须保留异常,并为负责人提供解决途径。
每次发布前检查
发布清单应证明兼容性、限制生产影响,并指出当前阶段的恢复动作。把证据随变更保存,事故发生时操作人员无需重新推测原意。
部署前确认:
- 应用版本可在本次发布前后的数据库状态下工作。
- 可能在流量后等待的架构命令已设置锁超时和语句超时。
- 回填或验证任务具备进度、暂停、恢复和限速控制。
- 仪表盘覆盖错误、延迟、锁、数据库负载、WAL 和副本延迟。
- 已测试回滚动作,且不依赖已经删除的对象。
记录明确的完成条件,例如完整任务周期内没有新的不一致失败、服务器控制流量的回退读取为零、所有已知使用方已升级,以及受控验证查询成功。回填期间处理百分比很有用,但处理了 100% 不等于 100% 正确。
迁移顺序要独立于代码审查进行复核。一组正确的 SQL 和应用变更,仍可能因部署按错误顺序执行而失败。明确哪些步骤必须在另一步完成后才能开始。
停止条件尽量量化。定义可接受的查询延迟、锁等待、副本延迟、错误率和批次时长。超过阈值时,操作人员应知道该暂停任务、取消等待中的语句,还是重定向读取,无需在事故期间重新寻求批准。
只有新表示已处理读写、历史数据通过验证、旧对象被删除、临时运维机制也已清除,迁移才算完成。
让流程可重复执行
可复用的迁移运行手册能把扩展/收缩变成日常发布工作,并设定明确负责人和可衡量的关卡。它应足够简短,方便在实时部署时遵循,也要足够具体,能描述部分失败后的状态。
运行手册应包含五部分:
- 扩展:确切的架构操作、预期锁、超时和事务要求。
- 兼容性:受影响的代码、写入方、读取方、开关、客户端和部署顺序。
- 回填:转换策略、分批、检查点、限速和异常处理。
- 验证:SQL 检查、业务不变量、遥测和完成阈值。
- 收缩:依赖移除、观察期、物理清理和恢复边界。
为每个过渡对象指定负责人和预期完成日期。在同一处跟踪列、索引、开关、触发器和任务。清理是迁移的一部分,不是可选维护。
使用 Koder.ai 构建的团队,可在生产变更开始前用 Planning Mode 写清这些阶段和检查点。导出源代码后,迁移 SQL 和兼容逻辑也能像其他应用代码一样接受审查。Koder.ai 支持部署、托管、快照和回滚,但不能假设应用回滚会撤销已提交的数据转换。应持续保留架构兼容性,直到数据库恢复计划不再依赖旧表示。
可行时把写入密集的工作安排在低流量时段,但不要只靠时间安排来保证安全。有限的事务、基于反馈的限速、可观察的进度和经过测试的暂停动作,才能让在线迁移在流量或数据表现与预期不同时仍可控。
常见问题
为什么架构变更会导致故障?
当旧版和新版应用依赖不同的数据库结构时,架构变更就会破坏生产环境。滚动发布期间两个版本可能同时运行,过早删除或重命名列会导致读取或写入失败。
什么是扩展/收缩迁移模式?
扩展/收缩模式把不兼容的变更拆成安全阶段。先添加新结构,再迁移代码和数据,等所有使用方都停止使用旧结构后,才将其删除。
怎样在不停机的情况下重命名或替换数据库列?
先添加新列,并保留旧列。部署能同时处理两个字段的代码,以小批次回填已有行,验证后切换读取路径,并在后续发布中删除旧列。
在 PostgreSQL 中添加列时能不阻塞流量吗?
通常可以。PostgreSQL 中,添加没有默认值的可空列往往只是很短的元数据操作,但仍需获取表锁。设置较短的锁超时,让迁移失败而不是在长事务后面持续等待。
怎样创建索引而不阻塞写入?
当表必须保持可写时,使用 CREATE INDEX CONCURRENTLY。它耗时更长、会增加数据库负载,而且不能在事务块中执行,因此运行期间要监控延迟、WAL 和副本延迟。
应用何时应同时写入旧字段和新字段?
当两种表示都必须保持最新时,应在同一个数据库事务中写入两个值。明确两者不一致时以哪个字段为准,并在 API、工作进程、导入和支持工具中使用相同的规范化规则。
怎样安全回填大型 PostgreSQL 表?
使用短小、可恢复的批次,每个批次后提交。保存检查点,只更新仍需处理的行;当查询延迟、锁等待、WAL 量或副本延迟上升时,放慢或暂停任务。
如何确认回填既完成又正确?
不要因为回填完成就切换读取路径。确认必填值都存在,对比新旧表示,监控回退读取,并确认历史数据处理后新写入仍保持一致。
何时应添加 NOT NULL、检查约束或外键?
在现有数据通过验证、所有活跃写入方都能提供有效值后,再添加严格约束。PostgreSQL 可以把某些约束作为 NOT VALID 添加,先对新行生效,再单独验证历史行。
何时可以安全移除旧的架构路径?
先移除回退读取,再停止旧写入,并观察系统度过一个完整运行周期。确认所有应用、任务、报表、集成和客户端都不再引用旧对象后,删除相关代码,并在后续发布中删除数据库列或表。