修改表结构(ALTER TABLE)
表一旦建好,业务就会变,结构也必须跟着变。ALTER TABLE 是 PostgreSQL 中风险最高的一条 DDL:它默认要拿 ACCESS EXCLUSIVE 锁,会阻塞表上的所有读写。本节讲清楚三件事——每种变更怎么写、哪些变更要重写整表、大表上怎么安全地做变更。
前置知识
阅读本节前,建议先了解:创建表(CREATE TABLE)
基础概念
ALTER TABLE 的变更可以按「代价」分成三个层级,这是本节最重要的一张分类表:
| 代价层级 | 含义 | 典型操作 | 锁级别 |
|---|---|---|---|
| 仅改元数据(Metadata Only) | 只更新系统目录,瞬间完成,与表大小无关 | ADD COLUMN(无默认值或有常量默认值)、DROP COLUMN、RENAME、SET DEFAULT、SET STATISTICS | ACCESS 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/UNLOGGED | ACCESS 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 也是事务性的),这意味着变更失败会自动回滚,不会留下"半改"状态。
语法与用法
完整语法骨架(按用途分块,省略分区、继承、存储参数细节):
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 }最简可运行示例:
-- 加一列(无默认值,只改元数据,瞬间完成)
ALTER TABLE users ADD COLUMN phone text;ALTER TABLE详细说明
增删列:ADD COLUMN 与 DROP COLUMN
ADD COLUMN 的代价取决于默认值:
-- 加列不指定默认值:只写系统目录,与表大小无关
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+ 上是秒完成的:
-- PostgreSQL 11+:即使 NOT NULL + DEFAULT,也不重写
ALTER TABLE big_table ADD COLUMN status text NOT NULL DEFAULT 'active';DROP COLUMN 同样是仅元数据操作——列被标记为 attisdropped = true,物理空间不立即回收,要等后续 VACUUM FULL 或表重建才真正释放。
-- 删列:默认 RESTRICT
ALTER TABLE orders DROP COLUMN remark;
-- 删除被视图、外键等依赖的列,必须显式 CASCADE(会连带删掉依赖对象)
ALTER TABLE orders DROP COLUMN source CASCADE;-- 验证列确实只是"逻辑删除"
SELECT attname, attisdropped, attnum
FROM pg_attribute
WHERE attrelid = 'orders'::regclass AND attnum > 0
ORDER BY attnum; attname | attisdropped | attnum
-----------------+--------------+--------
id | f | 1
user_id | f | 2
...
remark | t | 12
source | t | 13
(rows)被删掉的列仍占 attnum,这也是 PostgreSQL 系统列编号只增不减的原因。
改列类型:ALTER COLUMN TYPE 与 USING
改类型是三档里最危险的,是否重写取决于新旧类型是否"二进制兼容"。
-- 兼容类型:仅元数据变更(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 且系统找不到隐式转换时,会直接报错:
-- 错误!
ALTER TABLE users ALTER COLUMN age TYPE integer;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) 约束,或者该列有非空约束可被证明,就能走快路径。
-- 大表上的技巧:先加紧约束,再升格为真 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 里的默认表达式,不会回填已有行:
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
加约束时把校验拆开,是大表无停服加约束的标准做法:
-- 第一步:加约束但不校验旧数据(立即返回,只对新写入生效)
ALTER TABLE orders
ADD CONSTRAINT ck_orders_amount_positive CHECK (amount > 0) NOT VALID;
-- 第二步:校验已有数据(扫表,但持锁较轻,不阻塞读写)
ALTER TABLE orders VALIDATE CONSTRAINT ck_orders_amount_positive;-- 加外键同样适用
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;删除约束:
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
重命名只改系统目录里的名字,永远不重写:
-- 改列名
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 策略
把上面的知识组合起来,大表上的安全变更流程如下:
-- 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 这类必须重写的操作,生产方案通常是:
① 建新列(不同名),如 new_amount numeric(14,2)
② 应用双写:同时写入 old 列与 new 列
③ 后台分批回填历史数据(UPDATE ... WHERE new_amount IS NULL LIMIT 10000 循环)
④ 校验一致后,重命名列 → 一次 RENAME 瞬间完成
⑤ 删除旧列实战示例
场景:orders 表已上线半年、数据量 2000 万行,业务要求新增字段、加约束、改类型、改表名,且不能停服。我们按"从安全到危险"的顺序逐条落实。
第 1 步:准备演示环境
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);CREATE TABLE
CREATE TABLE
INSERT 0 2
INSERT 0 3第 2 步:加列——只改元数据,秒完成
-- 加一个可空列
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';ALTER TABLE
ALTER TABLE第 3 步:加约束——用 NOT VALID 两步法
-- 第一步:不扫表,立即返回
ALTER TABLE orders
ADD CONSTRAINT ck_orders_amount_positive CHECK (amount > 0) NOT VALID;ALTER TABLE-- 此时旧数据尚未校验,可以插一条违反约束的数据吗?不行——约束对新行立刻生效
INSERT INTO orders (user_id, order_no, amount) VALUES (1, 'SO-BAD', -1);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).-- 第二步:校验历史数据(锁较轻)
ALTER TABLE orders VALIDATE CONSTRAINT ck_orders_amount_positive;
-- 确认约束已生效
SELECT conname, convalidated
FROM pg_constraint WHERE conrelid = 'orders'::regclass AND contype = 'c';ALTER TABLE
conname | convalidated
---------------------------------+--------------
ck_orders_amount_positive | t
(1 row)第 4 步:加长索引型唯一约束
-- 唯一性靠唯一索引实现,先并发建索引避免锁表
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;CREATE INDEX
ALTER TABLE第 5 步:改类型——必须重写,先看清代价
-- 兼容类型(不重写):text → varchar 放宽
ALTER TABLE orders ALTER COLUMN order_no TYPE varchar(64);ALTER TABLE-- 不兼容类型(重写):numeric(10,2) → numeric(14,2) 需要 USING 或自动转换
ALTER TABLE orders ALTER COLUMN amount TYPE numeric(14,2);ALTER TABLE-- 确认数据完好无损
SELECT id, order_no, amount, source FROM orders ORDER BY id; 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——先加紧约束再升格
-- 把 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;ALTER TABLE
UPDATE 3
ALTER TABLE
ALTER TABLE第 7 步:重命名与移动
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;ALTER TABLE
ALTER TABLE
ALTER TABLE第 8 步:复核变更结果
-- psql 元命令:\d+ 查看列、约束、索引
\d+ shop_orders 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)-- 用系统表复核约束是否全部"已校验"
SELECT conname, contype, convalidated
FROM pg_constraint WHERE conrelid = 'shop_orders'::regclass
ORDER BY contype, conname; 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 步:清理
DROP TABLE shop_orders, users CASCADE;DROP TABLE注意事项
ALTER TABLE默认要ACCESS EXCLUSIVE锁,会阻塞表上所有读写。 大表上执行前务必先SET lock_timeout。
-- 正确:设好超时,拿不到锁就失败退出,而不是堵住整个表
SET lock_timeout = '3s';
ALTER TABLE big_table ADD COLUMN c1 text;- 锁队列插队会放大伤害。 变更本身只需几毫秒,但前面若有慢查询,后来的所有查询都会排在
ALTER TABLE后面。
-- 排查是否有长事务在阻塞
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;- 加带"非常量默认值"的列会重写整表。 只有常量默认值才走元数据快路径。
-- 危险!触发重写
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才能回收。
-- 查看表中被逻辑删除的列数量
SELECT count(*) FROM pg_attribute
WHERE attrelid = 't'::regclass AND attisdropped;- 改类型可能重写,且会在磁盘上产生双倍占用。 大表操作前先确认剩余空间。
-- 查看表与索引占用,估算重写所需空间
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。
-- 错误认知:以为已有行会自动变成 'pending'
ALTER TABLE users ALTER COLUMN status SET DEFAULT 'pending';
SELECT count(*) FROM users WHERE status IS NULL; -- 仍然是 NULLADD CONSTRAINT默认会扫表校验。 大表必须拆成NOT VALID+VALIDATE CONSTRAINT。ALTER TABLE ... RENAME不会重命名索引、序列、约束。 名字会与新表名不一致,长期维护时要手工收拾。视图的输出列名在基表重命名后不会自动更新。 引用虽然仍有效(基于 OID),但显示的名字会"过时"。
ALTER TABLE也能在事务里回滚,但重写操作产生的 WAL 与 IO 无法撤销。
BEGIN;
ALTER TABLE t ADD COLUMN x text;
ROLLBACK; -- 列不会真的加上去ALTER TABLE ONLY与ALTER TABLE在继承/分区场景下行为不同。 不加ONLY会递归作用到子表。
-- 只改父表本身
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,宁可失败重试,也不要让表"停服"。
SET lock_timeout = '3s';
ALTER TABLE orders ADD COLUMN IF NOT EXISTS remark text;
RESET lock_timeout;- 能用
IF NOT EXISTS/IF EXISTS就用,让迁移脚本天然可重复执行(幂等)。
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;- 大表加约束统一走两步法,并用迁移工具管理校验步骤。
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,避免长时间独占锁。
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表),并保证每个迁移可重入。
-- 记录已应用的迁移
CREATE TABLE IF NOT EXISTS schema_migrations (
version text PRIMARY KEY,
applied_at timestamptz NOT NULL DEFAULT now()
);迁移脚本按"元数据 → 校验 → 重写"从轻到重排序执行,让最危险的操作单独占一个窗口期。
变更后用
\d+与pg_constraint复核,确认约束名、校验状态、类型都符合预期。
SELECT conname, contype, convalidated, pg_get_constraintdef(oid)
FROM pg_constraint WHERE conrelid = 'orders'::regclass;ALTER TABLE与VACUUM冲突:重写类操作会等待正在运行的VACUUM,安排维护窗口时两者要错开。
下一节