数据库迁移的挑战
每次数据库Schema变更都潜藏着风险。与代码部署不同,数据库变更往往难以快速回滚——已删除的列无法"撤销部署",已迁移的数据无法简单地"还原"。在有用户实时使用的生产系统上执行变更,既要保证数据一致性,又不能影响服务可用性,这是每个工程团队都面临的挑战。
理解两种基本迁移类型至关重要:
- 向后兼容变更:如添加可为NULL的新列,新旧版本代码都能正常工作
- 破坏性变更:如重命名列、修改数据类型,需要特殊处理
展开-收缩模式(Expand-Contract Pattern)
展开-收缩是处理破坏性变更最安全的模式,将一次高风险变更拆分为多个低风险步骤:
场景:将 users.full_name 拆分为 first_name 和 last_name
第一阶段:展开(添加新列,保留旧列)
-- 迁移脚本1:添加新列,保持兼容性
ALTER TABLE users ADD COLUMN first_name VARCHAR(100);
ALTER TABLE users ADD COLUMN last_name VARCHAR(100);
-- 回填现有数据
UPDATE users
SET first_name = split_part(full_name, ' ', 1),
last_name = NULLIF(
trim(substring(full_name FROM position(' ' IN full_name))),
''
)
WHERE full_name IS NOT NULL;
-- 创建触发器保持同步(过渡期间新旧列同时维护)
CREATE OR REPLACE FUNCTION sync_name_columns()
RETURNS TRIGGER AS $$
BEGIN
IF NEW.full_name IS DISTINCT FROM OLD.full_name THEN
NEW.first_name := split_part(NEW.full_name, ' ', 1);
NEW.last_name := NULLIF(trim(substring(NEW.full_name FROM position(' ' IN NEW.full_name))), '');
ELSIF NEW.first_name IS DISTINCT FROM OLD.first_name
OR NEW.last_name IS DISTINCT FROM OLD.last_name THEN
NEW.full_name := trim(NEW.first_name || ' ' || COALESCE(NEW.last_name, ''));
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER users_name_sync
BEFORE UPDATE ON users
FOR EACH ROW EXECUTE FUNCTION sync_name_columns();
第二阶段:部署使用新列的应用代码,观察稳定运行后进入收缩阶段。
第三阶段:收缩(移除旧列)
-- 确认新代码完全稳定后,移除同步触发器和旧列
DROP TRIGGER IF EXISTS users_name_sync ON users;
DROP FUNCTION IF EXISTS sync_name_columns();
ALTER TABLE users DROP COLUMN full_name;
大表添加列的在线方式
对于亿级记录的大表,ALTER TABLE ADD COLUMN 配合默认值会锁表并重写全表,可能导致长时间服务中断。
-- 低效:锁表重写(百万行可能需要几分钟)
ALTER TABLE orders ADD COLUMN priority INT NOT NULL DEFAULT 0;
-- 高效方式1:先添加可NULL列,再异步回填,最后添加约束
ALTER TABLE orders ADD COLUMN priority INT; -- 瞬间完成,无锁
-- 异步分批回填(避免一次性锁定大量行)
DO $$
DECLARE
batch_size INT := 10000;
offset_val BIGINT := 0;
updated INT;
BEGIN
LOOP
UPDATE orders SET priority = 0
WHERE id IN (
SELECT id FROM orders WHERE priority IS NULL
ORDER BY id LIMIT batch_size
);
GET DIAGNOSTICS updated = ROW_COUNT;
EXIT WHEN updated = 0;
PERFORM pg_sleep(0.1); -- 控制写入速率
END LOOP;
END $$;
-- 回填完成后,在线添加约束(PostgreSQL 12+支持NOT VALID跳过历史数据验证)
ALTER TABLE orders
ALTER COLUMN priority SET DEFAULT 0,
ALTER COLUMN priority SET NOT NULL;
蓝绿数据库部署
对于需要重大重构的迁移,蓝绿部署提供了最安全的切换路径:
-- 阶段1:在绿库(新结构)建好Schema
-- 阶段2:使用逻辑复制将蓝库数据同步到绿库
-- PostgreSQL逻辑复制配置示意
-- 在蓝库(源)执行:
CREATE PUBLICATION blue_to_green FOR ALL TABLES;
-- 在绿库(目标)执行:
CREATE SUBSCRIPTION green_subscription
CONNECTION 'host=blue-db port=5432 dbname=myapp user=replication_user'
PUBLICATION blue_to_green;
-- 等待绿库与蓝库同步(lag接近0)
SELECT * FROM pg_stat_subscription;
-- 阶段3:应用层切换连接到绿库,观察无误后关闭蓝库
迁移版本管理
无论使用Flyway、Liquibase还是自定义迁移框架,都应遵循以下原则:
-- 创建迁移历史跟踪表
CREATE TABLE schema_migrations (
version VARCHAR(20) PRIMARY KEY,
description VARCHAR(200) NOT NULL,
applied_at TIMESTAMP NOT NULL DEFAULT NOW(),
applied_by VARCHAR(100) NOT NULL DEFAULT current_user,
checksum VARCHAR(64) -- 防止已执行的迁移文件被修改
);
-- 每次迁移前记录
INSERT INTO schema_migrations (version, description, checksum)
VALUES ('20260403001', 'split_users_fullname', 'sha256_of_migration_file');
迁移文件命名规范:V20260403001__split_users_fullname.sql,时间戳前缀确保执行顺序,每个迁移文件只做一件事,且必须幂等(可重复执行不报错)。
使用Erdmates规划迁移路径
Erdmates的Schema可视化帮助您在迁移前直观对比新旧数据库结构的差异,识别受影响的表和关联关系,制定周密的展开-收缩计划。将迁移前后的ERD分别保存,便于团队评审和回顾。
安全的数据库迁移需要耐心和纪律:宁可多分几个步骤,也不要冒险一次性完成所有变更。每一步都应该可验证、可回滚,这才是成熟工程团队的数据库变更文化。