MySQL迁移电科金仓KingbaseES实战:从评估到割接的完整避坑指南
上个月我接手了一个让我印象非常深刻的任务把一套跑了三年多的 MySQL 业务库整体迁到电科金仓KingbaseES。这套系统日常要扛几百个并发库里一共两百多张表最大的一张接近九千万行还养着十几个定时任务和三四个核心存储过程。整个项目从前期评估到最终割接上线前后折腾了三周。坦白说踩坑的数量远超预期而且很多坑在官方文档里根本没写明白全靠现场排查才定位到。这篇文章就是围绕这次 MySQL 到电科金仓的迁移实战把我踩过的坑、验证过的方案、以及最终落地的一套可复制流程完整整理出来。如果你正准备做类似的数据库替换或者已经在迁移路上被各种兼容性问题折磨这篇笔记应该能帮你省下不少时间。先说清楚这篇文章适合谁DBA、后端开发、运维同学以及那些需要给现有系统评估“能不能迁”“怎么迁”的技术决策者。我会尽量把每个决策背后的原因讲清楚不光是给结论更希望你理解为什么这么做。1. 迁移方案选型别急着导数据先搞清楚“迁移”到底意味着什么很多人一听说迁移数据库第一反应就是“把数据导过去不就行了”。这个想法在跨数据库产品时是最大的坑。MySQL 和电科金仓虽然在 SQL 层面有很多相似之处但底层内核、数据类型、约束行为、函数体系、事务语义都有差异。如果只搬数据不搬结构或者只搬结构不处理 SQL 兼容性后面应用接上来的时候会炸得非常难看。1.1 先评估三个问题再定迁移策略我在项目正式动工前先给自己定了三个问题第一现有库里有多少张表、总量多大、哪些是核心表哪些是日志表第二业务代码里有多少处 SQL 直接用到了 MySQL 特性比如REPLACE INTO、ON DUPLICATE KEY UPDATE、GROUP_CONCAT这类语法第三有没有存储过程、触发器、定时事件这类数据库对象。这三个问题直接决定了工作量。如果表量不大而且 SQL 风格偏标准那么迁移成本低甚至可以借助官方迁移工具一把梭。但如果像我这个项目一样业务系统是三年迭代过来的SQL 写法早就放飞自我了那你就得做好“迁移的主体根本不是数据而是 SQL 改造”的心理准备。我把这次迁移分成了几个阶段方案选型、数据盘点、结构迁移、数据迁移、SQL 适配、校验调优、割接切换。整个链路里最耗时的是 SQL 适配最磨人的是数据类型映射最有挫败感的是各种莫名其妙的报错。下面一个一个说。1.2 工具链组合官方工具、DataX、自写脚本各管一段迁移工具这块我实际对比了三条路线金仓官方提供的迁移工具、Apache DataX、还有自己写脚本。每条路线都有它的适用范围不要指望一个工具通吃全场景。官方图形化迁移工具在小数据量、简单对象结构下体验确实不错点几下就能把表结构和数据搬过去。但在我的场景里一旦涉及 JSON 字段、特殊字符集、大表分批抽取、视图和存储过程工具就频繁出幺蛾子而且排错信息非常不直观。DataX 在数据抽取层面很稳定适合当搬运工但表结构还是要单独处理。最后我采用的是“官方工具辅助评估 自写脚本控制全流程 DataX/COPY 做数据搬运”的混合方案。这里有个 principle 值得分享迁移工具负责的是“搬”不负责“兼容”。千万别指望工具帮你把TINYINT(1)自动变成布尔值把ENUM变成带约束的字段把存储过程自动翻译成 PL/pgSQL 风格。这些东西必须自己一层层梳理清楚。我后来甚至觉得官方工具最大的价值不是帮你迁移而是帮你生成一份表结构清单和兼容性报告让你知道有哪些风险点。2. 迁移前最该做的一件事彻底盘点你的数据库不夸张地说迁移能不能顺利在盘点阶段就决定了八成。这个阶段不需要写太多代码但需要你像发病历一样把所有对象、字段、SQL 都翻出来过一遍。我会带着团队做一张“兼容性检查表”一项项打勾。2.1 盘点对象清单表、字段、约束、索引、触发器、任务首先是表清单。我执行了这样一组查询把整个库的“家底”捞出来总表数、总数据量、每张表的行数和体积、存储引擎、字符集和排序规则。重点看有没有 MyISAM 表——如果有得先确认业务是否依赖它的表级锁特性或全文索引因为金仓这边没有 MyISAM 的概念需要全部转成普通事务表。接着是字段层面的检查。我会把每张表的字段类型、是否允许为空、默认值、是否自增全部导成 CSV。这一步特别有用因为后面做数据类型映射时可以直接对着清单逐条改不用反复连数据库看。另外要专门检查ENUM、SET、JSON、YEAR、BIT、TINYINT(1)这类 MySQL 特征明显的类型它们基本都是迁移时最容易出问题的对象。我自己这次就遇到了三张表用了JSON类型应用层还依赖 MySQL 对 JSON 的字段提取语法后面不得不单独做适配方案。最后是数据库对象视图、触发器、存储过程、函数、定时事件、外键约束。外键我建议迁移后先不建等数据迁完并校验通过后再补否则数据抽取顺序会非常受限制容易出现“父表没插完子表插不进去”的尴尬。2.2 SQL 兼容性评估这一步决定了改造工作量对象盘点做完了真正的坑才开始出现——那就是业务代码里的 SQL。我会用两种方式收集 SQL一是在应用代码里全局搜索SQL语句二是借助 MySQL 的general_log或慢查询日志抓取一段时间内的真实执行语句。抓到的 SQL 统一存成一个文本文件然后批量跑一次静态扫描找出里面含有的特殊语法。以我的项目为例实际扫描出来三类高危用法一是REPLACE INTO这个语法在金仓中并没有等价的直接实现二是ON DUPLICATE KEY UPDATE这是 MySQL 特有的“存在即更新”的写入方式三是GROUP_CONCAT做字符串聚合金仓中需要改写成STRING_AGG。这些 SQL 如果靠人工一点一点翻代码根本不现实但用脚本扫描加上关键字命中半小时就能定位完全部风险点。这一步还应该检查 JDBC 连接串和驱动。MySQL 的 JDBC URL 是jdbc:mysql://host:3306/db?useSSLfalseserverTimezoneAsia/Shanghai这种写法而金仓走的是jdbc:kingbase8://host:54321/db并且还涉及驱动版本、ssl参数、时区设置等细节。我强烈建议在评估阶段就写个小程序把目标库连一遍确认驱动能通、字符集正常、时区匹配不要到割接那天才第一次连目标库。2.3 目标端环境准备字符集、大小写、隔离级别金仓本身对 PostgreSQL 生态兼容程度很高所以很多行为和预期可以参考 PostgreSQL 来对待。目标端第一个要确定的是字符集我直接定为UTF8并且让所有库、表都继承这个字符集。MySQL 那边如果有些老表还是latin1最好先确认业务有没有依赖原始字节流否则迁移后中文会乱。第二个要确定的是大小写策略。MySQL 在 Linux 下表名区分大小写列名不区分金仓和 PostgreSQL 类似默认会把没有加引号的标识符折叠成小写。这听起来简单实际操作时非常容易坑人。如果原 MySQL 库里有驼峰表名或大写字面量字段迁移后应用去查就会发现找不到关系。第三个是隔离级别。MySQL 默认是REPEATABLE READ金仓默认是READ COMMITTED如果你的业务在事务里依赖 MySQL 的可重复读语义比如某些统计查询在长事务里要求看到一致的快照那就要提前评估是否需要把目标库的隔离级别调整一下。我这次遇到的麻烦不大但还是把这条写进了评估报告。3. 结构与数据迁移实操类型映射、自增序列、数据抽三件套结构和数据迁移是整个迁库过程中最“体力活”的部分但也是最容易埋雷的部分。这里说的“结构”不仅是建表语句还包括索引、约束、默认值、注释、自增序列等数据库元信息。数据迁移则是把几千万甚至上亿行的数据从一个引擎搬到另一个引擎效率和稳定性需要同时兼顾。3.1 数据类型映射一张表解决 90% 的字段问题做结构迁移时我手边放了一张类型映射表。这是我和团队在迁移中反复打磨出来的现在分享给大家。以一张非常典型的订单表为例。MySQL 原生建表语句大概长这样CREATE TABLE orders ( id INT NOT NULL AUTO_INCREMENT COMMENT 订单ID, order_no VARCHAR(32) NOT NULL COMMENT 订单号, user_id INT NOT NULL COMMENT 用户ID, status TINYINT NOT NULL DEFAULT 0 COMMENT 状态0待支付 1已支付 2已取消, amount DECIMAL(10,2) NOT NULL DEFAULT 0.00, pay_time DATETIME DEFAULT NULL COMMENT 支付时间, remark TEXT COMMENT 备注, extra JSON DEFAULT NULL COMMENT 扩展信息, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_order_no (order_no), KEY idx_user_status (user_id, status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;在金仓中转换后的建表语句是CREATE TABLE orders ( id SERIAL PRIMARY KEY, order_no VARCHAR(32) NOT NULL, user_id INTEGER NOT NULL, status SMALLINT NOT NULL DEFAULT 0, amount NUMERIC(10,2) NOT NULL DEFAULT 0, pay_time TIMESTAMP, remark TEXT, extra JSON, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ); COMMENT ON COLUMN orders.order_no IS 订单号; CREATE UNIQUE INDEX uk_order_no ON orders (order_no); CREATE INDEX idx_user_status ON orders (user_id, status);这套映射中最值得注意的规则有这些TINYINT映射成SMALLINT但如果业务只是拿它当布尔值用也可以直接映射成BOOLEAN不过这样应用层的0/1判断要做相应调整。稳妥起见我全用了SMALLINT。INT直接映射成INTEGERBIGINT保持BIGINT这个没有悬念。DATETIME映射成TIMESTAMP注意 MySQL 的DATETIME不带时区而金仓的TIMESTAMP也不带时区语义上是对得上的。但如果 MySQL 里用的是TIMESTAMP类型它本身带时区转换迁移时要小心时间的偏移。TEXT和BLOB分别对应金仓的TEXT和BYTEA。这里有个小坑金仓的TEXT虽然能存大文本但对TEXT做LIKE查询的索引方案和 MySQL 不一样后面性能调优时会提。JSON类型可以直接映射为金仓的JSON但两者支持的查询语法差异很大MySQL 的JSON_EXTRACT或-语法在金仓不通用。如果应用层只是存序列化字符串我建议直接映射成TEXT让应用自行解析反而省事。ENUM和SET是我的建议是直接用VARCHAR加CHECK约束或者更简单一点只保留VARCHAR由应用层保证合法性。这样能减少数据库端的兼容负担。YEAR映射成SMALLINT但要注意业务是否依赖YEAR在 MySQL 中的特殊取值行为。这张映射表在迁移时要贴在每个人桌上谁写结构迁移脚本都以它为准能够避免团队内部出现“你觉得该选这个类型、我觉得该选那个类型”的扯皮。3.2 主键、自增序列与索引重建MySQL 的AUTO_INCREMENT是一个“伪序列”它直接绑定在表上而金仓的SERIAL底层是创建一个独立的序列对象表默认值调取nextval。这个机制上的差异直接带来两个问题。第一个是序列起始值。比如 MySQL 的订单表id已经涨到一千万直接迁移表结构和数据后如果不做处理下一次插入会从序列原本的初始值开始轻则主键冲突重则数据错乱。所以我迁移完数据后都会跑这样一段 SQLSELECT setval(orders_id_seq, (SELECT MAX(id) FROM orders));注意序列名默认是“表名_字段名_seq”如果你建表时用了大写或加了双引号序列名也要跟着调整。如果创建表时用的是GENERATED BY DEFAULT AS IDENTITY这种语法重置方式又不一样需要查对应版本的文档确认。第二个是索引长度问题。MySQL 的 InnoDB 在 utf8mb4 字符集下普通索引键最大长度是 767 字节所以有些老表里索引字段不会定义得太长。金仓这边对索引长度的限制宽松一些但如果你原表里对超长VARCHAR建了前缀索引比如INDEX idx_title (title(100))这种语法在金仓并不直接支持需要改成普通全列索引或者改用表达式索引。遇到这种情况不要硬转先和业务确认索引的真实用途再决定方案。3.3 数据抽取的三种方式COPY 最快但不是每张表都适合数据量大时最忌讳的就是逐行 INSERT。我这次项目的实际经验是对于能连上目标库且连接串可达的场景大表用 COPY 类协议的速度远超普通 INSERT。金仓基于 PostgreSQL 内核支持COPY FROM STDIN协议。所以连接层面我直接用了支持该协议的驱动配合 Python 的psycopg2整条链路非常顺滑。我这里写了一个批量抽取脚本的思路可以看作是可复制的参考模板import pymysql import psycopg2 from io import StringIO src_conn pymysql.connect( hostmysql_host, port3306, usermigration, passwordxxxx, databaseapp_db, charsetutf8mb4 ) dst_conn psycopg2.connect( hostkingbase_host, port54321, usersystem, passwordxxxx, databaseapp_db ) cur src_conn.cursor() cur.execute(SELECT id, order_no, user_id, status, amount, pay_time, remark FROM orders) batch [] for row in cur: batch.append(row) if len(batch) 10000: _copy_batch(dst_conn, batch) batch.clear() if batch: _copy_batch(dst_conn, batch)_copy_batch内部做的事情就是把这批行写入一个内存中的 CSV 缓冲区然后调用COPY orders(id, order_no, ...) FROM STDIN WITH CSV推到目标库。用这种方式的单表千万级数据迁移时间是普通executemany的十分之一左右。当然不是所有表都适合用 COPY。如果有表级触发器、外键约束、自增字段需要回填原值我建议先关闭约束和触发器用 COPY 裸灌最后再统一处理序列、补校验、启用约束。大事务要拆成多个批次我一般控制在每批一万到两万行避免事务过大会引发锁和日志膨胀。3.4 存储过程、触发器和定时任务的特殊处理存储过程这次让我最头疼。MySQL 的存储过程语法和 PL/pgSQL 差异很大几乎是两种语言。比如一个最简单的支付更新存储过程MySQL 写法是DELIMITER // CREATE PROCEDURE sp_pay(IN p_order_no VARCHAR(32)) BEGIN UPDATE orders SET status 1, pay_time NOW() WHERE order_no p_order_no; END// DELIMITER ;金仓中的等价实现是CREATE OR REPLACE PROCEDURE sp_pay(p_order_no VARCHAR) LANGUAGE plpgsql AS $$ BEGIN UPDATE orders SET status 1, pay_time NOW() WHERE order_no p_order_no; END; $$;两者声明变量的位置、游标写法、异常捕获方式、返回结果集的方式都完全不一样。如果存储过程简单改写还能接受如果遇到几百行的复杂过程涉及动态 SQL、临时表、循环游标那改写的成本会非常高。我这次就有一个存储过程光改写加测试就花了一天最后我甚至建议业务方直接用应用层代码替代这个存储过程反而更可控。触发器也类似。MySQL 的BEFORE INSERT、AFTER UPDATE等触发器逻辑在金仓中可以保留但需要注意OLD、NEW的引用方式在 PL/pgSQL 中略有差异而且必须用CREATE TRIGGER配合函数来写。定时任务方面MySQL 的EVENT可以直接映射为数据库侧的 Job或者干脆交给云平台的定时调度器来做。我个人更推荐后者因为数据库侧的调度一旦出问题排错的代价太高。4. 应用 SQL 适配这场迁移的重头戏数据搬完只是完成了三分之一的工程量。真正让整个团队连续加班的是应用代码里的 SQL 改造。这节我会详细列出高频差异点和改写方案这些都是实打实趟出来的经验。4.1 高频函数对照IFNULL、DATE_FORMAT、GROUP_CONCAT 这类必须逐项清MySQL 的函数体系对开发者非常友好但换到金仓环境很多“理所当然”的函数不再存在或者行为不一致。我这里整理了一张高频对照表建议直接收藏MySQL金仓KingbaseES备注IFNULL(a, b)COALESCE(a, b)部分模式下可能兼容但统一改最保险IF(expr, a, b)CASE WHEN expr THEN a ELSE b END没有等价函数DATE_FORMAT(d, fmt)TO_CHAR(d, fmt)格式符完全不同NOW()CURRENT_TIMESTAMP行为基本一致CONCAT(a, b)CONCAT(a, b)注意 NULL 行为MySQL 忽略 NULLPG 返回 NULLGROUP_CONCAT(x)STRING_AGG(x, ,)这是最高频的改动点之一LIMIT n OFFSET m同样支持LIMIT n OFFSET m但大偏移时性能恶化需要改写REGEXP~正则语法有差异LENGTH(str)LENGTH(str)注意 MySQL 按字符长度PG 的length也是字符长度而octet_length才是字节长度以DATE_FORMAT为例MySQL 里写DATE_FORMAT(pay_time, %Y-%m-%d %H:%i:%s)到了金仓必须改成TO_CHAR(pay_time, YYYY-MM-DD HH24:MI:SS)。如果项目里这种格式化分散在几十个 SQL 里一个个改真的会想死。所以我强烈建议在应用层统一封装一个时间格式化工具方法尽可能让 SQL 层只取原始时间由代码来做格式化。这样不仅能减少 SQL 改写量也能更灵活地适应不同数据库。GROUP_CONCAT改成STRING_AGG也不只是换个函数名那么简单。MySQL 的GROUP_CONCAT默认最大长度是 1024 字节超长会被截断金仓的STRING_AGG没有这个限制但数据类型要求必须是文本。如果聚合字段是数字记得先CAST成文本再聚合。4.2 写入语法差异REPLACE INTO 和 ON DUPLICATE KEY UPDATE 怎么破这两类语法是 MySQL 业务系统里非常常见的。但金仓对它们并没有直接支持必须改写。我的推荐方案是统一改成“先判断、后写入”或者利用金仓的INSERT ... ON CONFLICT语法来做部分场景的替换。ON DUPLICATE KEY UPDATE的逻辑可以改写成INSERT INTO orders (id, order_no, status) VALUES (1001, NO1001, 1) ON CONFLICT (id) DO UPDATE SET order_no EXCLUDED.order_no, status EXCLUDED.status;这个语法在 PG 生态里是标准的金仓也支持。但要注意ON CONFLICT要求冲突目标必须是唯一索引或主键如果你的业务在 MySQL 里依赖的不是主键冲突而是某个业务唯一键冲突那么这里必须显式指定冲突列比如ON CONFLICT (order_no)。而REPLACE INTO就麻烦一些它在 MySQL 里做的事情其实是“先删除冲突记录再插入新记录”。金仓没有这个语义。我这次直接用事务包住“先 DELETE 再 INSERT”虽然在极端并发下会有瞬时空窗但对业务影响很小。如果业务要求必须无感替换可以用ON CONFLICT DO UPDATE替代前提是你能接受旧记录被更新而不是删除之后重建——这两者对于有自增主键、有外键关联的表来说语义差距巨大。所以我的建议是不要无脑替换先让业务方确认他们到底依赖哪种语义。4.3 大小写敏感与保留字冲突一批莫名其妙的报错源头我从没想到连表名大小写都能成为割接当晚的拦路虎。金仓默认会把没有引号的标识符折叠成小写而 MySQL 在 Linux 环境下表名保留原始大小写。如果原库里有张表叫UserInfo迁移后你在大写场景下查询金仓实际找的是userinfo就会报 “relation does not exist” 或类似的错误。我这次有十几张表是驼峰命名最后统一做了“小写化处理”建表时全部转成小写表名应用 SQL 也统一改为小写表名。这样处理虽然要动应用代码但一劳永逸比后续所有查询都加双引号要省心得多。保留字冲突是另一个隐形炸弹。MySQL 里可以用反引号把关键字包起来比如SELECT * FROM \order。金仓则要求关键字作为标识符时必须加双引号或者改名。我的做法是尽量给表加个前缀或者换个名词避免使用 order、user、level 这类高频保留字。如果业务 SQL 量很大改表名成本太高那就只能统一把相关位置改成双引号引用但这样后续写 SQL 会很痛苦。4.4 SQL 批量改造工具与回归验证方法面对几百条 SQL总不能真的靠 Vim 一个个翻。我的方法是写一个正则替换脚本先把最高频的几种模式批量处理比如把IFNULL(替换成COALESCE(把DATE_FORMAT(的参数按规则转换成TO_CHAR(。这里要特别小心正则的边界匹配不要误伤字符串里的内容。改完之后的回归验证我强烈建议做一次“影子库对比”准备一台测试机同时部署 MySQL 和金仓应用代码里做一个数据源切换开关跑同一组业务测试用例对比两者的返回结果集。不需要每行结果都一致但业务关键路径上的 SQL 必须核对。我通过这种方式发现了大约十几个之前静态扫描没发现的问题比如CONCAT在 NULL 行为上的差异导致某个人名拼接结果变成了空字符串这种 bug 在测试阶段暴露是幸运的上线后再暴露就是事故。5. 数据校验与性能调优别以为迁完就万事大吉数据迁完、SQL 改完、应用也连上目标库了这时候最大的风险不是“数据丢了”而是“数据不对了你没发现”。所以校验这个环节我不允许跳过而且方法要科学不能只数行数。金仓在查询性能上和 MySQL 也有明显差异原来走得好好的索引计划换个优化器可能就全表扫描了。5.1 数据校验三板斧数量、抽样、边界值第一板斧是总数校验。每张表各自SELECT COUNT(*)两边对比。这听起来简单但大表做COUNT(*)非常慢。我的做法是分片统计比如按主键范围切十个区间每个区间并行计算行数不仅快还能顺便确认主键连续性。不过要注意 MySQL 中 MyISAM 表的COUNT(*)会立刻返回而 InnoDB 需要全扫。如果迁移前后行数对不上第一时间检查是不是还有正在写入的应用连接最好是选一个业务停写窗口做最终校验。第二板斧是全字段抽样校验。选几张大表按主键哈希随机抽取几百条记录逐字段对比。我当时写了个对比脚本从两边分别取出同一条主键记录序列化成 JSON 后逐字段 diff。这种方式能把字段值的细微差异抓出来比单纯校验数量的价值高得多。第三板斧是时间边界值校验。对每张有时间字段的表查到 MySQL 的最小值和最大值再到金仓查同样范围确认没有数据错位或时间时区偏差。这个很容易忽略但时间数据出问题的概率一点都不低。5.2 迁移后 SQL 变慢的排查姿势迁移后最普遍的问题是“原来秒回的查询现在跑出十几秒”。原因一般集中在四个方向。第一个是没有更新统计信息优化器拿到的数据分布是空的自然选不出好计划所以迁完数据我立刻ANALYZE了所有核心表。第二个是没有索引原 MySQL 里的索引虽然建了但有些索引列在金仓侧因为类型映射变化导致表达式不匹配。第三个是 SQL 写法本身和优化器策略不合拍比如大偏移LIMIT 100000, 20这种写法在 MySQL 里就已经很差在金仓中更差需要改成基于游标或基于主键的分页方式。第四个是数据库参数还是默认值内存和并行设置都没有针对新库调优。我这次有一个典型例子一张三千万行的订单流水表查询条件是WHERE user_id ? AND status ? ORDER BY create_time DESC LIMIT 20。MySQL 那边有联合索引跑得很顺。金仓迁移后虽然建了同样的索引但优化器还是选了全表扫描原因是统计信息显示status列的分布极度倾斜优化器认为走索引回表代价更高。解决办法是给 SQL 加了一个status 0的针对性部分索引或者干脆强制指定优化器选择。这个案例让我认识到不同数据库的优化器性格差异很大别指望一套索引打天下要针对计划逐条调。5.3 服务器参数调整参考金仓的默认参数明显偏向保守。以我这次用的 64G 内存服务器为例调整幅度比较大的是这几个参数参数默认值调整后说明shared_buffers128MB16GB数据库共享缓存建议为内存的 25% 左右effective_cache_size4GB48GB让优化器知道系统可用缓存work_mem4MB64MB排序和哈希操作可用内存但要注意每个连接都会消耗maintenance_work_mem64MB2GB建索引、ANALYZE、VACUUM 等维护操作使用max_connections100500根据应用连接池上限调整wal_level/ 归档等默认按需开启影响故障恢复能力这里提醒一句参数调整不是越大越好work_mem如果设得过高并发连接多时内存会瞬间爆掉一定要结合最大连接数一起估算。比如work_mem64MB1000 个并发连接都跑排序峰值就是 64GB哪怕服务器内存是 128G 也扛不住。所以要让应用侧的连接池合理配比并且对极端 SQL 做限流。6. 高频问题速查表这次迁移踩过的坑都在这里我把这次迁移中遇到的典型报错和排查结论整理成一个速查表方便在不同阶段直接对照。报错或现象可能原因解决办法relation orders does not exist表名大小写不一致实际查询变成了orders之外的标识符统一表名为小写或加双引号syntax error at or near orderorder是保留字表名/字段改名或加双引号function concat(text, text) does not exist类型不匹配常见于数字和文本拼接先做CAST再拼接导入报错时间字段date/time value out of rangeMySQL 里存在0000-00-00这类非法时间导出前统一替换为 NULL 或合理默认值自增插入时主键冲突序列起始值没有重置迁移后执行setvalGROUP_CONCAT不识别函数不存在改写为STRING_AGG大表迁移速度极慢逐行 INSERT没有用 COPY 协议或批量提交改用 COPY批量提交SQL 在 MySQL 秒回金仓慢几十倍统计信息缺失、索引不匹配、参数未调优执行ANALYZE重新审查执行计划JDBC 连接失败报 SSL 错误目标库要求 SSL连接串缺参数在 URL 中显式配置ssl相关参数LIMIT大偏移量查询越来越慢分页写法本身低效改为游标分页或主键定位分页NULL 排序顺序和原先相反MySQL 默认 NULL 在前金仓默认 NULL 在后ORDER BY中显式写NULLS FIRST/LAST这张表只是我这次项目的一个缩影每个环境还会遇到自己特有的问题。但方法论是通用的遇到怪异报错先查标识符大小写再查数据类型然后是函数兼容性最后才是数据库参数和计划问题。这个项目结束后我个人的最大感受是迁移的本质不是搬数据而是做一次数据库思维的切换。MySQL 的很多便利特性在金仓中必须换成更标准的写法这个过程无法靠工具一键完成必须有人完整理解两套数据库的行为差异。如果这篇文章能帮你提前规避几个我踩过的坑那这三周的加班也算值了。如果你也在做类似的迁移我最后再补一句实际操作中的建议保留一套完整的 MySQL 原环境至少两周不要迁移完就急着清理。很多问题是真实业务跑起来之后才会暴露的保留原始环境可以让你随时回去对比数据和行为这是排查问题时的救命稻草。