接手过 apiSQL 迁移到已有 PostgreSQL 数据库的活儿之后,我才意识到很多人把这件事想简单了。第一反应通常是“把数据库连接串从 MySQL 改成 PostgreSQL 不就完事了吗”,可真动起手来,你会发现这只是万里长征第一步。apiSQL 这类工具的本质,是把 SQL 能力封装成 HTTP API 供上层调用,一旦底层库换了,SQL 方言、数据类型、事务行为、权限模型甚至分页逻辑都会跟着变,任何一个环节没对齐,线上就会出一堆奇奇怪怪的问题。
这篇指南我会用自己的实操经验,把 apiSQL 迁到已有 PostgreSQL 的完整链路拆开讲清楚。无论你手上是一套自研的 apiSQL 服务,还是基于开源方案二次开发的数据访问中间件,都可以照着这个思路走。内容覆盖迁移前的盘点、数据与结构的搬运、方言适配、灰度验证和回滚策略,以及那些文档里不会写的坑。适合正在做数据库替换的后端开发、DBA,以及负责数据中台或内部数据平台的运维同学。
1. 迁移的本质:表面是换库,实际是系统性改造
1.1 先搞清楚 apiSQL 在你的架构里扮演什么角色
apiSQL 通常是介于应用和数据库之间的一层薄薄的“翻译官”。业务方不直接写 SQL,而是通过 HTTP 请求发起查询或写入,apiSQL 内部完成鉴权、参数校验、SQL 拼接、执行和结果封装。这种设计在数据服务中台、BI 自助查询、内部运营后台里都很常见,好处是收口了数据访问入口,坏处是底层数据库的方言和特性被透传到了 API 层,迁移时要把这些透传出来的“方言尾巴”全部揪出来。
以我手头这套基于 Python 的 apiSQL 服务为例,它依赖 SQLAlchemy 做数据库无关的ORM映射,大部分基础操作可以做到透明切换,但那些直接写了原生 SQL 的接口、用数据库特有函数做的排序、带特定语法的大批量更新,全都在迁移清单上。所以迁移的第一步永远不是开搞,而是画一张“当前数据库能力使用地图”,把每个接口和它依赖的数据库特性对应起来。
1.2 迁移前必须想清楚的三个问题
第一个问题:迁移的终点是“已有 PostgreSQL”,那这个实例的版本是什么、部署在内网还是云上、现有负载还有多少余量。PostgreSQL 不同大版本之间的行为差异不小,比如jsonb的操作符、分区表语法、并行查询策略,都和版本强相关。我建议先记录目标库的完整版本号和关键配置,后面适配 SQL 时要对着版本来判断。
第二个问题:源库是什么类型。从 MySQL 迁到 PostgreSQL 和从 SQLite 迁到 PostgreSQL,完全是两个难度级别。MySQL 那边要重点处理AUTO_INCREMENT与序列、ENGINE=InnoDB注释、反引号引用的 SQL、ON DUPLICATE KEY UPDATE这类语法;SQLite 则要小心动态类型带来的隐式转换和数据倾斜。如果源库是 SQL Server 或 Oracle,那还得额外处理分页写法、空值排序、字符串拼接函数的差异。先明确这一点,后面才能快速聚焦。
第三个问题:业务是否可以接受短暂中断。迁移方案的选择直接受停机窗口约束。能停 30 分钟,可以用全量导入加一次增量补偿的方式;要求不停机,就得引入基于日志的增量同步工具。apiSQL 场景下,我倾向于先做全量搬迁,再在低峰期切换连接串,因为 apiSQL 层本身是无状态的,切换连接串的瞬间只需要把网关后端配置更新一下,几乎可以做到分钟级恢复。
2. 迁移前的盘点:摸清 apiSQL 现状和 PostgreSQL 的准备
2.1 应用画像:哪些东西必须跟着走
我会建议把需要迁移的内容分成四类,分别建清单。
第一类是表结构,包括建表语句、字段类型、默认值、注释、主键外键、唯一约束和索引。这一部分最耗时,因为 MySQL 与 PostgreSQL 的类型体系差异很大:TINYINT要转成SMALLINT,DATETIME要转成TIMESTAMP,LONGTEXT转成TEXT,BLOB转成BYTEA,ENUM要用 PostgreSQL 的CREATE TYPE或VARCHAR + CHECK来模拟。第二类是数据本体,包括每张表的行数和数据大小、是否有大字段、分区表、历史归档表。第三类是数据库对象,包括视图、存储过程、函数、触发器,这类对象在 apiSQL 中通常以SELECT * FROM some_view的形式被调用,迁移时很容易被漏掉。第四类是应用侧的静态配置,包括连接池大小、事务隔离级别、驱动参数、SQL 超时设置。
我在实际迁移时会给每张表建一个迁移记录表,字段包含:表名、源库行数、目标库行数、校验结果、是否包含大对象、迁移耗时。没有这张表,后面做数据校验根本无从谈起。
2.2 目标端 PostgreSQL 的核对清单
已有 PostgreSQL 实例不等于“拿来就能用”。先把这些基础项核对完,再开始建表导数据。
先检查字符集和排序规则,目标库最好和源库保持一致,或者至少在兼容性上确认UTF8编码,否则中文数据导入后可能出现乱码。再检查扩展模块,如果业务用到了JSONB的高级查询、pg_trgm模糊搜索、uuid-ossp生成 UUID,需要CREATE EXTENSION提前装好。然后检查表空间和磁盘容量,用SELECT pg_size_pretty(pg_database_size('你的库名'));看现有占用,再预留源库数据大小 1.5 倍以上的空间。还要检查max_connections、shared_buffers、work_mem等性能参数,如果目标实例是和其他业务共用的,要考虑高峰期互相挤兑的风险。
此外,还有权限问题。apiSQL 连接数据库的账号通常需要SELECT、INSERT、UPDATE、DELETE权限,如果涉及建表或迁移数据,还需要CREATE和USAGE权限。共用实例上,建议给 apiSQL 单独建一个 schema(如apisdb),不要和别人的表混在public里。PostgreSQL 的search_path在连接串里就能指定,非常清爽。
2.3 为什么优先选用已有实例而不是新建库
有些团队为了省事会专门开一个全新的 PostgreSQL 实例,再把 apiSQL 连过去。除非那个实例是私有化部署、完全隔离的独立资源,否则我并不推荐。共用已有实例的核心优势是运维体系复用:备份策略、监控告警、高可用切换都已经在跑,etcd 一样的道理,你额外引入一套库环境,就得额外承担一套运维成本。而且 apiSQL 大多数场景读多写少,放在已有实例上只要做好 schema 隔离和连接池管控,资源争抢完全可控。
真要新建实例,也要在同一个集群或同一套备份策略下创建,避免“野库”没人管。我见过不止一次,迁移团队高高兴兴把数据导过去了,半年后才发现新库从未纳入备份计划,一次磁盘故障直接丢数据。这种事一旦发生,前面的迁移成果全都白费,所以我在任何迁移方案里都会把“目标库必须纳入现有备份体系”列为强制验收项。
3. 核心环节:数据迁移的三种主流实操路径
3.1 路径一:pgloader 快速搬运
如果源库是 MySQL 或 SQLite,pgloader是我首选的搬运工具。它最大的优点是自动处理类型映射和索引重建,而且支持流式读取,不会把源库内存吃满。安装方式很简单,macOS 上可以用brew install pgloader,Debian/Ubuntu 可以直接apt install pgloader,它依赖的sbcl会自动装好。
一个最常用的 MySQL 到 PostgreSQL 迁移命令长这样:
pgloader mysql://user:password@source-host:3306/source_db \ postgresql://user:password@target-host:5432/target_db它会自动创建表、转换字段类型、迁移数据,并在最后输出一份统计报告,告诉你每张表迁移了多少行、花了多长时间、有没有错误。我用它迁移过一张 2 亿行的流水表,速度比手写脚本快得多。但如果源库是 Oracle,pgloader 支持度不够,我一般换用ora2pg先生成转换后的 SQL,再手工导入。
使用 pgloader 时有几个点要特别注意。源库账号必须拥有读取所有目标表的权限,且目标库的账号要有建表权限。迁移过程中如果遇到外键顺序问题,pgloader 会自动先禁用约束导完数据再启用,但前提是它识别得出外键。另外,pgloader 对 MySQL 的ENUM类型会转成TEXT,会丢失枚举约束,如果业务依赖这一约束,要在迁移后手工补CHECK约束。
3.2 路径二:先结构后数据的组合拳
pgloader 适合“一把梭”,但如果你不想把结构迁移完全交给工具,或者需要精细化控制 DDL,我推荐先导结构、再导数据的两步走。
先导结构,可以从源库获取建表语句。MySQL 可以用mysqldump --no-data --skip-comments --skip-add-locks导出纯结构,再用sed或手工替换的方式转成 PostgreSQL 语法。这一步不要偷懒,用pgAdmin的 schema diff 工具或者Apache Sedona这类转换工具辅助,但最终要人工过一遍 DDL。比如 MySQL 的:
CREATE TABLE users ( id INT NOT NULL AUTO_INCREMENT, name VARCHAR(50) NOT NULL, status ENUM('active', 'disabled') DEFAULT 'active', PRIMARY KEY (id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;要改成 PostgreSQL 的:
CREATE TABLE users ( id SERIAL PRIMARY KEY, name VARCHAR(50) NOT NULL, status VARCHAR(20) NOT NULL DEFAULT 'active', CONSTRAINT chk_users_status CHECK (status IN ('active', 'disabled')) );注意这里我用VARCHAR + CHECK模拟ENUM,是因为 PostgreSQL 的CREATE TYPE虽然更严谨,但后续ALTER TYPE ADD VALUE需要单独事务执行,在业务迭代频繁时反而容易卡住发布。SERIAL在 PostgreSQL 高版本里也可以用GENERATED BY DEFAULT AS IDENTITY替代,后者更符合标准 SQL,两种写法都值得掌握。
数据导入这一步,我推荐直接用COPY命令或者pg_restore,而不是逐条INSERT。原因很简单,PostgreSQL 的普通INSERT走的是 SQL 解析、计划生成、执行三层,每条数据都有固定开销;而COPY直接走文件流,速度能差一个数量级。实战中,我通常先把源表导成 CSV 或 TSV 文件,再通过:
\copy target_table FROM '/path/to/export.csv' WITH (FORMAT CSV, HEADER true, NULL 'NULL')批量导入。如果单个文件太大,先用split切分,再开多个\copy会话并行执行。注意并行度不要超过目标库max_connections的一半,给线上其他业务留足余量。
3.3 路径三:逻辑导出导入与大表处理
当源库数据达到几十亿行、单表超过几百 GB 时,上面两种全量方式就不太现实,至少要优化搬运顺序。我的习惯是:先按表特点分层处理。
小表和配置表,直接全量搬;大维表,按最后更新时间分批搬;流水类的事实表,优先搬最近 90 天数据,历史数据放归档分区。PostgreSQL 很擅长分区表,迁移前就可以把目标表建成按时间分区的结构,比如按月份PARTITION BY RANGE (create_time),搬数据时每导完一个分区就做一次ANALYZE,保持统计信息新鲜。
针对大表的具体操作,我会先导出所有数据,处理掉大字段数据类型的兼容性冲突,再分批导入。处理超长文本时,MySQL 的TEXT默认字符集是 UTF8,导入 PostgreSQL 后长度运算的单位会变成字符,虽然一般不影响存储,但如果你建表时设置了VARCHAR(n),字符长度超限会导致导入失败。大表导入前,一定先用SELECT max(length(字段)) FROM 源库表摸清长度分布,再决定目标字段用VARCHAR(65535)还是TEXT。
3.4 自增列、序列和约束的修复
PostgreSQL 和 MySQL 在自增列上的实现方式完全不同,这是迁移中最容易被忽视的经典坑。MySQL 的AUTO_INCREMENT最大值记录在表元数据里,插入了多少行,下一条继续往后走就行。PostgreSQL 的自增列依赖SEQUENCE,而CREATE TABLE建表时并不会自动把序列的last_value跳到当前数据最大值之后。
如果你先导结构再导数据,导入完成后直接插入新记录,大概率会碰到主键冲突——因为序列还停在 1。修复命令是:
SELECT setval(pg_get_serial_sequence('users', 'id'), (SELECT max(id) FROM users));如果多张表都有自增列,可以用一条动态 SQL 批量处理,或者在迁移脚本里统一遍历。还有一点:外键约束、唯一索引要在数据导入完成后再重建,而不是建表时一起建。原因很简单,先建约束再导入,每插一行都要做一次约束检查,大表性能会断崖式下降。正确顺序是:建表(不含约束和索引)→ 导数据 → 创建索引 → 添加约束 → 校验。我习惯把全部CREATE INDEX和ADD CONSTRAINT语句放到最后一个 SQL 文件里,统一执行并输出日志。
4. apiSQL 配置适配:让应用跑在 PostgreSQL 上
4.1 连接与驱动配置
数据搬过去只是第一步,apiSQL 服务本身也要改配置。以 Python 技术栈为例,如果 apiSQL 底层用的是psycopg2,连接串要改成:
DATABASE_URL = "postgresql://user:password@target-host:5432/target_db?search_path=apisdb"如果是从 SQLAlchemy 连接,直接用postgresql+psycopg2://前缀即可。连接池参数也要同步调整。MySQL 的连接很轻,但 PostgreSQL 每个连接会占用约 10MB 内存(视work_mem而定),如果 apiSQL 原来给 MySQL 配了max_connections=200,迁到 PostgreSQL 就不能直接照抄,建议先配max_overflow=20、pool_size=20,再根据监控逐步调整。
同时检查 apiSQL 的事务隔离级别。PostgreSQL 默认是READ COMMITTED,和很多 MySQL 服务端的REPEATABLE READ行为不同。如果业务依赖“一个事务内两次查询结果一致”,要么显式设置SET TRANSACTION ISOLATION LEVEL REPEATABLE READ,要么在 SQLAlchemy engine 参数里传isolation_level="REPEATABLE READ"。
4.2 SQL 方言差异:这些写法必须改
这一步是 apiSQL 迁移的重头戏。我列一张高频差异对照表,帮你省掉踩坑时间:
| 场景 | MySQL 写法 | PostgreSQL 写法 |
|---|---|---|
| 字符串拼接 | CONCAT(a, b)/ `a | |
| 分页 | LIMIT 10 OFFSET 20 | LIMIT 10 OFFSET 20(语法一致,但OFFSET过大时建议改游标) |
| 插入冲突更新 | INSERT ... ON DUPLICATE KEY UPDATE | INSERT ... ON CONFLICT (id) DO UPDATE SET |
| 字段引用 | 反引号`name` | 双引号"name"(且大小写敏感) |
| 布尔值 | TRUE/FALSE可当1/0 | TRUE/FALSE/'t'/'f',严格区分 |
| 日期函数 | NOW()、CURDATE() | NOW()、CURRENT_DATE |
| 自动分页 | LIMIT可以省略 | 部分情况必须显式LIMIT ALL |
| 别名 | SELECT name n FROM users | SELECT name AS n FROM users(AS可选但建议写) |
除此之外,还有两个更隐蔽的差异。一个是 PostgreSQL 对未加引号的标识符会自动转成小写,所以原来 MySQL 里叫UserList的表,到了 PostgreSQL 会变成userlist。如果 apiSQL 接口里拼了大小写混合的表名,一定要在目标库里用双引号显式建表或者统一改成小写命名。另一个是GROUP BY的严格性,PostgreSQL 默认要求SELECT中非聚合列必须出现在GROUP BY里,MySQL 宽容得多。迁移后会出现大量ERROR: column "xxx" must appear in the GROUP BY clause,这种 SQL 在 apiSQL 层就要重新改写。
4.3 API 层与权限模型适配
apiSQL 之所以叫 apiSQL,就是因为它在 API 层拦截并执行 SQL。迁移后,权限模型也要跟着改。MySQL 的账号是基于 host+user 的,PostgreSQL 是角色+对象权限,逻辑上差异很大。我的建议是给 apiSQL 创建一个专用角色,然后:
-- 创建只读账号(如果 apiSQL 只处理查询) CREATE ROLE apisql_ro LOGIN PASSWORD 'strong_password'; GRANT CONNECT ON DATABASE target_db TO apisql_ro; GRANT USAGE ON SCHEMA apisdb TO apisql_ro; GRANT SELECT ON ALL TABLES IN SCHEMA apisdb TO apisql_ro; ALTER DEFAULT PRIVILEGES IN SCHEMA apisdb GRANT SELECT ON TABLES TO apisql_ro;ALTER DEFAULT PRIVILEGES这行很关键,它保证以后新建的表自动赋权,否则每次建表都要手工GRANT一遍。如果 apiSQL 还要执行写入操作,再把对应权限加进去。如果 apiSQL 内部有独立的接口级权限控制,数据库层就只给最小权限,两边叠加,安全系数更高。
5. 验证、灰度与回滚方案
5.1 数据一致性校验
数据导完不能直接切换,必须做一致性校验。最简单的方式是行数比对,但对大表来说行数一样不代表数据一样。我更推荐用基于哈希的抽样校验:对每张表计算一条聚合校验值,源库和目标库都比较一遍。
PostgreSQL 端可以这样算:
SELECT md5(string_agg(t.row_hash, '')) AS table_hash FROM ( SELECT md5(record::text) AS row_hash FROM target_table ) t;MySQL 端用GROUP_CONCAT加MD5也有类似效果。但全表哈希在超大表上会很慢,所以我实际会分层:核心业务表全量校验,流水表按id % 100抽 5 个分片校验。结果对不上时,先确认是不是时点不一致(源库还有增量写入),再考虑是否要重导该分片。
5.2 功能回归测试
数据校验通过后,apiSQL 侧的验证同样重要。我会准备一个回归测试脚本,覆盖以下内容:每个接口能否正常返回;增删改操作是否生效;分页、排序、模糊搜索是否符合预期;大结果集响应时间是否达标;并发请求是否会触发死锁或连接池耗尽。
特别要测的是那些用到数据库特有函数的接口。比如原来用 MySQL 的DATE_FORMAT(create_time, '%Y-%m-%d')格式化日期,PostgreSQL 要换成TO_CHAR(create_time, 'YYYY-MM-DD'),返回格式也有细微差别。这类问题在测试环境根本发现不了,因为数据量小,接口不报错,只有到了生产环境才会暴露格式不对、精度丢失。所以我强烈建议在预发环境放一份真实数据量的子集,用压测脚本把高频接口全部扫一遍。
5.3 回滚策略
迁移必须有退路。我的回滚设计分两层。
第一层是应用层回滚:保留一套切换前的 apiSQL 配置文件,如果切换后发现 PostgreSQL 这边有致命问题,改一下网关配置就能重新连接旧库。因为 apiSQL 做的是一次连接串切换,数据搬移期间旧库并没有停止写入,所以回滚不会丢数据,只是要让旧库把切换期间的增量补回来。第二层是数据层回滚:在切换前做一次全量备份到对象存储,保留归档文件。万一 PostgreSQL 数据损坏,可以用备份重建。
实际操作中,我更推荐用“先切读流量、再切写流量”的方式灰度。apiSQL 的网关层如果支持按请求头分发流量,就把 5% 的读请求引到 PostgreSQL,观察接口报错率和响应延迟;确认没问题后扩大到 50%,最后全量切换写流量。整个过程可以在一个发布窗口内完成,风险远比一步到位低。
6. 常见问题与避坑经验
6.1 高频问题速查
| 现象 | 可能原因 | 解决办法 |
|---|---|---|
数据导入时报invalid byte sequence for encoding "UTF8" | 源库存在非 UTF8 字符 | 用iconv -f GBK -t UTF-8预处理 CSV,或修源数据 |
| 插入后查询很慢 | 索引缺失,或统计信息未更新 | 导入后执行ANALYZE;,按查询模式补齐索引 |
大批量插入报out of shared memory | max_locks_per_transaction不够 | 分批提交,或调大该参数 |
接口返回 500,错误是relation does not exist | search_path没指对 schema 或表名被转小写 | 连接串加search_path=apisdb,检查表名大小写 |
| 分页越翻越慢 | OFFSET过大 | 改用 keyset 分页(WHERE id > last_id ORDER BY id LIMIT n) |
| 主键冲突 | 序列未重置 | 执行setval修复自增序列 |
6.2 迁移团队的独家建议
最后分享几条只有干过的人才会注意到的经验。
第一条,导数据前先把目标表的autovacuum相关行为搞清楚。PostgreSQL 的VACUUM和ANALYZE是后台自动跑的,但大表刚导入完时,统计信息通常是陈旧或缺失的。没有统计信息,查询优化器会乱选执行计划,接口响应慢好几倍。所以大表导入完第一件事,就是跑ANALYZE。
第二条,不要天真地用同一条INSERT语句处理所有表。带BLOB、BYTEA的表和普通数字表在COPY时的表现完全不同,大对象字段建议单独拉出来走二进制流导入,避免编码转换损耗。
第三条,apiSQL 迁移最容易出问题的不是数据,而是连接池预热。切换完成后的一段时间,连接池会大量建立新连接,如果目标 PostgreSQL 的max_connections已经接近上限,这一波预热可能直接把库打挂。我会在正式切换前手动执行一轮连接预创建,比如用一个脚本先建立 30 个连接再释放,让数据库端的进程池先起来。
第四条,也是最想提醒你的一点:迁移过程中不要修改 apiSQL 的代码逻辑。迁移和业务功能迭代要严格分开。哪怕你发现某个接口在 PostgreSQL 下可以顺手优化,也不要在这时候改。把问题记下来,等迁移稳定后再发新版,否则一旦出了故障,你根本分不清是迁移问题还是业务改动问题。
根据我的经验,apiSQL 迁移到已有 PostgreSQL 这件事本身不算复杂,复杂的是把边界划清楚,把依赖关系摸透,把验证做扎实。一次成功的迁移,不应该是有惊无险的鏖战,而是每一步都能预测、都能回滚的例行操作。希望这份指南能让你少走一点弯路,也少熬几个大夜。