从本地PostgreSQL迁移到Supabase:全流程实战与避坑指南

发布时间:2026/10/5 4:17:36
从本地PostgreSQL迁移到Supabase:全流程实战与避坑指南 上个月我接了个活儿把一个跑了两年的本地项目整体搬到 Supabase 上。项目不大不小前后加起来三十六张表外键、触发器、视图、序列一个不少。当时想得很简单觉得“不就是导出再导入嘛”真动起手来才发现从本地数据库到云平台这条路上全是细节每一步都有可能让后面全线崩盘。这篇就把我整理出来的完整流程和踩过的坑记下来给准备做同类型迁移的朋友当个参考。后面可能还打算做类似迁移的人特别是后端开发和独立开发者这篇应该能帮你省掉一大半排查时间。Supabase 这个例子选得比较典型它本质上是托管 PostgreSQL 加一套 BaaS 能力所以从本地 PostgreSQL 迁过去的路子几乎可以平移借鉴到任何云数据库平台。哪怕你的目标平台是 RDS、Neon、或者是别的什么托管 PG 服务核心思路都是相通的。下面不废话直接说正事。1. 迁移前的盘点别急着导出先把家底摸清楚很多人拿到迁移任务第一反应就是打开终端执行 pg_dump。我劝你停一下。迁移这种事前期梳理越细后面收拾残局的时间就越少。第一步不是导出而是把源库和目标库的差异彻底搞清楚。1.1 确认源端与目标端的“方言”差异先问自己一个问题本地库到底是 PostgreSQL还是 MySQL又或者是别的什么这一点直接决定了后面的迁移难度。同一个 PostgreSQL 体系内迁移基本上就是版本兼容问题本地是 PG 13/14Supabase 目前是 PG 15只要不用太冷门的扩展问题不大。但如果是从 MySQL 迁到 Supabase那就要做一层“翻译”因为两边对数据类型、索引、自增主键、事务行为的处理方式截然不同。我整理了一份常见的数据类型对照表如果你是从 MySQL 过来基本可以参考这个映射关系MySQL 类型PostgreSQL/Supabase 类型说明INT AUTO_INCREMENTBIGSERIAL / IDENTITY显式自增注意序列处理TINYINT(1)BOOLEANMySQL 用长度 1 的 tinyint 表示布尔DATETIMETIMESTAMPTZ时区处理逻辑不同建议统一带时区TIMESTAMPTIMESTAMPTZ同上避免时间错乱ENUMVARCHAR CHECK / 自建 ENUMPG 有原生 enum但后续加值维护麻烦JSONJSONBJSONB 支持索引查询性能好很多TEXT / VARCHARTEXT / VARCHAR长度语义基本一致注意 VARCHAR 长度定义这里有一个非常容易踩的坑MySQL 的标识符默认大小写敏感度取决于 lower_case_table_names 参数而 PostgreSQL 会把所有不带引号的标识符强制转为小写。也就是说如果原 MySQL 库里有一张表叫UserInfo导入 PostgreSQL 之后要么变成userinfo要么变成必须带引号才能访问的UserInfo。这个细节放到后面第 4 部分详细说这里先记住一句话迁移前统一用小写加下划线的命名风格能省掉很多后面的麻烦。1.2 把表、外键、触发器、序列的依赖关系画出来这一步听着麻烦其实是整个迁移里最有价值的一个动作。你需要弄清楚这几件事一共有多少张表每张表大概多少行数据哪些表之间有外键依赖谁引用谁哪些表上挂着触发器、视图、物化视图哪些字段用的是序列自增序列的当前值是多少有没有自定义函数、存储过程我习惯直接跑一段 SQL 把外键关系拉出来虽然结果比较粗糙但梳理够用了SELECT conrelid::regclass AS table_name, confrelid::regclass AS referenced_table, conname AS constraint_name FROM pg_constraint WHERE contype f ORDER BY 1;触发器可以用这条查SELECT event_object_table AS table_name, trigger_name, action_timing AS timing, event_manipulation AS event FROM information_schema.triggers ORDER BY event_object_table;序列的当前值则是这样SELECT c.relname AS sequence_name, last_value, is_called FROM pg_sequences s JOIN pg_class c ON c.relname s.sequencename;为什么要做这一步因为导入云平台的时候外键约束的存在会导致导入顺序非常讲究。你得先导父表、再导子表否则插入数据时外键直接报错一条都进不去。触发器也是一样某些触发器会在导入数据时被意外触发导致数据被改写或拦截。把这些依赖提前摸清楚后面导出导入的时候就能按依赖顺序排兵布阵。1.3 目标端环境确认别在连接串上卡壳Supabase 本身的环境检查也不复杂但容易被忽视。你要确认几件事项目的 Region也就是数据库物理位置数据库的连接串通常长这样postgresql://postgres:[YOUR-PASSWORD]db.[REF].supabase.co:5432/postgres是否启用了 SSL 连接Supabase 默认要求 SSL项目里已经预装的相关扩展比如 pgcrypto、uuid-ossp 这些还有一个非常关键的认知差Supabase 的数据库账号不是超级用户。Dashboard 里那个 postgres 账号虽然有比较高的权限但它不能执行所有本地 PG superuser 能执行的操作。这一点会在后文导数据的时候反复出现比如某些扩展装不上、某些 SET 语句不允许执行都需要特殊处理。迁移前的检查我一般列成一张清单做完一项划掉一项检查项状态备注源库数据库类型与版本完成PG 14表数量 / 数据量统计完成36 张表 / 约 70 万行外键依赖关系列表完成31 个外键触发器清单完成8 个序列清单与当前值完成9 个序列目标端连接串完成SSL 已开启目标端预装扩展列表完成pgcrypto 存在这张表可以按你自己的项目改。核心思路是落袋为安确认了再往下走不然你会在迁移中途发现“某个依赖对象少导了”然后回来重跑一遍。2. 导出阶段别一把梭用好 pg_dump 的三个关键参数导出阶段是整个迁移里最有技术含量的一步。很多人在这一步就犯了致命错误拿个图形化工具选中表点个“转储 SQL 文件”然后发现导出来的文件要么没有外键要么没有序列要么数据量一大就内存溢出。这里我直接给出我验证过的方案。2.1 为什么主推 pg_dump而不是图形工具或者 CSV先说结论pg_dump是 PostgreSQL 自带的逻辑备份工具它能保留表结构、数据、索引、约束、触发器、函数、序列的完整依赖顺序。这个“依赖顺序”是手工导出 CSV 完全没法保证的。CSV 方式看着简单但你要自己管理建表顺序、外键顺序、序列同步数据量一大人很容易乱。图形化工具其实底层调用的也是 pg_dump。例如 DBeaver 或者 Navicat 的“转储”功能本质上是封装了 pg_dump 的参数。如果你对这些工具不够熟悉直接命令行反而更可控、更好排查。而且命令行导出的文件是标准文本或自定义格式放到任何环境都能重复使用。pg_dump还有一个特别有用的特性它默认是在导出开始时做一个一致性快照也就是说即便业务在读库导出的数据也是一致的时间点数据不会出现表 A 是上午的数据、表 B 是下午的数据这种错乱。这一点是手工导 CSV 做不到的。2.2 推荐的三段式导出结构、数据、对象分离我推荐的导出方式不是一次性pg_dump一把梭而是“三段式”。思路是先把结构导出来再导数据最后单独处理函数和触发器。这样做的好处是结构文件用于建库数据文件可以随时重导函数和触发器单独管理更方便排错。第一步导出表结构不包含数据pg_dump -h localhost -U postgres -d mydb \ --schema-only \ --no-owner \ --no-privileges \ -f mydb_schema.sql第二步导出数据不包含结构pg_dump -h localhost -U postgres -d mydb \ --data-only \ --no-owner \ --no-privileges \ -f mydb_data.sql第三步导出函数和触发器pg_dump -h localhost -U postgres -d mydb \ --sectionpre-data \ --sectionpost-data \ --no-owner \ --no-privileges \ -f mydb_objects.sql这三个参数是我反复用下来的核心组合--schema-only只导结构不导数据适合先建表--data-only只导数据导入时不会碰已存在的表结构--no-owner --no-privileges这两个参数非常关键。本地库的 owner 一般是你的本地用户名而 Supabase 里没有这个角色。如果不加这两个参数执行 SQL 时就会报role yourname does not exist卡在开头。还有人问要不要用-Fc自定义格式。-Fc的好处是可以用pg_restore灵活选择要恢复哪些对象实测在迁移到 Supabase 的场景里直接用纯 SQL 文件搭配 psql 执行更省事。因为 Supabase 的在线 SQL 编辑器接受的就是纯 SQL 文件你不需要额外套一层 pg_restore 的格式转换。2.3 从 MySQL 或者其他数据库迁移过来怎么办如果你是从 MySQL 迁过来mysqldump 导出的 SQL 基本不能直接给 PostgreSQL 用。常见的转换工具有pgloader不过实际我试用下来小项目用 pgloader 够用表一多、字段一杂还是容易出现类型映射不准和中文注释丢失的问题。我的实际建议是mysqldump 导出一个中间格式别想着一步到位。具体做法是先用 mysqldump 导出为 CSV按表拆开写一个小脚本把 MySQL 的类型定义翻译成 PostgreSQL 类型用COPY FROM批量导入这里贴一个 mysqldump 导出 CSV 的参考命令mysqldump -u root -p --tab/tmp/data --fields-terminated-by, mydb这个命令会为每张表生成一个.sql建表语句和.txt数据文件。然后你照着第 1.1 节的类型映射表把建表 SQL 翻译成 PG 版本再用COPY导数据COPY users FROM /tmp/data/users.txt WITH (FORMAT csv, DELIMITER ,);需要注意表名大小写的问题。MySQL 在 Linux 下默认区分表名大小写导到 Postgres 之后你建的表如果用了大写字母后续请求就会遇到我前面说的“引号表名”问题。迁移前在 MySQL 端先把表名统一改成小写下划线比迁移后批量改容易得多。2.4 导出之后先本地预检别直接上云这一步是我强烈建议的省钱省时操作。导出的 SQL 文件先在本地跑一个空的 PostgreSQL 实例验证一遍确认整个文件可以完整执行再拿去线上。我已经不止一次看到有同事把带着语法错误的 dump 文件直接怼到云数据库上结果执行到一半报错留下一堆半建好的表清洗起来非常痛苦。本地预检最简单的方式是 Docker 起一个干净的库docker run --name pg_precheck -e POSTGRES_PASSWORD123456 -d postgres:15然后依次执行docker exec -it pg_precheck psql -U postgres -f - mydb_schema.sql docker exec -it pg_precheck psql -U postgres -f - mydb_data.sql docker exec -it pg_precheck psql -U postgres -f - mydb_objects.sql如果本地预检全绿再往 Supabase 上导。你会在这一步提前发现很多问题比如扩展缺失、类型不匹配、外键顺序不对而不是到云上再反复试错。3. 导入 Supabase三种通道和一条铁律导入阶段通常是大家最没底的环节。Supabase 给了你三种方式把 SQL 灌进去Dashboard 的 SQL 编辑器、psql 命令行、supabase CLI。我挨个说清楚适用场景并给出推荐顺序。3.1 场景一SQL 编辑器只适合小文件Supabase Dashboard 左侧有一个 “SQL Editor”可以直接粘贴 SQL 并执行。它的优点是零门槛在浏览器里就能搞定。缺点是单次执行文件不宜过大超过几 MB 就容易超时文件里有COPY ... FROM stdin这种块的时候编辑器基本跑不动执行过程中出现错误不支持方便的断点续传你得手动定位所以 SQL 编辑器只适合导入单张小表或者执行一些简单的授权语句。正式迁移的核心文件请走下面这条路。3.2 场景二psql 命令行主力方式psql 是 PostgreSQL 自带的客户端Supabase 连接库也是用它。这是最稳、最好排查问题的通道。连接示例psql postgresql://postgres:your_passworddb.xxx.supabase.co:5432/postgres?sslmoderequire连接上之后执行\i mydb_schema.sql \i mydb_data.sql \i mydb_objects.sql这里有两个小细节第一一定要开启ON_ERROR_STOP。这样脚本遇到第一条错误就会停下来不会带着错误继续执行。执行方式是psql postgresql://postgres:...?...sslmoderequire -v ON_ERROR_STOP1 -f mydb_schema.sql第二如果文件比较大不要直接粘贴用\i让 psql 自己读文件这样终端不会卡死。文件特别大时还可以用管道方式cat mydb_data.sql | psql postgresql://postgres:...?...sslmoderequire -v ON_ERROR_STOP1实测比较稳妥。3.3 场景三supabase CLI适合后续 Schema 变更supabase CLI 不仅是本地开发工具也能做db push操作把本地结构变更同步到远程。但它更适合“迁移完成后日常变更 schema”的场景比如在本地改了一个字段类型然后推到云上。初次全量导入大数据时我还是推荐用 psql 而不是 CLI因为 CLI 对大数据量的导入控制力不够出了问题也不好定位。初始化命令大致是这样supabase init supabase link --project-ref your-project-ref supabase db push这里不展开太多CLI 的用法可以作为后续运维手段不塞进初次迁移的主流程里减少变量。3.4 导入后的数据核验别看到零错误就觉得万事大吉导入流程执行完了不代表迁移成功。真实检验标准是新库里能正常读写业务接口能跑通数据量和源库对得上。我每次迁移完必做三件事第一逐表核对行数SELECT users AS tbl, count(*) FROM users UNION ALL SELECT orders AS tbl, count(*) FROM orders;第二核对序列是否同步。这个很好理解如果你有一张users表主键 id 是序列生成的数据导进来之后序列的当前位置可能还停在 1那下次插入新记录就会报主键冲突。先把序列重置到当前最大值SELECT setval(users_id_seq, (SELECT max(id) FROM users));如果你不愿意一个个表手工写可以批量生成 setval 语句SELECT SELECT setval( || quote_literal(sequence_name) || , (SELECT COALESCE(max( || column_name || ), 1) FROM || table_name || )); FROM information_schema.columns WHERE column_default LIKE nextval%;第三抽查外键约束。随机找几条子表记录带外键关联查一下父表确认引用关系没有错位。到这一步数据层的迁移基本就算完成了。4. 最容易踩的坑我从实际项目里捡出来的问题清单这一部分是我最想写的。下面的每一个问题都是我或者身边朋友真实遇到过、排查过、解决过的。你大概率也会碰到其中的一两个。4.1 RLS 权限导致的“库里有数据但 API 查不到”这个坑太典型了。数据通过 psql 导入后在 Supabase SQL 编辑器里查询一切正常但通过前端 API 查询却返回空数组或者 403。原因是什么Supabase 的 API 层走的是 PostgREST这个接口默认以anon角色访问数据库。你的表如果此前没有开启 Row Level Security而且没有给anon角色授权PostgREST 就查不到数据因为默认情况下新表对 unauthenticated 角色是不可见的。解决办法是在表上启用 RLS并添加一条允许读取的 policy或者直接给anon角色授权。我推荐用 RLS policy因为更安全也更符合 Supabase 的设计理念。示例alter table public.users enable row level security; create policy anon_read_users on public.users for select to anon using (true);注意这里using (true)是在本示例的场景下允许全部读取实际项目一定要按业务来收紧否则就是把用户数据公开挂墙上了。这个坑导致很多人误以为数据导丢了其实是授权没跟上。4.2 序列不同步导致的“主键冲突”数据导入成功接口跑通结果正常写一条新数据就报duplicate key value violates unique constraint users_pkey。这是序列没归位的老问题。前面给过 setval 的写法这里补充一个细节Supabase 的序列名和本地不一定完全一样。本地序列名可能是users_id_seq导入后还是这个名字但你要确认一下序列到底真实存在不存在。用pg_get_serial_sequence查是最准的SELECT pg_get_serial_sequence(public.users, id);查出来之后再 setval不至于猜错序列名。4.3 扩展缺失导致的“函数不存在”本地库经常用一些第三方扩展比如pgcrypto、uuid-ossp、citext。Supabase 预装了一部分但并不是全部。最典型的报错是执行某条 SQL 时提示function gen_random_uuid() does not exist这就是pgcrypto扩展没装。在 Supabase 里补装扩展很简单create extension if not exists pgcrypto; create extension if not exists uuid-ossp; create extension if not exists citext;需要注意的是某些扩展需要超级用户权限Supabase 不能给你完整的 superuser 角色所以部分冷门的扩展装不上。遇到这种情况替代方案是用原生能力改写或者提前在 Supabase 工单里确认是否支持。如果你在本地预检阶段把扩展依赖查清楚这一步基本不会踩雷。4.4 触发器迁移后不生效实时订阅消失如果你的本地库里有触发器比如“插入时自动更新时间戳”这类pg_dump 导出的结构文件里会带上触发器定义所以基础触发器没问题。但有一类东西不会跟着迁移Supabase 的 Realtime API。我遇到过的情况是本地库里有一张表业务代码监听 Postgres 的NOTIFY事件迁移到 Supabase 后发现收不到事件推送了。原因是 Supabase 的 Realtime 功能需要在表上启用 replication而这是平台层面的配置不是单纯的数据库触发器。启用方式可以在 Dashboard 的 Database - Replication 里勾选表也可以通过 SQLalter publication supabase_realtime add table public.orders;排查看起来像“功能失效”实际上是平台特性没开启。迁移后务必检查所有依赖实时订阅的表把这层配置补上。4.5 大小写、引号和保留字的坑前面提到了 PostgreSQL 会强制把不带引号的标识符转成小写。如果原 MySQL 表名是OrderDetail迁移的时候建表 SQL 里表名被 mysqldump 转成了小写那没问题但如果你是从本地 PG 导出而原来的 PG 表名就带着大写字母比如OrderDetail那么导出文件里每个表名都是带引号的导入后你访问它也需要带上引号。Supabase 的 PostgREST API 路径和表名是对应的遇到带引号、带大写字母的表名API 路由会非常别扭需要 URL 编码拼接。最好的处理方式是在迁移时统一重命名表改成全小写加下划线。这个操作放在迁移前做比迁移后改容易太多。还有一类是保留字问题。比如你的表里有个字段叫user或者order这在 PostgreSQL 里都是保留字或者 JetBrains 里会给你标红的特殊关键字。建表时因为带了引号可以建但你写查询语句、写 API 过滤器时忘掉引号就会报语法错误。迁移时尽量顺手把这些字段名也改掉比如user改成user_nameorder改成order_info。4.6 时间类型与布尔类型的兼容问题从 MySQL 迁过来这两个类型最常见MySQL 的datetime不含时区信息PostgreSQL 的timestamptz是带时区的。数据导过来之后因为时区默认是 UTC如果你只导数据不处理类型查询结果可能和原库差 8 个小时。建议迁移前就把类型定成timestamptz导入后统一用set timezone或者应用层控制时区展示。MySQL 的tinyint(1)作为布尔值的用法在 PG 里最好转成真正的boolean。如果直接导入PG 会把tinyint识别成smallint字段值只能是 0 或 1查询时where is_active true这种写法就会报错。还是那句话建表前照第 1.1 节的映射表转一遍类型后面省心。5. 迁移完成后的收尾别把旧库立刻删掉数据导完、接口跑通、行数核对一致这个阶段很多人会直接关掉本地库或者把本地服务器释放了。我建议再等等至少留一周观察期。这期间有几个收尾动作值得做。5.1 生成新环境下的类型定义和接口文档Supabase 会根据数据库结构自动生成 REST API也可以导出 OpenAPI 规范。前端可以基于这份规范生成 TypeScript 类型整套接口的联调效率提升明显。你可以在 Supabase Dashboard 的 API 文档页面直接看到每个表对应的 CRUD 接口和示例不用自己写文档。5.2 定时把数据从 Supabase 拉回本地做冷备Supabase 自带自动备份但免费版的备份保留周期有限而且恢复粒度不如自己掌控来得踏实。我习惯设置一个定时脚本每天凌晨把关键表的数据用pg_dump拉回本地存储。这样就算云上出问题手里也有一份随时能恢复的本地快照。pg_dump postgresql://postgres:...?...sslmoderequire \ --data-only \ --no-owner \ --no-privileges \ -t users \ -t orders \ -f /backup/supabase_daily_$(date %F).sql5.3 重新检查所有自定义函数和存储过程的权限点本地 PG 的函数默认以调用者权限执行而 Supabase 平台对安全性的要求更严格。如果你的函数里访问了auth.uid()之类的上下文函数迁移后要么改用 security definer要么确保调用者有足够权限。这个属于业务代码层面的适配不算纯数据库迁移问题但迁移后不检查迟早会在某个页面上炸出来。关于权限我再补一个细节Supabase 里postgres角色不是万能的它不能给supabase_admin开权限也不能操作某些系统 schema。很多本地习惯的超级用户操作在云上是做不了的。功能上能不用就不用尽量走官方提供的 Dashboard 或 SQL 接口完成操作少折腾底层权限。最后说个我自己的习惯。每次做完这种迁移我都会顺手把旧库里长期不用的无主表、断掉的序列、废弃的索引清理一遍。本地库跑了两三年里面总有些项目早期留下的试验品平时用不上还占着维护成本迁移到云上之后这些“烂账”全都得带走。趁着迁移这个契机做一次架构上的瘦身比单纯完成搬迁更有价值。数据迁移本身没什么黑魔法无非是把家底清点清楚然后用合适工具按正确顺序搬运。希望这篇能让你少踩几个坑。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询