尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

PostgreSQL迁移Supabase完整指南:从pg_dump导出到数据校验

PostgreSQL迁移Supabase完整指南:从pg_dump导出到数据校验 年初帮团队把一个本地项目的 PostgreSQL 库迁到了 Supabase整个过程比想象中曲折。表面上就是“导出 SQL、导入 SQL”两条命令的事实际跑起来才发现本地环境、数据形态、云端的权限体系每一层都藏着坑。这篇就把我整理的完整流程和踩过的具体问题写下来给准备做同类迁移的人一个参考。Supabase 作为迁移目标有个天然优势它底层就是 PostgreSQL所以本地基于 PG 的存量系统迁过去之后 SQL 语义、索引结构、外键关系都能保留不需要像迁到 MySQL 那样做一轮方言改写。但“能兼容”不代表“直接能用”——版本差异、扩展缺失、行级安全策略、序列状态任何一个细节没照顾到导入过程就会中断或者数据导进去了应用一跑又出问题。这篇博文适合谁看准备把本地数据库迁到云平台、尤其是迁到 Supabase 的开发者已经在迁移中遇到报错、想排查的也可以直接翻第 4 部分的问题清单。我会把从环境盘点、导出、预处理、导入到校验的完整链路讲透并把我在实操中验证过的命令、参数、排查方法一并给出。1. 动手之前先搞清楚你迁移的是什么很多教程一上来就教你敲pg_dump这是最大的误导。迁移不是“把数据倒过去”而是“让一套系统在另一个运行环境里复现”。如果你不清楚本地库和 Supabase 之间的差异贸然导出只会得到一个看起来成功、实际残缺的结果。1.1 为什么选 Supabase 作为迁移目标我选择 Supabase 不只是因为它提供 Postgres 数据库而是它把 Postgres 托管、认证、实时订阅、对象存储和 REST API 打包在了一起。对中小型项目来说这意味着迁完数据库后后端接口层可以直接用 PostgREST 暴露认证模块用它的 Auth 服务文件上传用 Storage整个基础设施的维护成本会明显下降。从迁移角度说最核心的利好是Supabase 数据库实例完全兼容 PostgreSQL 协议和 SQL 语法。本地用pg_dump生成的是纯 SQL 文本本身就是为跨实例恢复设计的所以理论上不需要像跨数据库种类迁移那样做数据类型映射——你不需要把text改成varchar也不需要重写自增主键的语法这省掉了最繁重的一步。但这只是“理论上”。实际执行时版本差异、扩展安装情况、默认权限策略都会影响导入结果。我在后面几节会逐个拆开讲。1.2 迁移前必须摸清的两个门槛第一个门槛是 PostgreSQL 版本差异。我本地的开发库是 PG 12Supabase 当前托管的版本是 15/17 系具体取决于创建项目时的配置版本跨度大时会遇到两类问题一是旧版本导出文件中可能使用了新版本已调整的语法行为但这种情况比较少见更多见的是反向问题——本地用了很老的pg_dump导出的 SQL 在云端解析时碰到不兼容的语法。我的建议是尽量使用较高版本的客户端工具做导出生成的 SQL 文本兼容性更好。第二个门槛是权限模型。很多人没意识到Supabase 的数据库实例并不是一个“裸 Postgres”它内置了多套角色体系比如postgres超级管理角色、authenticated登录用户、anon匿名用户。新版 Supabase 默认开启了“保护 public schema”选项非超级用户在公共 schema 下建表会被拒绝导出的 SQL 里如果包含CREATE TABLE直接执行就可能报permission denied for schema public。这点我在第 4.5 节会详细展示解决方式。1.3 方案选型三种常用迁移路径的比较迁移方案不是只有pg_dump一条路。根据你要迁的内容和项目形态可以从三条路线里选方案迁移内容速度适合场景pg_dump 导出 SQL 后导入表结构 数据 函数 触发器 序列中慢全量迁移推荐默认选择COPY/CSV 导出再批量导入只迁移表数据结构需另外处理快数据量大且表结构简单Supabase CLI / Migration API结构 数据可脚本化中需要自动化、CI/CD 集成的团队我这次用的是第一种方案因为项目里有十几个表、几十个外键、若干触发器和函数纯 CSV 无法覆盖这些对象。如果你的项目只是几张简单的表用 CSV 走 Supabase 的Table Editor - Import data反而更快。第二个方案里的 COPY 方式在遇到超大表时优势明显。COPY是 Postgres 原生的批量导入机制比逐条执行INSERT快 5-10 倍但导出的 CSV 无法保留外键、序列等约束关系所以它更适合作为“数据搬运”手段而不是“系统迁移”方案。选型的关键就一句话如果你的目标是让整个系统在云端跑起来用 SQL 全量导出如果你只是要把一部分数据归档或共享再用 CSV/API。2. 迁移实操从本地库到 Supabase 的完整流程理论说完进入实操。我按自己验证过的路径把完整流程拆成了四步。每一步的命令我都尽量给出完整版本并解释关键参数的含义方便你对照自己的环境调整。2.1 第一步确认本地库环境与数据盘点动手导出之前先在本地库跑一轮盘点。我习惯用下面几条 SQL 把“家底”摸清楚-- 查看当前数据库版本 SELECT version(); -- 查看所有表及估算行数 SELECT tablename, n_live_tup FROM pg_stat_user_tables ORDER BY n_live_tup DESC; -- 查看所有扩展 SELECT extname, extversion FROM pg_extension; -- 查看所有序列 SELECT sequencename FROM pg_sequences;这一步的价值有三个。第一确认本地 PG 版本决定用什么版本的pg_dump客户端第二了解数据量级如果某张表超过百万行就要提前考虑用 COPY 方式而不是纯 INSERT第三记录扩展列表因为 Supabase 云端默认启用的扩展和本地不一定一致常见的uuid-ossp、pgcrypto在云平台可能已经内置也可能没启用后续导入前要先确认。我在本地库盘点时发现项目里大量使用了gen_random_uuid()作为主键默认值这个函数属于pgcrypto扩展而在导出 SQL 里CREATE EXTENSION语句可能执行成功也可能因为扩展已存在而报错——两种结果都需要你在导入前确认。2.2 第二步用 pg_dump 导出可移植的 SQL确认环境之后执行导出。我最常使用的命令是pg_dump -h 127.0.0.1 -U postgres -d mydb \ --formatplain \ --no-owner \ --no-privileges \ -f mydb_backup.sql参数解释一下--formatplain生成纯 SQL 文本方便检查、编辑、分段执行。虽然custom格式体积更小、支持并行恢复但它是二进制格式没办法手动改里面的内容对排查问题很不友好。--no-owner去掉对象属主信息。本地库的表属主大多是本地用户云端没有这个角色带着属主信息导入会直接报role does not exist这个参数属于必选。--no-privileges不导出权限授权语句理由同上本地角色到云端不存在权限语句反而会造成混乱。如果你只需要迁移publicschema 下的对象可以加--schemapublic缩小范围。如果数据库里有大量历史记录你可能还需要--data-only配合--schema-only分两次导出这样先导结构后导数据排错更清晰。2.3 第三步预处理导出文件导出的 SQL 文件不能直接拿去云端跑至少要做一轮“安全检查”。我用编辑器打开 SQL 文件重点排查几类内容。第一类危险的DROP语句。本地库如果执行过重建操作导出文件开头很可能包含DROP TABLE IF EXISTS public.users; DROP TABLE IF EXISTS public.orders;这在本地执行没问题但拿到云端执行时如果云端恰好存在同名表比如你在 Supabase 控制台建过测试表就会被删掉。我不是说所有 DROP 都要删而是建议你逐条确认尤其在已经创建了 Supabase 项目、里面有一定数据的情况下宁可手动删掉文件中所有DROP SCHEMA public、DROP TABLE开头的语句也不要盲审漏掉。第二类扩展创建语句。SQL 文件里一般会有CREATE EXTENSION IF NOT EXISTS pgcrypto WITH SCHEMA public;Supabase 对于部分扩展是允许普通用户创建的但也有的扩展需要超级权限。遇到CREATE EXTENSION报错时可以先登录 Supabase Dashboard到Database - Extensions页面手动启用对应扩展再重新执行导入。第三类schema 与注释语句。部分导出文件会包含COMMENT ON ...或者REVOKE ALL ON SCHEMA public FROM PUBLIC这类安全策略调整语句非必要内容我建议直接删掉减少干扰。预处理的原则是让 SQL 文件只保留“建对象”和“插数据”两件事其余一律不信赖默认值。2.4 第四步导入 Supabase 并观察实时日志预处理完开始导入。如果你直接在 Supabase Dashboard 的 SQL Editor 里粘贴整个文件大概率会遇到超时因为浏览器请求有时间限制而大事务导入往往需要几分钟。我推荐用本地的psql客户端连接云端数据库执行psql postgresql://postgres:你的密码db.你的项目编号.supabase.co:5432/postgres \ -f mydb_clean.sqlSupabase 连接串在 Dashboard 的Project Settings - Database里能看到里面包含用户名、密码、主机名和端口。使用psql有几个好处能实时看到每一条报错信息、可以配合-v ON_ERROR_STOP1让脚本在遇到第一个错误时立即停止、终端日志能完整保存下来供排查。这个参数的用法是psql 连接串 -v ON_ERROR_STOP1 -f mydb_clean.sql如果表数量多、数据量大我会把导入拆成“结构导入”和“数据导入”两步先用--schema-only导出的 SQL 建好表、函数、索引再用数据文件灌数据。这样一旦数据导入失败表结构已经就位定位问题更快也不用重复建表。导入过程中保持终端开着看到ERROR先停下来修复不要等到全部跑完再回看日志。3. 导入后的数据校验别让脏数据蒙混过关导入命令执行完毕只代表“程序没有报错”不代表“数据正确”。这一步是很多人会跳过的也是最容易出事故的环节。我见过导入成功但行数对不上的案例也见过序列没迁移导致新增数据立刻主键冲突的案例所以校验必须做而且要有步骤地做。3.1 行数与计数校验第一步逐表比对行数。在本地库执行SELECT users AS tbl, count(*) FROM users UNION ALL SELECT orders AS tbl, count(*) FROM orders UNION ALL SELECT products AS tbl, count(*) FROM products;然后在 Supabase 的 SQL Editor 里执行同样语句逐表对比数值。这一步能发现最明显的问题漏导、重复导入、事务中途回滚导致的部分数据缺失。如果某个表的数据量有几十万行用count(*)本身就比较耗时可以用前文提到的pg_stat_user_tables.n_live_tup做粗略对比但要意识到这是一个估算值精确校验还是得跑 count。我在实践中发现两者结合最有效先用估算值快速扫一遍所有表再对差异可疑的表跑精确 count。3.2 业务逻辑抽样校验行数一致不代表字段内容正确。日期格式、空字符串与 NULL、布尔值存储方式不同客户端或不同版本之间可能有微妙差异。我建议挑三到五个业务核心表写几条抽样 SQL 做比对。例如订单表的核心状态字段SELECT order_id, status, created_at, total_amount FROM orders WHERE order_id IN (关键订单1, 关键订单2, 关键订单3);在本地库和云端分别执行逐字段对比。更高效的方式是直接对比整个结果集-- 本地执行后导出结果文件 \copy (SELECT order_id, status, created_at, total_amount FROM orders ORDER BY order_id) to orders_local.csv with csv header -- 云端执行后导出结果文件 \copy (SELECT order_id, status, created_at, total_amount FROM orders ORDER BY order_id) to orders_cloud.csv with csv header然后用diff命令对比两个 CSV差异一目了然。这一步虽然朴素但能抓住bool存成了字符串、时间字段时区被改写、浮点精度被截断这类隐蔽问题。3.3 序列自增与触发器状态检查校验数字之外还要检查两个容易被忽略的“隐状态”序列和触发器。序列是 Postgres 自增主键的底层机制。如果你的表用了SERIAL或BIGSERIAL类型导入数据后表的行 ID 是固定的但序列的当前位置还停在初始值。此时应用插入新记录主键会从初始值开始与已有记录冲突直接报duplicate key value violates unique constraint。修复方法是把序列同步到表当前最大 IDSELECT setval(public.users_id_seq, (SELECT max(id) FROM public.users));表名和序列名的对应规则一般是“表名_id_seq”如果你的主键列不叫id序列名就按实际结果来先执行\d 表名查看底层序列名称。触发器方面如果在迁移过程中有业务触发器执行过比如自动更新时间戳、写入审计日志可能造成部分行数据不一致。建议在导入前用ALTER TABLE ... DISABLE TRIGGER ALL暂时禁用导入完成后再ENABLE TRIGGER ALL恢复避免触发器在批量导入期间产生无意义的写放大或报错。4. 迁移中的常见问题与排查实录这部分是我最想写的因为实操中踩过的坑几乎都集中在这里。我按发生频率从高到低排列每个问题都给出现象、原因和解决路径你可以直接对照自己的报错信息排查。4.1 行级安全策略导致查询与插入失败Supabase 默认的安全机制里有一个很重要的特性行级安全Row Level SecurityRLS。新建表后如果表上启用了 RLS普通角色只能看到自己有权访问的行如果用authenticated或anon角色操作可能一条数据都查不出来。我在迁移导入数据后用 API 查接口返回结果一直是空数组本地库同样的查询却有数据。排查半天才发现Supabase 项目默认开启 RLS新建的表自动带上了策略限制。解决方法有两个方向。第一在导入前或导入后通过 SQL Editor 以postgres角色执行ALTER TABLE public.users DISABLE ROW LEVEL SECURITY;临时关闭行级安全。如果你的业务完全依赖后端 API 控制权限关闭 RLS 不会影响功能。第二如果希望保留 RLS就需要在导入后为每个表创建合理的 Policy这属于业务权限设计不是一两句话能讲完的。一般情况下迁移初期先关闭 RLS确认系统运行正常再按需开启并编写策略。4.2 外键约束导致导入顺序报错导入过程中最常见的报错莫过于ERROR: insert or update on table orders violates foreign key constraint orders_user_id_fkey原因是pg_dump默认按照表名的字母顺序生成插入语句不会智能地先插父表再插子表。当插入子表时引用的父表记录还没插入外键校验自然失败。解决这个问题我比较推荐的做法是利用 Postgres 自身的外键延迟检查机制。在导入数据之前先执行SET session_replication_role replica;这条命令会临时禁用触发器和外键约束让导入过程不检查外键关系导入完成后执行SET session_replication_role origin;恢复约束检查。注意这个命令只对当前会话有效所以用psql导入时要保证这三条命令在同一个会话里执行。我的做法是把它们直接拼进导入 SQL 文件的开头和结尾或者在psql交互会话里先执行 SET 再执行\i导入。用这个方案之后还需要跑一次完整性校验确认没有真正破坏外键关系。可以执行SELECT count(*) FROM orders o LEFT JOIN users u ON u.id o.user_id WHERE u.id IS NULL;如果结果不为 0说明有孤儿数据需要单独修复。4.3 主键冲突与序列不同步这个前面已经提到这里展开讲。迁移后向用户表插入新用户时报ERROR: duplicate key value violates unique constraint users_pkey原因就是序列没有同步。我在第 3.3 节写过单表修复方法实际项目中表很多我建议直接写一个批量修复脚本把库里所有自增序列统一同步到对应表的最大 IDDO $$ DECLARE tbl_name text; seq_name text; max_id bigint; BEGIN FOR tbl_name, seq_name IN SELECT c.relname, s.relname FROM pg_class s JOIN pg_depend d ON d.objid s.oid AND d.refobjsubid 4 JOIN pg_class c ON c.oid d.refobjid WHERE s.relkind S LOOP EXECUTE format(SELECT COALESCE(max(id), 1) FROM %I, tbl_name) INTO max_id; EXECUTE format(SELECT setval(%L, %s), seq_name, max_id); END LOOP; END $$;这个脚本遍历所有序列将序列值同步到对应表主键的最大值。如果你主键列名不是id需要对脚本做一点调整把max(id)换成实际的列名。这个操作做完新插入数据就不会再撞主键了。4.4 大表导入慢如何预估和提速第一次导入时我用 Supabase 的 SQL Editor 直接跑导入脚本其中有一张 80 万行的日志表跑了十几分钟后浏览器报超时整个事务回滚一切归零。后来改成psql导入快了一些但依然需要 20 多分钟。问题出在pg_dump默认生成的是逐条INSERT语句每条语句都有独立的事务开销。对这种场景更快的路径是先用COPY导出纯数据psql -h 127.0.0.1 -U postgres -d mydb \ -c \copy public.logs TO /tmp/logs.csv WITH CSV HEADER然后在云端用psql执行 COPYpsql 云端连接串 \ -c \copy public.logs FROM /tmp/logs.csv WITH CSV HEADER我实测时80 万行通过 COPY 导入大概 3 到 5 分钟比逐条 INSERT 快了差不多一个数量级。需要注意两点如果表有外键COPY 导入时建议同样先临时禁用外键COPY 只导数据如果你还没建表结构需要先执行建表 SQL。另外如果数据文件很大几个 GB建议不要通过psql传本地文件而是把 CSV 上传到 Supabase Storage 再走COPY读取或者直接用 Supabase 的Import Data功能分批导入减少网络中断导致的反复重试。4.5 权限不足新版 Supabase 的 public schema 限制新版 Supabase 项目默认启用了“保护 public schema”策略非超级角色在 public schema 下没有CREATE权限。导入 SQL 时如果文件包含建表语句就会报ERROR: permission denied for schema public这种情况分两种处理方式。如果你全程用postgres角色连接数据库实际上不会遇到这个问题因为postgres是超级用户可以绕过大部分权限限制。如果你用psql连接时指定的是postgres用户仍然报权限错误那就要检查连接串里的密码或角色名是否真的对应超级用户。另一种处理方式是把表建到自定义 schema 下。比如建到appschemaCREATE SCHEMA IF NOT EXISTS app;然后把搜索路径切到app再执行导入。但这样你的 API 访问路径会变复杂因为 Supabase 的 PostgREST 默认暴露的是 public schema 下的表如果你把表放到非 public schema需要额外配置 schema 暴露。除非有明确理由我更建议直接用postgres角色导入省去后续配置。5. 迁移后的运维与后续建议数据校验通过、应用跑起来迁移还不算结束。之后的一段时间里你需要对云端数据库做持续的观察和备份管理同时处理好和本地库的关系避免两边写数据产生冲突。5.1 备份策略与迁移存档Supabase 自带每日备份Backups 服务付费版还支持 PITR时间点恢复但我不建议只依赖云平台的备份策略。迁移完成后我做的第一件事就是本地留存一份完整的pg_dump产物压缩后放到对象存储和 Git 仓库如果是保密数据注意加密。这样即使云平台侧出现账户异常、项目误删、配置错乱你手头永远有一份可恢复的基线数据。另外我会把这次迁移用的预处理后的 SQL 文件也留存下来命名为类似migrate_2025_01_supabase_clean.sql放在项目的迁移目录里。后续如果要在另一个环境重建一套同样结构的库这份文件可以直接复用不用重新处理一轮。5.2 本地库与云端库的并存细节迁移结束后本地库要不要继续保留我的建议是可以保留但角色要重新定位。本地库只做历史查询、离线分析、开发调试线上写入全部切换到 Supabase。如果你继续在本地写数据又同时让应用连接云端库两条数据链路会出现分叉后期合并成本极高。如果你确实需要一个本地开发环境可以考虑用 Supabase CLI 在本地跑一套 Supabase 容器从云端库拉取结构或部分数据下来作为开发沙箱。这样本地写的数据不会影响云端生产库但开发时又能用同一套技术栈和权限模型。我自己后来就是这么做的避免了很多“本地正常、云端异常”的烦恼。写在最后的实操体会这次迁移让我印象最深的一点是数据库云迁移真正难的不是数据搬运本身而是搬完之后系统能否无缝地跑起来。RLS、序列、外键顺序、权限模型这些问题在导出文件里一个都看不见但每一个都能让线上应用立刻出故障。如果让我给一个最重要的建议那就是正式迁移之前一定先在 Supabase 里新建一个临时项目或者至少临时数据库完整跑一遍导入流程。这个“演练环境”能帮你把所有权限、扩展、校验的问题提前暴露出来等你有了处理过一轮的清洗后 SQL 文件再对正式库执行的时候整个过程就会顺畅得多。别嫌多花的一两个小时——对比线上事故的恢复时间这点成本实在不算什么。
返回列表