AI 辅助数据库迁移:把旧表安全搬到新结构
数据库迁移,简单说就是把数据从一套表结构或一种数据库引擎,搬到另一套结构或另一种引擎。它可能是同引擎内的 schema 演进,比如把一张大表拆成主表与明细表;也可能是跨引擎搬迁,比如把业务从 MySQL 迁到 PostgreSQL。无论哪种,迁移都带着天然的「破坏性」:一旦写错,丢的就是真金白银的数据。大语言模型(LLM)擅长理解结构、生成转换代码并解释差异,正好能在这类任务里当「副驾驶」。但模型不能替你承担数据丢失的风险——它生成的东西,最终要在影子库里跑过、校验过、再灰度上线。
本文讲清四件事:迁移有哪些典型风险、你该给 AI 喂哪些上下文、如何用分步策略把旧表安全搬到新结构、以及实战里怎么用 AI 生成迁移 SQL 与校验脚本。所有示例都尽量可运行,关键事实会标注核验状态。
数据库迁移的典型风险
动手之前,先认清迁移会踩的坑。把风险列在前面,后面的护栏才有针对性。
- 数据丢失:迁移脚本里少搬一张表、漏掉一个字段、或者
WHERE条件写错,都会让部分数据静默消失。最危险的是「看起来跑成功了」,因为报错没抛出来,只是行数对不上。 - 类型不兼容:源库和目标库对同一概念的类型定义不同。比如 MySQL 没有原生布尔型,常用
TINYINT(1)顶替;PostgreSQL 则有真正的BOOLEAN。直接搬过去,应用层读取语义可能错位。 - 约束与外键:外键、唯一约束、非空约束在目标库里若没建好或顺序不对,数据搬运时会因为违反约束整批失败;反过来,若为了迁过去而把约束全卸掉,又会在目标库埋下脏数据。
- 停机与一致性:在线业务要求「边跑边迁」。如果直接锁表导数据,迁移期间服务不可用;如果分两步走又不做增量同步,切换瞬间会丢迁移过程中产生的新数据。
- 语义差异:同样的 SQL 在不同引擎结果不同。比如字符串拼接,PostgreSQL 用
||,MySQL 用CONCAT;分组拼接 MySQL 用GROUP_CONCAT,PostgreSQL 用STRING_AGG。应用里若有这类语句,光迁数据不够,还得迁逻辑。 - 字符集与排序规则:源库用
utf8mb4、目标库若用UTF8(PostgreSQL 里实为UTF8)或排序规则不同,可能导致中文检索大小写敏感、排序结果不一致,甚至导入时出现编码报错。迁移前要核对两边字符集与 collation 是否等价。 - 默认值与触发器:源库的
ON UPDATE CURRENT_TIMESTAMP、触发器、存储过程在目标引擎可能没有一一对应物,需要重写为目标引擎语法,或下放到应用层实现。这类对象常常被自动迁移工具忽略,要单独盘点。
给 AI 的上下文:让它「看得见」旧库
模型不会读你的数据库。它生成什么,完全取决于你喂的上下文。上下文越完整,生成的迁移越靠谱。最少要准备四类材料。
DDL 与约束
把源库的建表语句、索引、外键、视图定义完整导出。例如 MySQL 可用 SHOW CREATE TABLE 表名 或 mysqldump --no-data 拿到纯结构。PostgreSQL 可用 pg_dump --schema-only。把这些 DDL 原样贴给模型,它才能知道字段类型、长度、默认值与约束。
-- 源库(MySQL)中的一张用户表
CREATE TABLE `user` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`name` VARCHAR(64) NOT NULL,
`is_active` TINYINT(1) NOT NULL DEFAULT 1,
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_name` (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
样本数据与目标引擎
只给结构不够,最好再给几行脱敏样本数据,让模型理解真实取值。比如 is_active 字段到底是存 0 和 1,还是有 NULL;created_at 是否出现过 0000-00-00 00:00:00 这种 MySQL 特有的零值。同时明确告诉模型目标引擎与版本,因为类型映射会随版本变化。
下面是交给模型的提示词模板,放在 ```text 块里,方便你直接复制填充。注意这里用占位符,不要把它嵌进 Python f-string 内层。
你是数据库迁移助手。请把下面这份 MySQL DDL 转换为 PostgreSQL 16 兼容的 DDL。
要求:
1. AUTO_INCREMENT 改为 GENERATED ALWAYS AS IDENTITY;
2. TINYINT(1) 改为 BOOLEAN,并说明默认值映射;
3. 去掉 ENGINE 与 CHARSET 等 MySQL 专属子句;
4. 标识符改用双引号,保留原表名与字段名语义;
5. 输出完整且可执行的 PostgreSQL DDL。
源 DDL:
{source_ddl}
业务约束与不可丢失规则
除了数据库约束,还有业务层「红线」。比如「订单表与支付表必须行数一致」「金额字段不能出现负数」这类规则,模型不知道,得由你写进上下文,后续用作校验依据。
分步迁移策略
不要试图一条脚本把旧库变新库。把迁移拆成四步,每一步都可独立验证、可回退。
第一步:Schema 映射
先只迁结构,不迁数据。把源 DDL 映射为目标 DDL,重点处理类型转换、标识符引号、自增机制、默认值函数。映射完成后在目标库执行,确认建表成功、约束齐全。这一步成本低、风险小,适合反复让 AI 调整直到通过。
第二步:数据搬运
结构就绪后搬数据。小表可用 INSERT ... SELECT 或导出 CSV 再 COPY 导入;大表建议分批、按主键区间切片,避免长事务锁表。切片的思路是把一张千万级大表按 id 区间分成多段,每段单独开事务,任一段失败只需重跑该段,不会拖垮整体。跨引擎优先用成熟工具,比如 pgloader 能从 MySQL 直接流式搬到 PostgreSQL,并自动做类型转换。
当源库是在线业务、不能停写时,一次性搬迁不够,需要「全量加增量」两段式:先迁历史全量数据,再持续同步迁移期间产生的新变更(通过 binlog、WAL 或应用双写),直到目标库与源库延迟足够小,才做最终切换。这套方案的细节超出本文范围,但务必记住:只迁一次全量、不做增量衔接,切换瞬间就会丢失迁移过程中写入的数据。
# 用 pgloader 把 MySQL 整库迁到 PostgreSQL(官方文档已核验 pgloader 支持此模式)
pgloader mysql://user:pwd@127.0.0.1/source_db \
postgresql://user:pwd@127.0.0.1/target_db
第三步:数据校验
搬完不等于正确。至少做三层校验:行数一致、关键字段非空率一致、以及业务校验(如金额合计相等)。校验脚本建议独立编写,不依赖迁移工具自身报告,互相印证更可信。
第四步:回滚预案
任何迁移都要假设「可能失败」。回滚预案包括:迁移前全量备份、保留旧库只读观察一段时间、准备一份反向脚本把新结构数据导回旧结构。只有回滚路径演练过,上线才敢切。
实战示例:用 AI 生成迁移 SQL 与校验脚本
下面用一段可运行 Python 演示两件事:一是把上下文拼成提示词交给模型生成目标 DDL;二是写一个独立的校验脚本,核对行数与金额合计。模型名称与 API 地址属于部署相关参数,标「待核实」。
这里特意把「生成」和「校验」拆成两段独立代码,是有意为之:生成由 AI 完成、带有不确定性;校验由我们手写、结果可信任。两者职责分离,才谈得上用校验去约束生成,而不是让模型自己证明自己正确。
让 AI 生成目标 DDL 的调用代码
import os
from openai import OpenAI
# 模型名称与 base_url 均为部署相关参数,待核实
client = OpenAI(
api_key=os.environ.get("OPENAI_API_KEY"),
base_url=os.environ.get("OPENAI_BASE_URL", "https://api.openai.com/v1"),
)
source_ddl = """
CREATE TABLE `user` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`name` VARCHAR(64) NOT NULL,
`is_active` TINYINT(1) NOT NULL DEFAULT 1,
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_name` (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
"""
prompt = (
"你是数据库迁移助手,请把以下 MySQL DDL 转换为 PostgreSQL 16 兼容 DDL。"
"要求:AUTO_INCREMENT 改 GENERATED ALWAYS AS IDENTITY;"
"TINYINT(1) 改 BOOLEAN;去掉 ENGINE/CHARSET;标识符用双引号。"
"只输出可执行 DDL。\n\n源 DDL:\n" + source_ddl
)
resp = client.chat.completions.create(
model=os.environ.get("MIGRATION_MODEL", "gpt-4o"),
messages=[{"role": "user", "content": prompt}],
temperature=0,
)
print(resp.choices[0].message.content)
独立的校验脚本
from sqlalchemy import create_engine, text, func
# 连接串为部署相关参数,待核实
src = create_engine("postgresql://user:pwd@127.0.0.1/source_db")
dst = create_engine("postgresql://user:pwd@127.0.0.1/target_db")
def row_count(engine, table):
with engine.connect() as conn:
return conn.execute(text(f"SELECT COUNT(*) FROM {table}")).scalar()
def sum_amount(engine, table, column):
with engine.connect() as conn:
return float(conn.execute(text(f"SELECT COALESCE(SUM({column}), 0) FROM {table}")).scalar())
tables = ["user", "order", "payment"]
for t in tables:
s, d = row_count(src, t), row_count(dst, t)
status = "OK" if s == d else "MISMATCH"
print(f"表 {t} 行数 源={s} 目标={d} 状态={status}")
# 业务校验:订单与支付金额合计应相等(字段名按真实 schema 调整,待核实)
order_sum = sum_amount(dst, "order", "amount")
pay_sum = sum_amount(dst, "payment", "amount")
print(f"金额合计 订单={order_sum} 支付={pay_sum} 状态={'OK' if order_sum == pay_sum else 'MISMATCH'}")
注意上面 Python 里用了 f-string 拼接 SQL,仅为演示简洁;生产环境请改用参数化查询或 SQLAlchemy 核心表达式,避免注入与类型问题。
引擎迁移:MySQL 与 PostgreSQL 的差异与 AI 辅助转换
跨引擎最头疼的是「同词不同义」。把常见差异列成清单交给 AI,让它逐项处理,比笼统说「帮我迁」准确得多。以下差异为社区广泛共识,具体语法以官方文档为准(待核实)。
- 自增主键:MySQL 用
AUTO_INCREMENT,PostgreSQL 推荐GENERATED ALWAYS AS IDENTITY(旧写法SERIAL也可用,但IDENTITY更标准)。 - 布尔:MySQL 用
TINYINT(1)模拟,PostgreSQL 用BOOLEAN。迁移时把1/0映射为TRUE/FALSE,并注意NULL是否需要补默认值。 - 标识符引号:MySQL 用反引号,PostgreSQL 用双引号。AI 转换时容易漏改,需在校验阶段用正则扫一遍目标 DDL 是否残留反引号。
- 零值时间:MySQL 允许
0000-00-00 00:00:00,PostgreSQL 不认。pgloader 默认会把这类零值改写为NULL,迁移前要确认业务能否接受。 - 引擎专属子句:
ENGINE=InnoDB、CHARSET=utf8mb4、ON UPDATE CURRENT_TIMESTAMP等在 PostgreSQL 无对应物,需逐个取舍。 - 字符串函数:拼接用
||还是CONCAT、分组拼接用STRING_AGG还是GROUP_CONCAT,应用层 SQL 也要跟着改。 - 分页与限制:
LIMIT 10 OFFSET 20两种引擎都支持,但 MySQL 还支持LIMIT 20, 10这种逗号写法,PostgreSQL 不认,需改写为标准LIMIT ... OFFSET ...。 - 自增边界:MySQL
INT UNSIGNED最大值约四十二亿,迁移到 PostgreSQL 时若用INTEGER可能溢出,应评估数据规模改用BIGINT或BIGINT GENERATED ... AS IDENTITY。
AI 的价值在于:你把这份差异清单和源 DDL 一起给它,它能稳定地逐字段产出映射,而不是靠人肉记忆每个类型。但最终一定要在目标库执行并跑测试,模型给的映射可能有版本偏差。把 AI 当作「草稿生成器」,把「正确性裁定」留给人加测试。
安全护栏:影子库、备份、幂等、灰度
生成能跑的迁移只是第一步,落地要靠护栏兜住意外。
- 影子库先行:先在隔离的影子库完整跑一遍迁移与校验,确认行数、约束、业务校验全过,再动真库。影子库还能用来压测迁移耗时,估算停机窗口。
- 全量备份:迁移前对源库做一次物理或逻辑全备,并确认备份可恢复。宁可多花十分钟备份,不要赌迁移不出错。
- 幂等脚本:迁移脚本写成可重复执行。比如建表前先
DROP TABLE IF EXISTS,或插入前先按主键去重。这样失败了重跑不会因「对象已存在」卡住。pgloader 默认就采用 DROP 加 CREATE 的可重复策略(官方文档已核验)。 - 灰度切换:大表或大库不要一刀切。可先迁只读副本验证,再迁从库,最后切主库;或者双写一段时间,新旧两套结构并存,比对一致后再下掉旧结构。
- 可观测与可回退:切换期间监控错误率与行数漂移;保留反向脚本,一旦发现目标库异常,能快速导回旧结构并把流量切回。
- 双写兜底:对关键表采取新旧结构双写一段时间,任何一侧失败都先告警而不是静默。双写期间用校验脚本持续比对两侧数据,差异收敛到零后再停掉旧写入。这套机制成本较高,但能在出问题时把影响控制在秒级。
- 权限最小化:迁移脚本使用的数据库账号只给必需的库表权限,避免「用管理员账号一把梭」导致误操作面扩大。迁移完成及时回收临时账号。
小结
数据库迁移的本质,是在「变动」和「不丢数据」之间找平衡。AI 能显著提升效率:它帮你把 DDL 从一种引擎映射到另一种、列出类型与语法差异、生成校验与回滚脚本。但它不是保险箱——模型的映射可能随版本偏差,生成结果必须在影子库跑通、用独立脚本校验、再配合备份与灰度护栏上线。记住一条铁律:任何迁移脚本,在没有演练过回滚路径之前,不要碰生产库。
参考与延伸阅读
- pgloader 官方文档(数据库到 PostgreSQL 的一键迁移与类型转换):https://pgloader.readthedocs.io/ —— 已核验,确认其支持从 MySQL 等源库流式迁移到 PostgreSQL,并默认采用可重复的 DROP 加 CREATE 策略。
- Alembic 官方文档(SQLAlchemy 生态的轻量级 schema 迁移工具):https://alembic.sqlalchemy.org/ —— 已核验,确认 Alembic 是配合 SQLAlchemy 使用的版本化迁移工具,支持自动生成、离线 SQL 输出与回滚。
- SQLAlchemy 官方文档(Python 数据库工具包,Alembic 的底层依赖):https://www.sqlalchemy.org/ —— 已核验(经 Alembic 文档关联确认),是本文校验脚本中
create_engine与text的来源。 - PostgreSQL 官方文档「数据类型」与「SQL 语法」:https://www.postgresql.org/docs/current/ —— 待核实,MySQL 到 PostgreSQL 的具体类型映射与版本差异请以该文档及 MySQL 官方文档为准。
- MySQL 官方文档「MySQL 到 PostgreSQL 迁移」相关章节:https://dev.mysql.com/doc/ —— 待核实,跨引擎语义差异的权威出处,建议迁移前逐条比对。