Skip to content

修改表结构(ALTER TABLE) ​

表一旦建好,业务就会变,结构也必须跟着变。ALTER TABLE 是 PostgreSQL 中风险最高的一条 DDL:它默认要拿 ACCESS EXCLUSIVE 锁,会阻塞表上的所有读写。本节讲清楚三件事——每种变更怎么写、哪些变更要重写整表、大表上怎么安全地做变更。

前置知识

阅读本节前,建议先了解:创建表(CREATE TABLE)

基础概念 ​

ALTER TABLE 的变更可以按「代价」分成三个层级,这是本节最重要的一张分类表:

代价层级含义典型操作锁级别
仅改元数据(Metadata Only)只更新系统目录,瞬间完成,与表大小无关ADD COLUMN(无默认值或有常量默认值)、DROP COLUMN、RENAME、SET DEFAULT、SET STATISTICSACCESS EXCLUSIVE(但极短)
扫描整表校验(Scan & Validate)需要读一遍全表验证,时间与行数成正比ADD CONSTRAINT ... CHECK、ADD CONSTRAINT ... FOREIGN KEY、VALIDATE CONSTRAINT校验阶段锁较弱(SHARE UPDATE EXCLUSIVE)
重写整表(Table Rewrite)物理重建整张表与其索引,产生双倍磁盘占用ALTER COLUMN TYPE(不兼容类型)、ADD COLUMN ... DEFAULT 非常量、SET LOGGED/UNLOGGEDACCESS EXCLUSIVE(全程持锁)

几个必须分清的概念:

  • 重写(Rewrite):PostgreSQL 会新建一张表文件,把旧数据逐行搬过去,再删旧文件。期间表上锁被一直持有,磁盘瞬时占用接近两倍,且产生大量 WAL(预写日志)。
  • ACCESS EXCLUSIVE 锁:PostgreSQL 中最强的表级锁,与所有其他锁模式冲突。一旦拿到,连 SELECT 都要排队。所以大表上做重写操作,等于让表"停服"。
  • 锁队列插队问题:即使变更本身很快,只要前面有一个慢查询持有锁,ALTER TABLE 的加锁请求会排在其后,而后来的所有查询又排在 ALTER TABLE 之后——形成"锁队列堵死"。lock_timeout 是必备护栏。
  • VALIDATE CONSTRAINT 两步法:先 ADD CONSTRAINT ... NOT VALID(不扫表,秒完成,只对新数据生效),再单独 VALIDATE CONSTRAINT(扫描校验旧数据,锁较轻)。这是大表加约束的标准姿势。
  • attnotnull / atthasdef / attidentity / attgenerated:pg_attribute 中与"列属性"相关的字段,查询表结构时常用。

PostgreSQL 12 起,ALTER TABLE 与 CREATE INDEX 一样支持事务内运行(DDL 也是事务性的),这意味着变更失败会自动回滚,不会留下"半改"状态。

语法与用法 ​

完整语法骨架(按用途分块,省略分区、继承、存储参数细节):

text
ALTER TABLE [ IF EXISTS ] [ ONLY ] name [ * ]
    action [, ... ]

-- ① 增删列
    ADD [ COLUMN ] [ IF NOT EXISTS ] column_name data_type [ column_constraint [ ... ] ]
    DROP [ COLUMN ] [ IF EXISTS ] column_name [ RESTRICT | CASCADE ]

-- ② 改列属性
    ALTER [ COLUMN ] column_name [ SET DATA ] TYPE data_type [ COLLATE collation ] [ USING expression ]
    ALTER [ COLUMN ] column_name SET DEFAULT expression
    ALTER [ COLUMN ] column_name DROP DEFAULT
    ALTER [ COLUMN ] column_name { SET | DROP } NOT NULL
    ALTER [ COLUMN ] column_name SET STATISTICS integer
    ALTER [ COLUMN ] column_name SET ( attribute_option = value [, ... ] )
    ALTER [ COLUMN ] column_name ADD GENERATED { ALWAYS | BY DEFAULT } AS IDENTITY [ ( sequence_options ) ]
    ALTER [ COLUMN ] column_name { SET GENERATED { ALWAYS | BY DEFAULT } | SET sequence_option | RESTART } ...
    ALTER [ COLUMN ] column_name DROP IDENTITY [ IF EXISTS ]

-- ③ 增删约束
    ADD [ CONSTRAINT constraint_name ] { CHECK | UNIQUE | PRIMARY KEY | FOREIGN KEY | EXCLUDE } ...
    ADD table_constraint [ NOT VALID ]
    DROP CONSTRAINT [ IF EXISTS ] constraint_name [ RESTRICT | CASCADE ]
    VALIDATE CONSTRAINT constraint_name
    ALTER CONSTRAINT constraint_name [ DEFERRABLE | NOT DEFERRABLE ] [ INITIALLY ... ]

-- ④ 重命名与移动
    RENAME [ COLUMN ] column_name TO new_column_name
    RENAME CONSTRAINT constraint_name TO new_constraint_name
    RENAME TO new_name
    SET SCHEMA new_schema

-- ⑤ 存储与所有权
    SET ( storage_parameter [= value] [, ... ] )
    RESET ( storage_parameter [, ... ] )
    SET LOGGED | SET UNLOGGED
    OWNER TO { new_owner | CURRENT_ROLE | CURRENT_USER | SESSION_USER }

最简可运行示例:

sql
-- 加一列(无默认值,只改元数据,瞬间完成)
ALTER TABLE users ADD COLUMN phone text;
text
ALTER TABLE

详细说明 ​

增删列:ADD COLUMN 与 DROP COLUMN ​

ADD COLUMN 的代价取决于默认值:

sql
-- 加列不指定默认值:只写系统目录,与表大小无关
ALTER TABLE orders ADD COLUMN remark text;

-- 加列并给常量默认值:PostgreSQL 11+ 也是"仅元数据",不会重写表
ALTER TABLE orders ADD COLUMN source text NOT NULL DEFAULT 'web';

-- 危险!默认值是非常量表达式 → 触发整表重写
ALTER TABLE orders ADD COLUMN seq_no bigint DEFAULT nextval('order_seq');

PostgreSQL 11 之前,加带默认值的列会重写整表;从 PostgreSQL 11 起引入了 pg_attribute.attmissingval 机制,常量默认值被记录成"缺失值",读取时才按需补上。所以下面这种写法在 PostgreSQL 11+ 上是秒完成的:

sql
-- PostgreSQL 11+:即使 NOT NULL + DEFAULT,也不重写
ALTER TABLE big_table ADD COLUMN status text NOT NULL DEFAULT 'active';

DROP COLUMN 同样是仅元数据操作——列被标记为 attisdropped = true,物理空间不立即回收,要等后续 VACUUM FULL 或表重建才真正释放。

sql
-- 删列:默认 RESTRICT
ALTER TABLE orders DROP COLUMN remark;

-- 删除被视图、外键等依赖的列,必须显式 CASCADE(会连带删掉依赖对象)
ALTER TABLE orders DROP COLUMN source CASCADE;
sql
-- 验证列确实只是"逻辑删除"
SELECT attname, attisdropped, attnum
FROM pg_attribute
WHERE attrelid = 'orders'::regclass AND attnum > 0
ORDER BY attnum;
text
     attname     | attisdropped | attnum
-----------------+--------------+--------
 id              | f            |      1
 user_id         | f            |      2
 ...
 remark          | t            |     12
 source          | t            |     13
(rows)

被删掉的列仍占 attnum,这也是 PostgreSQL 系统列编号只增不减的原因。

改列类型:ALTER COLUMN TYPE 与 USING ​

改类型是三档里最危险的,是否重写取决于新旧类型是否"二进制兼容"。

sql
-- 兼容类型:仅元数据变更(text ↔ varchar)
ALTER TABLE users ALTER COLUMN nickname TYPE varchar(64);

-- 不兼容类型:必须提供 USING 表达式,触发整表重写
ALTER TABLE users ALTER COLUMN age TYPE text USING age::text;

-- 数值精度提升:numeric(10,2) → numeric(12,2) 需要重写
ALTER TABLE products ALTER COLUMN price TYPE numeric(12,2);

不写 USING 且系统找不到隐式转换时,会直接报错:

sql
-- 错误!
ALTER TABLE users ALTER COLUMN age TYPE integer;
text
ERROR:  column "age" cannot be cast automatically to type integer
HINT:  You might need to specify "USING age::integer".

下面的对照表列出常见类型变更的代价,是决定能否在业务高峰期执行的关键依据:

原类型 → 新类型是否重写说明
varchar(n) → varchar(m)(m > n)否放宽长度限制,仅元数据
varchar(m) → varchar(n)(m > n)否*元数据变更,但需扫描校验长度,失败则报错
varchar ↔ text否完全二进制兼容
numeric(10,2) → numeric(12,2)是存储格式随精度定义变化
integer → bigint是存储宽度从 4 字节变 8 字节
text → integer是需 USING 显式转换
timestamp → timestamptz是语义变化,需 USING col AT TIME ZONE ...
加 COLLATE(不同排序规则)是索引需重建
设置/取消 STORAGE否仅元数据
SET LOGGED / SET UNLOGGED是整表重写并切换 WAL 记录方式

一个重要例外:把 varchar(n) 改成更短的 varchar(m),PostgreSQL 只需扫描校验而不重写,但如果存在超长数据会直接失败。此时应先清洗数据。

SET/DROP NOT NULL 与 DEFAULT ​

SET NOT NULL 需要扫描全表确认没有 NULL。PostgreSQL 12 起支持跳过扫描——只要表上存在一个已 VALIDATE 的 CHECK (col IS NOT NULL) 约束,或者该列有非空约束可被证明,就能走快路径。

sql
-- 大表上的技巧:先加紧约束,再升格为真 NOT NULL
ALTER TABLE orders ADD CONSTRAINT ck_orders_remark_nn CHECK (remark IS NOT NULL) NOT VALID;
ALTER TABLE orders VALIDATE CONSTRAINT ck_orders_remark_nn;   -- 扫描校验(锁较轻)
ALTER TABLE orders ALTER COLUMN remark SET NOT NULL;          -- 秒完成(PG 12+ 走快路径)

SET DEFAULT 只是改 pg_attrdef 里的默认表达式,不会回填已有行:

sql
ALTER TABLE users ALTER COLUMN status SET DEFAULT 'pending';
ALTER TABLE users ALTER COLUMN status DROP DEFAULT;

-- 查看默认值定义
SELECT pg_get_expr(adbin, adrelid) AS default_expr
FROM pg_attrdef WHERE adrelid = 'users'::regclass;

增删约束与 VALIDATE CONSTRAINT ​

加约束时把校验拆开,是大表无停服加约束的标准做法:

sql
-- 第一步:加约束但不校验旧数据(立即返回,只对新写入生效)
ALTER TABLE orders
  ADD CONSTRAINT ck_orders_amount_positive CHECK (amount > 0) NOT VALID;

-- 第二步:校验已有数据(扫表,但持锁较轻,不阻塞读写)
ALTER TABLE orders VALIDATE CONSTRAINT ck_orders_amount_positive;
sql
-- 加外键同样适用
ALTER TABLE orders
  ADD CONSTRAINT fk_orders_user FOREIGN KEY (user_id) REFERENCES users (id) NOT VALID;
ALTER TABLE orders VALIDATE CONSTRAINT fk_orders_user;

-- 加唯一约束会直接建索引(可先用 CREATE INDEX CONCURRENTLY 再挂约束)
CREATE UNIQUE INDEX CONCURRENTLY uq_orders_no ON orders (order_no);
ALTER TABLE orders ADD CONSTRAINT uq_orders_no UNIQUE USING INDEX uq_orders_no;

删除约束:

sql
ALTER TABLE orders DROP CONSTRAINT IF EXISTS ck_orders_amount_positive;

下表汇总约束相关操作对锁与代价的影响:

操作是否扫表锁级别是否阻塞读写
ADD CONSTRAINT ... NOT VALID否ACCESS EXCLUSIVE(极短)瞬间
VALIDATE CONSTRAINT是SHARE UPDATE EXCLUSIVE否
ADD CONSTRAINT ...(默认)是ACCESS EXCLUSIVE全程阻塞
DROP CONSTRAINT否ACCESS EXCLUSIVE(极短)瞬间
ALTER CONSTRAINT ... DEFERRABLE否ACCESS EXCLUSIVE(极短)瞬间

RENAME 与 SET SCHEMA ​

重命名只改系统目录里的名字,永远不重写:

sql
-- 改列名
ALTER TABLE users RENAME COLUMN nickname TO display_name;

-- 改约束名
ALTER TABLE users RENAME CONSTRAINT uq_users_email TO uq_users_email_key;

-- 改表名(索引、约束、序列的名字不会自动跟着变)
ALTER TABLE users RENAME TO app_users;

-- 移到另一个模式
ALTER TABLE public.app_users SET SCHEMA archive;

RENAME 的坑在于依赖对象的旧名字会一起暴露:视图定义里存的是 regclass 引用,重命名表后视图仍能工作(引用是 OID),但视图的输出列名不会自动更新,\d+ 里看到的还是旧列名。

大表在线 DDL 策略 ​

把上面的知识组合起来,大表上的安全变更流程如下:

sql
-- 0. 设置锁等待上限,避免被慢查询堵死(会话级)
SET lock_timeout = '3s';
SET statement_timeout = '0';

-- 1. 只改元数据的操作:直接做,几毫秒
ALTER TABLE big_table ADD COLUMN IF NOT EXISTS new_col text;

-- 2. 加带默认值的列(PG 11+ 常量默认值也走元数据路径)
ALTER TABLE big_table ADD COLUMN IF NOT EXISTS status text NOT NULL DEFAULT 'active';

-- 3. 需要校验的约束:NOT VALID + VALIDATE 两步走
ALTER TABLE big_table ADD CONSTRAINT ck_new CHECK (new_col <> '') NOT VALID;
ALTER TABLE big_table VALIDATE CONSTRAINT ck_new;

-- 4. 不可避免的重写:用 pg_repack 或"影子表 + 双写 + 切换"方案,避开 ACCESS EXCLUSIVE 长锁

对于 ALTER COLUMN TYPE 这类必须重写的操作,生产方案通常是:

text
① 建新列(不同名),如 new_amount numeric(14,2)
② 应用双写:同时写入 old 列与 new 列
③ 后台分批回填历史数据(UPDATE ... WHERE new_amount IS NULL LIMIT 10000 循环)
④ 校验一致后,重命名列 → 一次 RENAME 瞬间完成
⑤ 删除旧列

实战示例 ​

场景:orders 表已上线半年、数据量 2000 万行,业务要求新增字段、加约束、改类型、改表名,且不能停服。我们按"从安全到危险"的顺序逐条落实。

第 1 步:准备演示环境

sql
DROP TABLE IF EXISTS orders CASCADE;
DROP TABLE IF EXISTS users  CASCADE;

CREATE TABLE users (
  id    bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  email text   NOT NULL UNIQUE
);

CREATE TABLE orders (
  id         bigint        GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  user_id    bigint        NOT NULL REFERENCES users (id),
  order_no   text          NOT NULL,
  amount     numeric(10,2) NOT NULL,
  created_at timestamptz   NOT NULL DEFAULT now()
);

INSERT INTO users (email) VALUES ('a@example.com'), ('b@example.com');
INSERT INTO orders (user_id, order_no, amount) VALUES
  (1, 'SO-001', 100.00),
  (1, 'SO-002', 250.00),
  (2, 'SO-003',  99.00);
text
CREATE TABLE
CREATE TABLE
INSERT 0 2
INSERT 0 3

第 2 步:加列——只改元数据,秒完成

sql
-- 加一个可空列
ALTER TABLE orders ADD COLUMN IF NOT EXISTS remark text;

-- 加一个带常量默认值的非空列(PG 11+ 不重写)
ALTER TABLE orders ADD COLUMN IF NOT EXISTS source text NOT NULL DEFAULT 'web';
text
ALTER TABLE
ALTER TABLE

第 3 步:加约束——用 NOT VALID 两步法

sql
-- 第一步:不扫表,立即返回
ALTER TABLE orders
  ADD CONSTRAINT ck_orders_amount_positive CHECK (amount > 0) NOT VALID;
text
ALTER TABLE
sql
-- 此时旧数据尚未校验,可以插一条违反约束的数据吗?不行——约束对新行立刻生效
INSERT INTO orders (user_id, order_no, amount) VALUES (1, 'SO-BAD', -1);
text
ERROR:  new row for relation "orders" violates check constraint "ck_orders_amount_positive"
DETAIL:  Failing row contains (4, 1, SO-BAD, -1.00, 2026-09-24 11:30:02, null, web).
sql
-- 第二步:校验历史数据(锁较轻)
ALTER TABLE orders VALIDATE CONSTRAINT ck_orders_amount_positive;

-- 确认约束已生效
SELECT conname, convalidated
FROM pg_constraint WHERE conrelid = 'orders'::regclass AND contype = 'c';
text
ALTER TABLE

             conname             | convalidated
---------------------------------+--------------
 ck_orders_amount_positive       | t
(1 row)

第 4 步:加长索引型唯一约束

sql
-- 唯一性靠唯一索引实现,先并发建索引避免锁表
CREATE UNIQUE INDEX CONCURRENTLY uq_orders_no ON orders (order_no);

-- 再把索引"挂"成约束
ALTER TABLE orders ADD CONSTRAINT uq_orders_no UNIQUE USING INDEX uq_orders_no;
text
CREATE INDEX
ALTER TABLE

第 5 步:改类型——必须重写,先看清代价

sql
-- 兼容类型(不重写):text → varchar 放宽
ALTER TABLE orders ALTER COLUMN order_no TYPE varchar(64);
text
ALTER TABLE
sql
-- 不兼容类型(重写):numeric(10,2) → numeric(14,2) 需要 USING 或自动转换
ALTER TABLE orders ALTER COLUMN amount TYPE numeric(14,2);
text
ALTER TABLE
sql
-- 确认数据完好无损
SELECT id, order_no, amount, source FROM orders ORDER BY id;
text
 id | order_no | amount | source
----+----------+--------+--------
  1 | SO-001   | 100.00 | web
  2 | SO-002   | 250.00 | web
  3 | SO-003   |  99.00 | web
(3 rows)

第 6 步:SET NOT NULL——先加紧约束再升格

sql
-- 把 remark 变成非空(大表上应先加 CHECK 再升格)
ALTER TABLE orders ADD CONSTRAINT ck_orders_remark_nn CHECK (remark IS NOT NULL) NOT VALID;
UPDATE orders SET remark = '' WHERE remark IS NULL;
ALTER TABLE orders VALIDATE CONSTRAINT ck_orders_remark_nn;
ALTER TABLE orders ALTER COLUMN remark SET NOT NULL;
text
ALTER TABLE
UPDATE 3
ALTER TABLE
ALTER TABLE

第 7 步:重命名与移动

sql
ALTER TABLE orders RENAME COLUMN remark TO buyer_note;
ALTER TABLE orders RENAME CONSTRAINT ck_orders_remark_nn TO ck_orders_buyer_note_nn;
ALTER TABLE orders RENAME TO shop_orders;
text
ALTER TABLE
ALTER TABLE
ALTER TABLE

第 8 步:复核变更结果

sql
-- psql 元命令:\d+ 查看列、约束、索引
\d+ shop_orders
text
                                          Table "public.shop_orders"
   Column    |           Type           | Collation | Nullable |           Default
-------------+--------------------------+-----------+----------+------------------------------
 id          | bigint                   |           | not null | generated always as identity
 user_id     | bigint                   |           | not null |
 order_no    | character varying(64)    |           | not null |
 amount      | numeric(14,2)            |           | not null |
 created_at  | timestamp with time zone |           | not null | now()
 buyer_note  | text                     |           | not null |
 source      | text                     |           | not null | 'web'::text
Indexes:
    "shop_orders_pkey" PRIMARY KEY, btree (id)
    "uq_orders_no" UNIQUE CONSTRAINT, btree (order_no)
Check constraints:
    "ck_orders_amount_positive" CHECK (amount > 0::numeric)
    "ck_orders_buyer_note_nn" CHECK (buyer_note IS NOT NULL)
Foreign-key constraints:
    "shop_orders_user_id_fkey" FOREIGN KEY (user_id) REFERENCES users(id)
sql
-- 用系统表复核约束是否全部"已校验"
SELECT conname, contype, convalidated
FROM pg_constraint WHERE conrelid = 'shop_orders'::regclass
ORDER BY contype, conname;
text
           conname            | contype | convalidated
------------------------------+---------+--------------
 ck_orders_amount_positive    | c       | t
 ck_orders_buyer_note_nn      | c       | t
 shop_orders_user_id_fkey     | f       | t
 shop_orders_pkey             | p       | t
 uq_orders_no                 | u       | t
(5 rows)

第 9 步:清理

sql
DROP TABLE shop_orders, users CASCADE;
text
DROP TABLE

注意事项 ​

  • ALTER TABLE 默认要 ACCESS EXCLUSIVE 锁,会阻塞表上所有读写。 大表上执行前务必先 SET lock_timeout。
sql
-- 正确:设好超时,拿不到锁就失败退出,而不是堵住整个表
SET lock_timeout = '3s';
ALTER TABLE big_table ADD COLUMN c1 text;
  • 锁队列插队会放大伤害。 变更本身只需几毫秒,但前面若有慢查询,后来的所有查询都会排在 ALTER TABLE 后面。
sql
-- 排查是否有长事务在阻塞
SELECT pid, state, wait_event_type, now() - xact_start AS xact_age, left(query, 60)
FROM pg_stat_activity
WHERE state <> 'idle' ORDER BY xact_start;
  • 加带"非常量默认值"的列会重写整表。 只有常量默认值才走元数据快路径。
sql
-- 危险!触发重写
ALTER TABLE t ADD COLUMN created_seq bigint DEFAULT nextval('s1');

-- 安全:常量
ALTER TABLE t ADD COLUMN status text NOT NULL DEFAULT 'active';
  • DROP COLUMN 不释放磁盘空间。 列只是被标记为 attisdropped,需要后续 VACUUM FULL 或 pg_repack 才能回收。
sql
-- 查看表中被逻辑删除的列数量
SELECT count(*) FROM pg_attribute
WHERE attrelid = 't'::regclass AND attisdropped;
  • 改类型可能重写,且会在磁盘上产生双倍占用。 大表操作前先确认剩余空间。
sql
-- 查看表与索引占用,估算重写所需空间
SELECT pg_size_pretty(pg_total_relation_size('big_table')) AS total_size;
  • SET NOT NULL 在 PostgreSQL 12 前必须扫全表。 大表上用 CHECK ... NOT VALID + VALIDATE 再升格。

  • SET DEFAULT 不会回填已有行。 想让历史数据也有值,必须另写 UPDATE。

sql
-- 错误认知:以为已有行会自动变成 'pending'
ALTER TABLE users ALTER COLUMN status SET DEFAULT 'pending';
SELECT count(*) FROM users WHERE status IS NULL;   -- 仍然是 NULL
  • ADD CONSTRAINT 默认会扫表校验。 大表必须拆成 NOT VALID + VALIDATE CONSTRAINT。

  • ALTER TABLE ... RENAME 不会重命名索引、序列、约束。 名字会与新表名不一致,长期维护时要手工收拾。

  • 视图的输出列名在基表重命名后不会自动更新。 引用虽然仍有效(基于 OID),但显示的名字会"过时"。

  • ALTER TABLE 也能在事务里回滚,但重写操作产生的 WAL 与 IO 无法撤销。

sql
BEGIN;
ALTER TABLE t ADD COLUMN x text;
ROLLBACK;   -- 列不会真的加上去
  • ALTER TABLE ONLY 与 ALTER TABLE 在继承/分区场景下行为不同。 不加 ONLY 会递归作用到子表。
sql
-- 只改父表本身
ALTER TABLE ONLY parent_table ADD COLUMN c text;

-- 连同所有子表一起改
ALTER TABLE parent_table ADD COLUMN c text;
  • ATTACH PARTITION / DETACH PARTITION 也有锁与校验代价,分区表的 DETACH 默认仍会持有 ACCESS EXCLUSIVE。

最佳实践 ​

  • 任何 ALTER TABLE 前先 SET lock_timeout,宁可失败重试,也不要让表"停服"。
sql
SET lock_timeout = '3s';
ALTER TABLE orders ADD COLUMN IF NOT EXISTS remark text;
RESET lock_timeout;
  • 能用 IF NOT EXISTS / IF EXISTS 就用,让迁移脚本天然可重复执行(幂等)。
sql
ALTER TABLE orders ADD COLUMN IF NOT EXISTS remark text;
ALTER TABLE orders DROP COLUMN IF EXISTS legacy_col;
ALTER TABLE orders DROP CONSTRAINT IF EXISTS ck_old;
  • 大表加约束统一走两步法,并用迁移工具管理校验步骤。
sql
ALTER TABLE orders ADD CONSTRAINT ck_x CHECK (amount > 0) NOT VALID;
ALTER TABLE orders VALIDATE CONSTRAINT ck_x;
  • 大表加唯一约束先 CREATE UNIQUE INDEX CONCURRENTLY,再 ADD CONSTRAINT ... USING INDEX,避免长时间独占锁。
sql
CREATE UNIQUE INDEX CONCURRENTLY uq_orders_no ON orders (order_no);
ALTER TABLE orders ADD CONSTRAINT uq_orders_no UNIQUE USING INDEX uq_orders_no;
  • 改类型优先"加新列 + 双写 + 回填 + 重命名",而不是直接 ALTER COLUMN TYPE。

  • 列默认值用常量,需要动态值就让应用层显式写入,不要在 DDL 里塞 now()/nextval()。

  • 删除列前先 DROP DEFAULT、DROP NOT NULL,确认无依赖后再删,减少 CASCADE 的连锁风险。

  • 重命名列时同步更新视图与文档,并优先保留兼容别名,避免应用发布与 DDL 的时序耦合。

  • 把 DDL 变更纳入版本管理(如 schema_migrations 表),并保证每个迁移可重入。

sql
-- 记录已应用的迁移
CREATE TABLE IF NOT EXISTS schema_migrations (
  version    text        PRIMARY KEY,
  applied_at timestamptz NOT NULL DEFAULT now()
);
  • 迁移脚本按"元数据 → 校验 → 重写"从轻到重排序执行,让最危险的操作单独占一个窗口期。

  • 变更后用 \d+ 与 pg_constraint 复核,确认约束名、校验状态、类型都符合预期。

sql
SELECT conname, contype, convalidated, pg_get_constraintdef(oid)
FROM pg_constraint WHERE conrelid = 'orders'::regclass;
  • ALTER TABLE 与 VACUUM 冲突:重写类操作会等待正在运行的 VACUUM,安排维护窗口时两者要错开。

参考链接 ​