跳到主要内容

修改表

ALTER TABLE 用于修改已有表的结构。添加列可能很简单,但在生产环境中,任何结构变更都要同时考虑历史数据、依赖对象、锁、表重写、部署顺序和回滚方式。

本文先介绍常见 SQL 操作,再使用 PostgreSQL 语法说明迁移策略。不同数据库的 ALTER TABLE 能力差异很大,执行前必须核对目标数据库和版本。

基本形式​

ALTER TABLE table_name
action;

PostgreSQL 允许在一条语句中组合多个动作:

ALTER TABLE users
ADD COLUMN nickname TEXT,
ALTER COLUMN email SET NOT NULL;

组合动作可以减少重复获取锁,但也会扩大单次失败的影响。是否合并应根据锁时长、回滚和发布计划决定。

添加列​

ALTER TABLE users
ADD COLUMN nickname TEXT;

添加可空列通常风险较低。给已有大表添加必填列则要考虑历史行:

ALTER TABLE users
ADD COLUMN account_status TEXT;

一种稳妥的分步方式是:

  1. 添加可空列;
  2. 发布能同时兼容新旧结构的应用;
  3. 分批回填历史数据;
  4. 设置默认值供新写入使用;
  5. 验证不存在空值;
  6. 最后设置 NOT NULL。
ALTER TABLE users
ALTER COLUMN account_status SET DEFAULT 'active';

UPDATE users
SET account_status = 'active'
WHERE account_status IS NULL;

ALTER TABLE users
ALTER COLUMN account_status SET NOT NULL;

现代 PostgreSQL 对某些常量默认值可避免立即重写整表,但易变表达式、具体版本和后续操作仍会影响成本。不能仅凭语法短就断定操作瞬间完成。

删除列​

ALTER TABLE users
DROP COLUMN nickname;

删除列会丢失数据,并可能影响视图、索引、约束、函数和应用代码。默认使用 RESTRICT 思维,先检查依赖;不要为了让命令成功就直接加 CASCADE。

零停机发布常采用“先停止使用,再删除结构”:

  1. 应用停止写入旧列;
  2. 应用停止读取旧列;
  3. 观察一个完整发布周期;
  4. 删除依赖对象和旧列。

对大型系统,删除列后磁盘空间未必立即归还操作系统。物理回收可能需要表重写,应另行安排维护窗口。

修改默认值​

ALTER TABLE users
ALTER COLUMN account_status SET DEFAULT 'pending';

删除默认值:

ALTER TABLE users
ALTER COLUMN account_status DROP DEFAULT;

默认值变更只影响以后使用默认规则的写入,不会更新历史行。数据回填应作为独立步骤明确执行。

修改空值限制​

ALTER TABLE users
ALTER COLUMN email SET NOT NULL;

若存在空值,操作会失败。大表上还可能扫描数据。可以先添加并验证等价检查约束,再让数据库利用已验证事实完成后续变更;具体优化是否适用取决于 PostgreSQL 版本,应在目标版本验证执行计划和锁行为。

取消限制:

ALTER TABLE users
ALTER COLUMN email DROP NOT NULL;

放宽数据库约束之前,要确认应用和下游系统能处理新增的空值情况。

修改数据类型​

直接转换:

ALTER TABLE products
ALTER COLUMN stock TYPE BIGINT;

需要明确转换表达式时使用 PostgreSQL 的 USING:

ALTER TABLE events
ALTER COLUMN occurred_at TYPE TIMESTAMPTZ
USING occurred_at AT TIME ZONE 'Asia/Shanghai';

时区转换必须先弄清旧值的真实语义。把一个没有时区的时间戳转换成带时区类型,不是简单“加上时区”这么机械。

类型变更可能:

  • 因历史值无法转换而失败;
  • 改变精度、排序或比较语义;
  • 重写整表并长时间持锁;
  • 重建索引;
  • 影响视图、函数、驱动和 ORM 映射。

高风险变更可采用“新增列、双写、分批回填、切换读取、删除旧列”的扩展与收缩模式。

添加、验证和删除约束​

ALTER TABLE products
ADD CONSTRAINT ck_products_price
CHECK (price >= 0);

PostgreSQL 可先为新写入启用规则,再验证历史数据:

ALTER TABLE products
ADD CONSTRAINT ck_products_price
CHECK (price >= 0) NOT VALID;

ALTER TABLE products
VALIDATE CONSTRAINT ck_products_price;

删除约束:

ALTER TABLE products
DROP CONSTRAINT ck_products_price;

修改约束通常需要创建新约束并删除旧约束。迁移过程中要避免出现无人保护的数据窗口。

使用现有索引建立唯一约束​

大表创建唯一索引可能耗时较长。PostgreSQL 可先并发创建符合要求的唯一索引,再把它附加为约束:

CREATE UNIQUE INDEX CONCURRENTLY uq_users_email_idx
ON users (email);
ALTER TABLE users
ADD CONSTRAINT uq_users_email
UNIQUE USING INDEX uq_users_email_idx;

CREATE INDEX CONCURRENTLY 不能放在普通事务块中,失败后还可能留下无效索引,需要检测和清理。它减少阻塞但不代表没有资源消耗或短暂锁。

重命名表、列和约束​

重命名列:

ALTER TABLE users
RENAME COLUMN username TO login_name;

重命名表:

ALTER TABLE users
RENAME TO app_users;

重命名约束:

ALTER TABLE app_users
RENAME CONSTRAINT uq_users_email TO uq_app_users_email;

数据库内部按对象标识跟踪的依赖通常会继续有效,但应用 SQL、动态字符串、报表、监控和外部工具可能仍引用旧名称。重命名不是天然的零停机操作,旧版与新版应用并存时尤其危险。

移动模式和修改所有者​

移动到另一个模式:

ALTER TABLE app_users
SET SCHEMA account;

修改所有者:

ALTER TABLE account.app_users
OWNER TO app_owner;

这会影响名称解析、权限管理和运维责任。应用应尽量使用稳定的限定名称或受控 search_path,并在迁移时同步检查授权。

锁与表重写​

许多 ALTER TABLE 子命令会申请较强锁,即使元数据变更本身只需很短时间,也可能因为前方有长事务而长时间等待,并阻塞后续查询。

上线前应检查:

  • 操作需要什么锁模式;
  • 等锁时是否会阻塞新的请求;
  • 是否扫描或重写整表;
  • 是否重建索引和产生大量 WAL;
  • 复制延迟和磁盘空间是否足够;
  • 超时后会留下什么中间状态。

可以在受控会话设置较短的锁等待上限,获取不到锁就退出重试:

SET lock_timeout = '5s';

超时值应结合迁移系统设计,不能机械套用。

事务与回滚​

PostgreSQL 的许多 DDL 可以放在事务中:

BEGIN;

ALTER TABLE products
ADD COLUMN archived_at TIMESTAMPTZ;

-- 检查结构和依赖

COMMIT;

但长事务会延长锁持有时间,CREATE INDEX CONCURRENTLY 等操作也不能放在普通事务块中。可事务回滚不等于迁移没有风险:已经阻塞的请求、生成的日志和外部应用行为无法简单回滚。

扩展与收缩迁移​

需要兼容滚动发布时,可采用两阶段方法。

扩展阶段:

  1. 添加新列或新结构,保持旧接口可用;
  2. 发布兼容新旧结构的代码;
  3. 双写或建立同步机制;
  4. 分批回填并校验。

收缩阶段:

  1. 切换所有读取到新结构;
  2. 停止旧结构写入;
  3. 观察并确认无旧版本实例;
  4. 删除旧列、旧约束和兼容代码。

这种方式步骤较多,但能避免数据库与应用必须在同一瞬间切换。

修改表检查清单​

  1. 历史数据能否满足新类型和新约束;
  2. 应用新旧版本是否会同时运行;
  3. 是否存在视图、函数、触发器、报表和动态 SQL 依赖;
  4. 操作需要什么锁,预计等待和执行多久;
  5. 是否重写表、重建索引或产生大量日志;
  6. 回填能否分批并支持重试;
  7. 是否准备了验证查询和停止条件;
  8. 回滚是撤销结构,还是让应用切回兼容路径;
  9. 权限、所有者、复制和备份是否受影响;
  10. 是否已在相近数据量和相同版本环境演练。

ALTER TABLE 的难点通常不在语法,而在变更过程。把结构变更当作一次需要兼容、观测和回退的发布,才能让简单 SQL 在真实系统中安全落地。