把数据库部署写成 PostgreSQL 程序

2026-08-26 24 预计阅读时间: 1 分钟
来源: postgr.es AI 摘要 Original link

Disclaimer: This article is an AI-assisted summary. Read it together with the original source when precision matters. The summary may omit context, version differences, or edge cases and is not official documentation.

预计阅读时间:11 分钟

数据库迁移工具解决了执行问题,却不一定能表达完整的部署策略。发布流程一旦需要“在某个迁移之后运行检查”“在事务之外创建并发索引”或“只有校验通过才能提交”,团队通常会开始寻找新的参数、钩子或插件。

这些需求背后有一个更基础的事实:迁移工具本质上是一个执行 SQL 的程序。它决定哪些 SQL 运行、运行顺序、是否包在事务中,以及结果能否提交。与其不断等待工具增加配置项,不如反过来,让项目用 SQL 拥有这部分策略。

从工具配置转向项目策略

传统迁移流程通常是:工具扫描文件名,按顺序执行 SQL,再根据自身规则提交事务。文件名能够表达简单顺序,却很难表达完整的数据库语义,例如:

  • 某个结构变更完成后,必须立即验证数据完整性;
  • 某个索引必须使用 CREATE INDEX CONCURRENTLY,不能放进事务;
  • 一组迁移需要共享事务,而另一条迁移必须独立提交;
  • 即使 SQL 执行成功,只要校验结果不满足条件,整个发布也不能提交。

在这种视角下,迁移工具的配置不是部署策略本身,而只是工具提供的策略语言。工具没有对应的表达能力,就只能提交功能请求。

更灵活的做法是让工具只负责准备一个 PostgreSQL 会话,并把项目中的迁移、阶段、依赖关系和执行状态物化为会话中的关系。之后,由项目拥有的 SQL 程序读取这些关系,选择并排序工作,控制事务边界,执行验证,并决定是否允许提交。

换句话说,工具负责“怎么执行”,项目负责“执行什么以及什么结果才算成功”。

PostgreSQL 会话就是策略运行时

PostgreSQL 适合承载这种部署程序,是因为它不仅能执行数据变更,也能表达检查和控制逻辑:

  • 表可以表示待执行的迁移计划和当前状态;
  • 查询可以实现排序、依赖选择和条件过滤;
  • DO 块或存储过程可以执行验证逻辑;
  • 事务可以明确划分提交边界;
  • RAISE EXCEPTION 可以阻止不合格的结果提交;
  • advisory lock 可以避免两个部署进程同时操作同一个数据库。

这里的关键不是把所有发布系统都塞进数据库,而是把数据库语义交给最了解它的地方。云资源创建、密钥读取、人工审批和流水线权限仍然可以由外部系统负责;数据库程序只处理迁移选择、执行顺序、事务控制和结果验证。

可以把一个部署过程抽象成下面的关系:

project_migrations  ->  选择待执行工作
                     ->  排序并执行 SQL
                     ->  验证数据库状态
                     ->  允许提交或抛出异常

一个可改造的最小示例

下面的示例假设迁移工具会在当前 PostgreSQL 会话中准备一张临时表 deploy_migrations。实际工具可以使用不同的表名和字段,但契约可以保持类似:

  • migration_id:迁移唯一标识;
  • phase:迁移阶段;
  • sql_body:待执行 SQL;
  • transactional:是否允许在事务中执行;
  • applied:是否已经执行。

下面的 SQL 展示了一个事务阶段:它获取部署锁,按项目定义的顺序执行变更,检查约束结果,并在检查失败时通过异常阻止提交。示例中的 deploy_migrations 和迁移内容是可运行的假设数据,便于在测试数据库中改造验证。

-- 运行环境:PostgreSQL 14+。请在专用测试数据库中执行。
BEGIN;

SELECT pg_advisory_xact_lock(hashtextextended('app-schema-deploy', 0));

CREATE TEMP TABLE deploy_migrations (
    migration_id text PRIMARY KEY,
    phase integer NOT NULL,
    sql_body text NOT NULL,
    transactional boolean NOT NULL,
    applied boolean NOT NULL DEFAULT false
);

INSERT INTO deploy_migrations (migration_id, phase, sql_body, transactional)
VALUES
    (
        '20250301_add_accounts',
        10,
        'CREATE TABLE IF NOT EXISTS accounts (
             id bigint PRIMARY KEY,
             email text NOT NULL,
             created_at timestamptz NOT NULL DEFAULT now()
         )',
        true
    ),
    (
        '20250302_add_email_check',
        20,
        'ALTER TABLE accounts
           ADD CONSTRAINT accounts_email_not_blank
           CHECK (length(trim(email)) > 0)',
        true
    );

DO $deploy$
DECLARE
    migration record;
    bad_rows bigint;
BEGIN
    FOR migration IN
        SELECT migration_id, sql_body
        FROM deploy_migrations
        WHERE NOT applied
          AND transactional
        ORDER BY phase, migration_id
    LOOP
        RAISE NOTICE 'applying %', migration.migration_id;
        EXECUTE migration.sql_body;
    END LOOP;

    SELECT count(*)
      INTO bad_rows
      FROM accounts
     WHERE length(trim(email)) = 0;

    IF bad_rows > 0 THEN
        RAISE EXCEPTION 'deployment validation failed: % invalid accounts', bad_rows;
    END IF;
END
$deploy$;

COMMIT;

这个例子只演示机制,不代表所有迁移工具都会接受动态 SQL 或把迁移正文存进关系。生产实现需要补充迁移审计表、幂等规则、失败恢复策略和权限控制。最重要的是,迁移顺序与校验条件已经成为项目代码的一部分,而不是隐藏在某个工具的命令行开关中。

事务边界必须显式建模

CREATE INDEX CONCURRENTLY 是最容易暴露传统迁移模型局限性的例子。它不能在事务块中执行,因此不能简单地放进一个“所有迁移都包在 BEGINCOMMIT 之间”的流程。

可以把部署拆成两个明确阶段:事务阶段负责表结构和约束,自动提交阶段负责并发索引。示例命令如下,运行前请将连接字符串替换成目标数据库连接信息:

#!/usr/bin/env bash
set -euo pipefail

export DATABASE_URL='postgresql://app:secret@localhost:5432/app'

psql "$DATABASE_URL" -v ON_ERROR_STOP=1 <<'SQL'
BEGIN;
SELECT pg_advisory_xact_lock(hashtextextended('app-schema-deploy', 0));

ALTER TABLE accounts ADD COLUMN IF NOT EXISTS normalized_email text;

UPDATE accounts
   SET normalized_email = lower(trim(email))
 WHERE normalized_email IS NULL;

DO $check$
BEGIN
    IF EXISTS (
        SELECT 1 FROM accounts
        WHERE normalized_email IS NULL OR normalized_email = ''
    ) THEN
        RAISE EXCEPTION 'normalized_email validation failed';
    END IF;
END
$check$;

COMMIT;
SQL

# 该命令运行在事务之外,故意保留为独立部署阶段。
psql "$DATABASE_URL" -v ON_ERROR_STOP=1 -c \
  'CREATE UNIQUE INDEX CONCURRENTLY IF NOT EXISTS accounts_normalized_email_uq
     ON accounts (normalized_email);'

如果项目策略规定“并发索引成功后还要再次验证”,就继续把验证写成数据库程序的一部分,并在成功后记录部署状态。不能把并发索引和普通事务变更假设成同一种工作单元,否则失败时的恢复行为会变得含糊。

这种倒置带来的代价

把策略交给项目并不意味着系统自动变简单。成本会从“等待迁移工具支持”转移到项目自身:

  • 项目需要维护部署关系的结构和生命周期;
  • 团队需要定义幂等、重试、回滚和部分成功的语义;
  • 动态执行 SQL 会提高权限设计和审计要求;
  • 多个部署进程之间必须使用锁或其他并发控制;
  • 迁移程序本身需要测试,不能只测试单个 SQL 文件;
  • 运维人员需要理解数据库中的状态,而不只是流水线日志。

还有一个边界必须保持清楚:数据库内的 SQL 程序不能替代云平台审批、密钥服务、发布权限和跨服务编排。它只应该拥有数据库内部的决策权。把外部系统控制逻辑强行搬进 PostgreSQL,通常会扩大故障半径,也会让权限模型更难审计。

采用前的检查清单

可以先从一个需要特殊事务语义的真实迁移开始试验,而不是一次性重写全部发布系统:

  • 明确工具会向 PostgreSQL 会话提供哪些关系和字段;
  • 将迁移选择、排序和依赖规则写成可测试的 SQL;
  • 为事务迁移、自动提交迁移和验证阶段定义清晰边界;
  • 使用 advisory lock 防止并发部署;
  • 记录每个迁移的状态、执行时间和失败原因;
  • 在测试数据库中演练中断、重试和部分完成;
  • 保留外部流水线对审批、凭据和云资源的控制。

当部署策略真的属于项目,而不是迁移工具的配置时,工具升级不再决定项目能表达什么。项目可以用 PostgreSQL 自己的关系、事务和异常机制,精确描述哪些工作应该发生,以及什么结果才允许发布完成。


相关推荐