ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

从MySQL到PostgreSQL:大厂数据库迁移实战与避坑指南

从MySQL到PostgreSQL:大厂数据库迁移实战与避坑指南 最近这半年数据库选型又成了团队里讨论最多的话题。以前大家聊到关系型数据库第一反应就是 MySQL官方文档顺手、中间件成熟、DBA 也好招。但从去年开始越来越多的新项目、甚至老项目的重构方案里都直接把 PostgreSQL 定了下来。PostgreSQL 这个词在社区和招聘市场里的热度肉眼可见地在涨很多从 MySQL 过来的同学第一反应是这玩意儿真有那么好吗换库的代价可不小大厂到底图什么这篇文章就围绕“为何大厂纷纷转向 PostgreSQL”这个话题把从 MySQL 到 PG 的进阶之路完整拆一遍。先聊清楚背后的驱动力再对比两者核心差异然后给出实际迁移操作的路线图和避坑指南。如果你正在评估新项目选型或者手上刚好有 MySQL 迁移 PG 的活儿这篇文章就是按我实际经验来的可以直接当参考用。1. 为什么这几年大厂都在换数据库1.1 从“默认 MySQL”到“先看场景”早期互联网应用选 MySQL很大程度上是历史惯性。LAMP 架构太普及了MySQL 作为其中一环教程多、社区大、云厂商支持好很多公司从第一天起就把它当默认数据库。直到现在绝大多数中小型 Web 应用用 MySQL 依然合理MySQL 8.0 以后的能力也一直在提升。但“默认”不等于“最优”。过去五六年业务场景明显变复杂了数据结构不再是一张张整齐的二维表JSON 半结构化数据越来越多分析型查询和 OLTP 混在同一个库里的需求越来越普遍。此时 PostgreSQL 的几个硬实力就开始显山露水它天生支持 JSONB、数组、范围类型自带窗口函数和 CTE还有 PostGIS 这种让它直接变成地理空间数据库的扩展。对于一个技术团队来说一个库能同时承担传统事务、部分分析、半结构化和空间数据的角色诱惑力是很大的。而且 PostgreSQL 并不是一夜爆红的。它在学术圈和企业级应用里默默积累了几十年事务机制、优化器、扩展性设计都是按教科书级别做的。很多人第一次接触 PG 是在读书或开源项目里等到真正生产环境需要这些能力时自然就想起它。1.2 商业驱动的三个现实问题抛开技术情怀大厂换数据库最终都是被商业问题逼的。第一个问题是复杂 SQL 的执行效率。MySQL 的优化器这些年进步不小但在复杂关联查询、子查询、CTE 递归这类场景下跟 PostgreSQL 的优化器比还是有差距。PostgreSQL 里有更准确的统计信息机制、更细粒度的代价估算EXPLAIN 的输出也直观得多。简单说同样一条多表关联的报表 SQL在 PG 里经常能跑出比 MySQL 好得多的计划。第二个问题是数据治理和 SQL 标准合规。很多公司的敏感数据要审计、要统一口径、要支持 BI 工具直接对接。PostgreSQL 对 SQL 标准的支持比 MySQL 严格得多窗口函数、WITH RECURSIVE、LATERAL JOIN、FILTER 子句这些都直接用不用再靠奇技淫巧绕过语法限制。做数据平台的同学应该深有感触从 MySQL 导数据到数仓SQL 方言差异经常要单独写一层转换逻辑PG 就能省很多事。第三个问题是扩展生态。这里不单指 PostGIS 和 pgvector 这种明星扩展还包括很多小功能你可以自定义数据类型、自定义聚合函数、甚至自己写一个索引访问方法。PG 允许你在数据库内部“生长”出 MySQL 很难拥有的能力。这些年 AI 应用火起来pgvector 让 PG 直接成了向量检索的轻量级方案很多团队根本不需要再单独搭一套向量数据库。这种“一个库解决多个问题”的能力正是技术团队降本增效最爱看的东西。1.3 适合谁迁移不适合谁迁移我见过不少脑子一热就提迁移方案的结果迁移完发现团队根本驾驭不了。数据库选型不是换品牌是换一套能力模型。楼主如果负责评估下面这两类判断标准很有用。适合迁移的团队业务里查询逻辑复杂、需要窗口函数和 CTE 做报表、需要地理空间或全文检索、需要容器化部署和管理成本可控、团队愿意统一一套数据库技术栈或者是新项目、没有任何存量包袱那直接用 PG 是很顺理成章的事。不适合硬迁的团队业务极其简单、就是几张表的增删改查MySQL 完全够用公司 DBA 团队的运维体系全是围绕 MySQL 建的备份、监控、中间件都成型了此时要替换意味着运维体系全盘重做团队没有一个人写过 PG出了问题连日志都不知道去哪看那再好的技术也会被落地问题拖死。换句话说大厂的转向不是“无脑换”而是它们原本的复杂需求已经超出了 MySQL 的舒适区。技术选型这事匹配才是第一位。2. MySQL 和 PostgreSQL 的核心差异到底在哪里2.1 SQL 标准与语法差异从 MySQL 迁移到 PG最先感受得到的不是性能而是 SQL 写法要改。我第一次从 MySQL 迁过去的时候被 GROUP BY 的报错整得非常狼狈。MySQL 在关闭 ONLY_FULL_GROUP_BY 时允许 SELECT 后面出现非聚合列PG 则始终严格拒绝。再比如 INSERT ... ON DUPLICATE KEY UPDATEMySQL 的经典写法在 PG 里不存在PG 的替代语法是 INSERT ... ON CONFLICT (唯一键) DO UPDATE SET。功能层面对应但写法完全不同改起来真不是单纯换关键字那么简单。还有字符串函数和聚合函数MySQL 的 GROUP_CONCAT 对应 PG 的 STRING_AGGMySQL 的 IFNULL 对应 PG 的 COALESCEMySQL 的 DATE_FORMAT 对应 PG 的 TO_CHAR。如果不提前把这些差异排查一遍迁移后第一轮灰度测试就会输在语法兼容上。下面是我整理的一张高频语法对照表迁移前建议直接拿去当检查清单功能描述MySQL 写法PostgreSQL 写法冲突时更新INSERT ... ON DUPLICATE KEY UPDATEINSERT ... ON CONFLICT(...) DO UPDATE SET拼接多行GROUP_CONCAT(col SEPARATOR ,)STRING_AGG(col, ,)空值替代IFNULL(expr, default)COALESCE(expr, default)限制返回行数LIMIT n OFFSET m同样支持 LIMIT/OFFSET字符串拼接CONCAT(a, b)a || b 或 CONCAT(a, b)日期格式化DATE_FORMAT(d, %Y-%m-%d)TO_CHAR(d, YYYY-MM-DD)正则匹配REGEXP pattern~ pattern 或 ~* 忽略大小写布尔字段查询WHERE is_active 1WHERE is_active true提个细节PG 里未加引号的标识符会自动折叠成小写所以表名要是在 MySQL 里用了大写驼峰迁移之前最好先统一成小写否则查询时一会儿成功一会儿报错排查起来非常折磨。2.2 数据类型与存储机制对比第二个绕不开的是数据类型映射。很多看起来“差不多”的类型实际语义差得很远。MySQL 里最常用的 TINYINT(1) 通常被当作布尔用而 PG 里有真正的 BOOLEAN 类型MySQL 的 DATETIME 不带时区PG 推荐用 TIMESTAMPTZ 来避免时区换算出幺蛾子。AUTO_INCREMENT 也得重点说。MySQL 的自增主键是表属性而 PG 里常见的是序列加默认值。旧写法用 SERIAL 伪类型PG 10 之后更推荐 GENERATED AS IDENTITY语义上更标准。很多人没注意的一个坑是MySQL 里执行 INSERT 失败后 AUTO_INCREMENT 的值也会消耗掉PG 的序列同样有这个问题但 PG 里序列的行为更可控你可以用 SETVAL 重新调整序列值这在做数据修复时非常好用。还有个经常被忽略的差异在字符串类型。MySQL 的 VARCHAR(50) 表示最多 50 个字符PG 里同样成立。但 MySQL 旧版里的 TEXT 类型不能有默认值PG 的 TEXT 和 VARCHAR 基本通用没有固定长度限制反而更省心。另外 PG 里 CHAR(n) 在存储时会填充空格查出来还要你去 TRIM实际开发中大家基本只用 VARCHAR 或 TEXT。JSON 类型值得单独拎出来讲。MySQL 8.0 的 JSON 是二进制存储已经很好用了但 PG 的 JSONB 更激进——它把 JSON 解析成二进制格式支持 GIN 索引可以在 JSON 内部的 key 上建索引。这在很多“半结构化业务”场景里是降维打击。你不需要专门上一个文档数据库直接在关系库里就能把 JSON 查得很顺手。2.3 索引与查询优化器的差异索引这块MySQL 的主力是 BTreePG 的主力也是 B-Tree但 PG 的扩展性要强很多。说说 PG 的 GIN 索引。它专为数组、JSONB 和全文检索设计比如你有一列 tags text[]想查包含“postgres”的标签记录建一个 GIN 索引后查询速度可以提升一到两个数量级。还有 BRIN 索引适合海量有序数据比如日志表它只存储块范围的信息索引体积非常小对超大表的查询性能提升也很明显。除此之外PG 还支持 Partial Index、Expression Index。LOWER(email) 上建索引、只对 statusactive 的行建索引这些都是 MySQL 语法层面想都不敢想的操作。对大厂来说这些索引特性意味着能直接解决慢查询问题而不需要额外引入搜索引擎或数仓。在查询优化器方面PG 的 EXPLAIN ANALYZE 输出真的非常适合逐行阅读它会告诉你每个节点的代价、循环次数、实际行数你基本能顺着输出定位到瓶颈是索引缺失、统计信息过期还是关联顺序错乱。MySQL EXPLAIN 虽然也够用但信息量没有这么细致。配合 pg_stat_statements 插件做慢查询分析基本上排查 SQL 问题就是“现代战争”了。3. 从 MySQL 到 PostgreSQL 的迁移实操路线3.1 迁移前评估与版本选择迁移不是下载个安装包把数据倒过去就完事。我见过太多失败案例都是因为前期评估不到位上线后才发现 SQL 不兼容、字符集乱码、时区错乱最后只能回滚搞得团队半个月都在填坑。你至少要做四件事盘点存量对象所有表、视图、存储过程、触发器、事件、定时任务列一个完整清单。检查 SQL 兼容性把应用里的 SQL 全部收集出来跑一遍语法扫描。常用工具比如 pgloader 有 dry-run 模式或者用 EXPLAIN 在 PG 里挨个跑看有没有语法报错。确认字符集与排序规则MySQL 里常见的 utf8mb4 在 PG 里就是 UTF8但要注意排序规则可能不同尤其是中文排序建议迁移后统一用 en_US.UTF-8 或 C.UTF-8避免排列顺序和应用预期不一致。明确版本策略PostgreSQL 版本迭代很快14、15、16、17 各有提升但生产环境我一般建议选当前稳定版的前一两个版本比如 PG 16 或 15。太新的版本社区反馈数据少踩坑了都没地方问。版本选择这个话题云托管和自己部署是两个思路。云托管比如 RDS for PostgreSQL省心升级和备份都有人管自建则要自己处理流复制、突发故障、磁盘空间等问题。从 MySQL 过来的团队我建议先走云托管等团队熟悉了再考虑自建。除非公司已经有很成熟的 PG 运维体系否则刚开始就自建 PG 集群运维压力会非常大。3.2 全量迁移pgloader 与 mysqldump 双方案全量迁移一般两条路一条是工具直连转换一条是文件转储导入。我平时最推荐的是 pgloader因为是专门为这类迁移设计的能自动处理类型映射和常用语法转换。先看 pgloader 怎么用。假设 MySQL 在 127.0.0.1:3306数据库名 app_db目标 PG 在 192.168.1.10:5432数据库名 app_target在服务器上创建一个 load.load 文件LOAD DATABASE FROM mysql://app_user:app_pass127.0.0.1:3306/app_db INTO postgresql://pg_user:pg_pass192.168.1.10:5432/app_target WITH include drop, create tables, create indexes, reset sequences, data only CAST type datetime to timestamptz drop default drop not null using zero-dates-to-null;然后执行pgloader load.load这个过程会自动建表、迁移数据、创建索引并在迁移结束后输出一份详细报告告诉你哪些表成功、哪些字段做了类型转换、有多少行报错。我建议你第一次执行时先不要带 “data only”确保表结构生成正确后再导数据分步走更容易定位问题。如果你的网络环境访问不到源库或者 DBA 不允许直连那可以走文件导出路线。MySQL 侧使用 mysqldump 导出兼容格式mysqldump -h 127.0.0.1 -u app_user -p --compatiblepostgresql \ --default-character-setutf8mb4 --no-autocommit app_db app_db.sql不过说实话mysqldump 的 --compatiblepostgresql 只能处理一小部分语法类型转换还是得人工处理。导出的文件拿到 PG 里执行前你需要先手工改掉自增列、布尔字段、JSON 类型这些差异。所以我更推荐用小表数据用工具直迁、大表提前分批导的策略结构用 pgloader 建数据用 mysqldump 导出成 CSV再用 PG 的 COPY 命令批量导入。COPY 在大数据量下的效率非常猛比一行一行 INSERT 快一个数量级。下面是我常用的手动类型映射参考表MySQL 类型PostgreSQL 类型迁移备注TINYINT(1)BOOLEAN0 转 false1 转 trueTINYINT / SMALLINTSMALLINT仅剩少数字节长度不影响INTINTEGER可直接对应BIGINTBIGINT可直接对应VARCHAR(n)VARCHAR(n)长度一致时可直接对应TEXT / LONGTEXTTEXT无长度限制DATETIMETIMESTAMP / TIMESTAMPTZ推荐 TIMESTAMPTZ注意时区TIMESTAMPTIMESTAMPTZ别忽略时区JSONJSONB推荐使用 JSONB方便索引ENUMVARCHAR CHECKPG 也有 ENUM但尽量少用BLOBBYTEA二进制类型对应给个实战例子吧MySQL 里的建表语句是CREATE TABLE users ( id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(50) NOT NULL, is_active TINYINT(1) DEFAULT 1, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, profile JSON );迁移到 PG 里应该写成CREATE TABLE users ( id INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, name VARCHAR(50) NOT NULL, is_active BOOLEAN DEFAULT TRUE, created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, profile JSONB );这里有个心理预期要提前建立迁移不是一次性的“数据搬家”而是重新整理数据结构的机会。很多团队在迁移时顺手把不规范的类型全部纠正过来效果远好于机械复制。3.3 增量同步与并行切换如果你不能接受停机迁移那就需要增量同步。PG 生态里最正统的方案是逻辑复制源端 MySQL 开启 binlog中间用 Debezium 或者 Flink CDC 把 binlog 事件解析出来再通过 PG 的逻辑复制或直接写入目标库实现两个库的准实时同步。这里要注意PG 的逻辑复制在 PG 10 后才正式成熟发布端和订阅端版本最好都比较新。操作大致是在 MySQL 侧开启 binlog确保 binlog_formatROW。部署一个 Debezium Connector把 MySQL 数据变更推送到 Kafka。下游连到目标 PG写入变更。切流之前做全量一致性校验确认数据一致后再把应用连接从 MySQL 切换到 PG。我实际操作中增量同步最难的不是同步本身而是数据一致性校验。两边库的哈希值没法直接比对尤其是 JSON、浮点、时区字段很容易出现“看起来一样实际二进制不同”的情况。建议校验工具自己做每张表按主键分批读取计算行数、聚合 checksum不一致的地方再细查差异行。别怕麻烦这个环节越严格切换后的问题越少。切换策略上我强烈建议做“灰度切换”。先切只读流量让一部分查询走 PG跑一周确认没有性能问题再切写流量。有些团队上来就全量切出了问题直接原地爆炸这其实是项目管理问题不是技术问题。4. 迁移后容易踩的 8 个坑与排查技巧4.1 连接与部署类问题第一个坑是连接数。MySQL 的连接数因为有中间层通常不是问题PG 默认的 max_connections 是 100生产环境必须调大但调大之后内存占用也会变高。每个连接在 PG 里都可能分配 work_mem你要是把连接数拉到 1000内存直接爆掉。正确做法是应用层用连接池比如 PgBouncer后端保持稳定数量的长连接。第二个坑是 pg_hba.conf。PG 默认只允许本地连接你从应用服务器远程连不上时第一个想到的就是去检查 pg_hba.conf 的 host 规则而不是怀疑防火墙。很多新手折腾半天最后只是这条配置没改。下面是常见错误速查表建议收藏错误信息原因排查方向FATAL: password authentication failed密码错误或 pg_hba.conf 认证方式冲突先确认 pg_hba.conf 是否用的 scram-sha-256 / md5could not connect to server: Connection refusedPG 未启动、端口不对或防火墙拦截检查 pg_isready确认端口 5432 是否监听sorry, too many clients already连接数达到 max_connections顺手查一下 pg_stat_activity看活跃连接分布must be superuser to create extension扩展安装权限不足用超级用户执行 CREATE EXTENSIONpermission denied for schema public用户未授权检查用户权限 GRANT USAGE ON SCHEMA ...duplicate key value violates unique constraint序列值落后于表内最大值执行 SELECT setval(..., max(id))value too long for type character varying(n)数据超过 VARCHAR 长度与 MySQL 的宽松截断行为完全不同PG 直接报错第三个坑是序列值滞后。因为迁移数据时用了 COPY表的 id 序列不会自动更新结果应用插入新数据时说主键冲突。这个几乎是每个迁移团队都会踩的坑操作其实很简单对每张表执行一次 setval 同步序列到当前最大值即可SELECT setval(pg_get_serial_sequence(users, id), (SELECT COALESCE(MAX(id), 1) FROM users));4.2 数据行为差异类问题第一个大差异是 NULL 排序。MySQL 里默认升序排序时 NULL 排最前PG 里默认 NULL 排在最后。对于分页报表这个差异很容易导致数据顺序和之前完全不一致。解决办法是按业务需求显式加上 NULLS FIRST / NULLS LAST。第二个大差异是 GROUP BY 的严格模式前面已经说过了。还有一个小兄弟是 ONLY_FULL_GROUP_BY 造成的“看似能跑、跑了结果是错的”这类隐形问题在 MySQL 宽松模式下能过在 PG 里直接报错。从另一个角度想这种严格性反而是好事——它强迫你把 SQL 写规范。第三个是时区。MySQL 的 DATETIME 不带时区迁移到 PG 如果用 TIMESTAMPTZ显示出来会跟着服务器时区走应用层拿到的时间数字可能跟以前不一样。排查法很简单统一用 UTC 存数据应用层统一转本地时间。如果不想动太多代码迁移初期也可以用 TIMESTAMP 类型避开时区差异但长期看还是统一 TIMESTAMPTZ 更规范。第四个坑是 Boolean 和整数混用。MySQL 里你可以写 WHERE is_active1PG 里这写会报错operator does not exist: boolean integer。看起来是小事但如果业务代码里到处是这种写法迁移工作量大到你怀疑人生。最好写个简单的 SQL 扫描脚本把应用中涉及布尔字段的查询全部揪出来。4.3 SQL 写法兼容类问题除了前面语法对照表里提到的还有两个高频问题值得注意。第一个是 REGEXP 和正则表达式风格。MySQL 的 REGEXP 用的是 POSIX 风格简化方言PG 的 ~ 操作符用的是 POSIX 标准正则功能更强但表达式写法可能不完全兼容。比如 MySQL 里简单的 LIKE %abc% 迁移后可以用 LIKE 或 POSITION但复杂正则建议逐条重写并做好离线回归测试。第二个是存储过程的差异。MySQL 的存储过程和函数用 BEGIN...END 包起来变量语法是 DECLARE SETPG 的 PL/pgSQL 则用 $$ ... $$ 结构变量用 : 赋值。迁移时不要想着自动转换几乎不可能手写重写反而最省时间。除非你的项目里存储过程特别多那迁移成本会很高评估时需要单独列出来。4.4 出问题时怎么排查比较高效迁移上线前两周大概率是问题高发期。我的排查顺序是先看 pg_stat_activity查有没有锁等待或长时间运行的事务。打开慢查询日志或者用 pg_stat_statements 按执行时间排序把 TOP 20 SQL 揪出来。对每条慢 SQL 执行 EXPLAIN (ANALYZE, BUFFERS)看预计行数和实际行数偏差大不大。如果偏差大说明统计信息没过新要跑一次 ANALYZE。检查 VACUUM 是不是堵住了。PG 的 MVCC 依赖 VACUUM 清理死行如果 autovacuum 没有正常运行表会无限膨胀查询性能会肉眼可见地下降。这里补一句关于 VACUUM 的很多人从 MySQL 过来容易忽略。MySQL 的 InnoDB 有后台 purge 线程自动清理PG 也自动但不够及时。对频繁更新的大表要关注表的 bloat 情况。必要时手动执行 VACUUM (ANALYZE) 或 VACUUM FULL后者会锁表只能趁维护窗口做。最后一定要在迁移后跑一周以上的并行对比。两个库同时写入每天对比核心业务数据是否一致应用层流量逐步放大。这个过程虽然拉长了项目周期但能把风险分散到最小。5. 迁移后的优化与长期运维建议5.1 基础参数调优PG 默认配置很保守刚安装完就直接上生产的话性能很难看。和 MySQL 类似调参的核心是内存。下面是我经过多次压测后的起步参数你可以根据自己的机器情况调整# postgresql.conf 关键参数 shared_buffers 4GB # 建议为物理内存的 1/4 effective_cache_size 12GB # 建议为物理内存的 3/4 work_mem 64MB # 排序/哈希操作内存按需调整 maintenance_work_mem 1GB # VACUUM、CREATE INDEX 用 checkpoint_completion_target 0.9 max_connections 300 # 实际按业务定 random_page_cost 1.1 # SSD 环境建议调低要注意的是work_mem 不是越大越好。它是每个查询执行节点都可能分配的内存连接数多了以后总内存消耗会爆炸。压测时观察峰值内存再逐步调。5.2 索引与 Vacuum 的日常功课PG 里索引不是越多越好。MySQL 里冗余索引会影响写入性能PG 也一样。但 PG 的索引类型多要按查询模式选精确匹配走普通 B-TreeJSONB 和数组查询走 GIN日志型大表走 BRIN。索引设计做完之后别忘了定期跑 VACUUM维护统计信息和释放空间。5.3 要不要做读写分离MySQL 场景里读写分离是个常见架构通常靠中间件或主从复制实现。PG 同样支持物理流复制和逻辑复制也有 Patroni 这类高可用方案。但我建议你先别急着搭集群。单机 PG 在绝大多数业务里性能都够先把单机跑稳、参数调对、索引建好比盲目搞一堆副本实际得多。等业务量确实上来了再用 Patroni etcd 做高可用复杂度可控。最后说一点我自己的体会。带团队从 MySQL 迁到 PG最困难的根本不是 SQL 语法差异而是调整认知惯性。很多人在 MySQL 里习惯性用“绕过问题”的方式写 SQL换到 PG 里很多问题可以直接用正规功能解决。PostgreSQL 的能力很强但它不会替你决定怎么做数据模型它只是给了你更多正确做事的可能性。如果你愿意静下心读一遍官方文档再把习惯性思维调整过来这场迁移带来的收益远不止“换了数据库”这么简单。
返回列表