1. 迁移前必须先想清楚的几件事
在拿到"SQL Server 迁移到 MySQL"这个任务时,先别急着开工具导数据。我见过太多人第一步就栽在评估上——误以为两边都是关系型数据库,表结构导过去、数据灌进去就算完事,结果上线当晚存储过程全部报错、自增主键错乱、中文乱码一片,最后通宵救火。所以要先把整个流程拆成几个阶段:评估盘点、环境准备、结构迁移、数据迁移、对象改写、验证回滚。每个阶段都有独立的验收标准,前一个没过关就别往后走。
先说你到底为什么要迁。常见的理由有三类:一是成本,SQL Server 的许可费在中小公司里确实是笔不小的开销,而 MySQL 开源免费;二是生态,业务上要接一些开源组件或云数据库服务,而它们对 MySQL 的兼容性更好;三是团队技能,招人时懂 MySQL 的候选人明显比懂 SQL Server 的多。但这三类理由对应的迁移难度完全不同。如果只是报表库,那迁移成本很低,大部分工作就是改连接串;如果业务核心库有几百个存储过程、几十个作业和一堆 SSIS 包,那这个项目就是个小型重构工程,得按项目制来排期。
在开始之前,最好先对源库做个摸底。用下面这几条 SQL 快速统计一下对象数量,能让你心里大概有个底:
-- 统计表和行数规模 SELECT t.name, p.rows FROM sys.tables t JOIN sys.partitions p ON t.object_id = p.object_id WHERE p.index_id IN (0,1) ORDER BY p.rows DESC; -- 统计存储过程、视图、函数数量 SELECT type, COUNT(*) AS cnt FROM sys.objects WHERE type IN ('P','V','FN','TF','IF') GROUP BY type; -- 统计索引数量 SELECT COUNT(*) FROM sys.indexes WHERE type > 0;这些数字直接决定你要投入多少人力。我做过一个真实项目,源库有 300 多张表,存储过程 500 多个,迁移之前我按对象数量估了三周,实际干了一个半月——因为那些存储过程里用到的语法差异,远比文档里写的要复杂。所以第一课:永远给评估阶段留足时间,别拿"都是关系型数据库"当挡箭牌。
2. 数据库对象迁移:表结构、数据类型与索引的差异处理
确定要迁移之后,第一个硬骨头就是表结构。这里最容易踩的坑是数据类型映射。SQL Server 和 MySQL 虽然都是关系型数据库,但数据类型体系差异相当大,直接用默认映射工具处理,往往会留下后患。
2.1 数据类型映射规则
先给你一份我实际项目中沉淀下来的映射表,照着用基本不会出大问题:
| SQL Server 类型 | MySQL 类型 | 备注 |
|---|---|---|
| INT | INT | 直接对应 |
| BIGINT | BIGINT | 直接对应 |
| SMALLINT | SMALLINT | 直接对应 |
| TINYINT | TINYINT | SQL Server 的 TINYINT 是 0-255,MySQL 也是 0-255,没问题 |
| BIT | TINYINT(1) | 千万别用 BIT,MySQL 的 BIT 是位类型,行为不同 |
| DECIMAL(p,s) | DECIMAL(p,s) | 精度标度一致,直接搬 |
| FLOAT | DOUBLE | SQL Server 的 FLOAT 默认 53 位,对应 MySQL 的 DOUBLE |
| DATETIME | DATETIME | SQL Server 的 DATETIME 精度约 3.33ms,MySQL 的 DATETIME 精确到秒,数据本身没差别 |
| DATETIME2 | DATETIME(3) / DATETIME(6) | 按实际需要的精度来,DATETIME2(3) 对应 DATETIME(3) |
| SMALLDATETIME | DATETIME | SMALLDATETIME 只到分钟,DATETIME 精度更高,不丢数据 |
| NVARCHAR(n) | VARCHAR(n) CHARACTER SET utf8mb4 | 关键点:SQL Server 的 NVARCHAR 是 UTF-16,MySQL 的 VARCHAR 在 utf8mb4 下是变长 UTF-8,n 的含义完全不同,要特别注意长度换算 |
| VARCHAR(n) | VARCHAR(n) | 单字节字符集下问题不大 |
| NTEXT | TEXT / MEDIUMTEXT | 根据最大长度选 |
| TEXT | TEXT / MEDIUMTEXT | 同理 |
| IMAGE | LONGBLOB | 二进制大对象 |
| UNIQUEIDENTIFIER | CHAR(36) | MySQL 8 支持原生 UUID 类型,但兼容性最好还是用 CHAR(36) |
| MONEY | DECIMAL(19,4) | MONEY 是定点数,别用 FLOAT 替代 |
| XML | TEXT / LONGTEXT | MySQL 没有原生的 XML 类型,只能存文本 |
| GEOGRAPHY | GEOMETRY(4326) | 空间数据,MySQL 8 支持,但函数体系完全不同 |
| ROWVERSION | BINARY(8) | 用于乐观并发,迁移后要改造逻辑 |
| SQL_VARIANT | 无对应 | 必须手动拆分或改设计 |
这里最容易翻车的是 NVARCHAR。SQL Server 里 NVARCHAR(100) 表示能存 100 个 Unicode 字符,到了 MySQL 里 VARCHAR(100) 在 utf8mb4 下也是 100 个字符,这个倒没变。但要命的是 MySQL 的 VARCHAR 长度有个 65535 字节的行限制,utf8mb4 下一个字符最多占 4 字节,所以 VARCHAR(16384) 在 SQL Server 里没问题,搬到 MySQL 就超限了。处理办法是:把超长字符串列改成 TEXT 类型(TEXT 不走行内字节限制),或者缩小定义长度。注意,TEXT 类型在 MySQL 里不能有默认值,这个问题虽然在 MySQL 8.0.13 之后得到部分解决(允许默认值为表达式),但仍有坑——如果你的应用在插入时不显式给值,就会拿到 NULL 而不是期望的空字符串。
还有一个 MONEY 类型,SQL Server 的 MONEY 是十进制定点数,精度到万分之一。有人图省事直接映射成 DOUBLE,金额这种东西用浮点存,迟早要出事。老老实实映射成 DECIMAL(19,4)。如果业务上对金额精度有更高要求,改成 DECIMAL(20,6) 也行,但要同步改应用层的读取逻辑。
2.2 主键、自增列与索引迁移
SQL Server 的自增列(IDENTITY)对应 MySQL 的 AUTO_INCREMENT。结构迁移时两张表的主键定义看起来差不多,但有几个行为差异你必须知道:
SQL Server 的 IDENTITY 列插入显式值后,种子会自动跳到该值 + 1。MySQL 的 AUTO_INCREMENT 同样如此。但两者有个关键区别:SQL Server 支持SET IDENTITY_INSERT [table] ON来强制插入,MySQL 只需要在 INSERT 语句里显式列出自增列名即可。迁移数据时这是必须的操作,否则历史主键值对不上,后续应用逻辑全乱。
索引方面,SQL Server 的聚集索引和 MySQL 的 InnoDB 主键概念有对应关系,但非聚集索引的处理要小心。SQL Server 允许在视图上建索引(索引视图),MySQL 8 虽然支持功能索引和表达式索引,但视图片本身不支持索引。如果你源库里有索引视图,迁移后这些索引全部失效,得评估对查询性能的影响。
还有全文索引。SQL Server 的全文索引和 MySQL 的 FULLTEXT 索引在分词器、语法(CONTAINS vs MATCH...AGAINST)上完全不同。如果有全文检索需求,建议在迁移后考虑用专业的全文检索引擎(比如开源界常用的 Elasticsearch),而不是硬把 SQL Server 的全文索引搬到 MySQL 上。
3. SQL 语法差异与存储过程改写
结构迁完后,真正耗时间的是 SQL 语法层面的改写。T-SQL 和 MySQL 的 SQL 语法虽然同源,但差异点多到能单独写一本书。这里我把最常遇到的挑出来,每个都配上改写前后的对比,方便你直接抄作业。
3.1 基础语法差异对照表
| 功能 | SQL Server (T-SQL) | MySQL | 说明 |
|---|---|---|---|
| 限制返回行数 | SELECT TOP 10 ... | SELECT ... LIMIT 10 | 最常用的差异,OGG/DataX 等工具不帮你改,得自己来 |
| 字符串拼接 | SELECT 'a' + 'b' | SELECT CONCAT('a','b') | T-SQL 里 + 也能拼,MySQL 里 + 是数字运算 |
| 获取当前时间 | GETDATE() | NOW()/CURRENT_TIMESTAMP | 几乎每个存储过程都会用到 |
| 日期格式化 | CONVERT(VARCHAR, GETDATE(), 120) | DATE_FORMAT(NOW(), '%Y-%m-%d %H:%i:%s') | 完全不同的语法体系 |
| 字符串长度 | LEN('abc') | CHAR_LENGTH('abc') | LEN 不含尾部空格,LENGTH 在 MySQL 里是字节数 |
| 子串截取 | SUBSTRING('abc', 1, 2) | SUBSTRING('abc', 1, 2) | 这个居然一致,用起来最舒服 |
| 类型转换 | CAST(x AS INT) | CAST(x AS SIGNED) | MySQL 没有 INT 作为转换目标,必须用 SIGNED |
| 判空函数 | ISNULL(x, 0) | IFNULL(x, 0) | 拼写只差一个字母,但意思一样 |
| NULLIF | NULLIF(a, b) | NULLIF(a, b) | 这个一致 |
| 逻辑判断 | IF ... ELSE | IF ... ELSEIF ... ELSE | 注意是 ELSEIF 而不是 ELSE IF |
| 分页 | OFFSET ... FETCH | LIMIT offset, count | 写法区别明显 |
| 布尔值 | WHERE flag = 1 | WHERE flag = 1 | SQL Server 的 BIT 映射成 TINYINT(1) 后,查询条件不用变 |
| 取模 | a % b | MOD(a, b)或a % b | 都支持,写法随意 |
| 字符串查找 | CHARINDEX('a', 'abc') | LOCATE('a', 'abc')/INSTR('abc', 'a') | 参数顺序不同,极易搞反 |
| 去空格 | LTRIM(RTRIM(x)) | TRIM(x) | MySQL 的 TRIM 默认去两端空格 |
| 时间加减 | DATEADD(DAY, 1, GETDATE()) | DATE_ADD(NOW(), INTERVAL 1 DAY) | 完全不同 |
| 求日期差 | DATEDIFF(DAY, d1, d2) | DATEDIFF(d1, d2) | 参数顺序完全相反,大坑 |
DATEDIFF这个参数顺序问题是典型的隐蔽错误。SQL Server 的写法是DATEDIFF(单位, 开始, 结束),MySQL 的写法是DATEDIFF(结束, 开始),而且单位固定是"天"。迁移后你不会立刻看到报错,只会莫名其妙地发现日期差算成了负数或者差了一整天。这类问题靠跑一遍批量测试能抓出来,但更靠谱的是在迁移前写一个对比测试脚本,把每条 SQL 在两边各跑一遍,结果不一致的标记出来。
3.2 存储过程改写实战
存储过程的改写是整场迁移的核心战役。SQL Server 存储过程的功能密度相当高,里面充斥着批处理、临时表、游标、动态 SQL、错误处理。逐条改写工作量极大,我给你一套高效改写的步骤:
第一步,拿到源库的存储过程代码后,先做静态扫描。用正则搜索这些高频模式:WITH、CTE、TOP、GETDATE()、ISNULL、STRING_AGG、PIVOT、TRY...CATCH、@@ERROR等。特征匹配到的直接按对照表批量替换,这一步能解决 60% 的机械性改动。
第二步,处理临时表语义差异。SQL Server 里#temp是会话级临时表,存储过程结束时自动清理。MySQL 5.7 之前不支持会话级临时表在存储过程里反复创建(存储过程编译时临时表必须存在),MySQL 8.0 之后也仍然不建议依赖这个行为。最常见的改写方式是换成内存临时表,但要注意 MySQL 临时表的创建语句会被记录到 binlog,在复制环境下有主从数据不一致的风险。更加稳妥的做法是:把临时表改成语义等价的派生表(FROM 子查询)或者 CTE,虽然可读性差点,但彻底绕开了临时表生命周期的问题。如果业务逻辑确实离不开临时表(比如要建索引、多次引用),那 MySQL 里就用CREATE TEMPORARY TABLE,但每次调用存储过程时先DROP TEMPORARY TABLE IF EXISTS,避免残留数据。
有一个经验数据:如果源库存储过程里有超过 3 个临时表,改写成 CTE 的难度会急剧上升,因为 CTE 的作用域只在单个语句内。这时候我的建议是直接在 MySQL 存储过程里用临时表,但在开头加个清理动作。实测下来这种方案性能不差,而且改动量最小。
第三步,事务与错误处理。SQL Server 的TRY...CATCH结构对应 MySQL 的DECLARE ... HANDLER或者SIGNAL,但两者错误传播机制完全不同。MySQL 的默认行为是存过过程运行时遇到错误就中止,这点对应用层可能反而是好的(语义更简单),但如果你的存储过程里有"捕获错误、记录日志、继续执行"的模式,就必须用CONTINUE HANDLER。改写时注意,MySQL 存储过程里没有RAISERROR,新版用SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '...'来主动抛出业务错误。
第四步,游标处理。SQL Server 的游标语法和 MySQL 类似,但有个区别:MySQL 的游标必须在声明 handler 之前声明,否则会出现"declare handler order"的报错。实际改写经验:能不用游标就尽量不用游标,MySQL 8 支持窗口函数,很多在 SQL Server 里必须用游标做的行级操作,比如累加求和、取前 N 行、相邻行比较,现在都能用窗口函数一行搞定,性能提升十倍不止。
分享一个真实案例。某财务系统的存储过程,从 SQL Server 迁到 MySQL 后性能不升反降。排查后发现是游标嵌套:外层游标 200 行,内层对每行做一次子查询,导致执行次数暴涨到几十万次。改写方案是把嵌套逻辑改成一条 JOIN 加窗口函数,执行时间从 47 秒降到 1.2 秒。所以存储过程改写不只是翻译语法,更是一次重构的机会,任何能用集合运算解决的问题,都不要用逐行迭代去解决。
4. 数据迁移方案与工具选型
结构搞完,终于到了灌数据这一步。数据迁移方案的选择取决于数据量、停机窗口和可用工具。我按数据量给你三档方案,各自适配不同场景。
4.1 十万行以下:手工导入
表少、数据量小的情况下,直接基于 SSMS 右键导出数据,或者用bcp工具导出文本文件,再在 MySQL 侧用LOAD DATA INFILE导入。LOAD DATA INFILE是 MySQL 最快的导入方式,比逐条 INSERT 快几十倍。需要留意的是:
- 导出时统一使用 UTF-8 编码;
- 字段分隔符别用逗号,数据里可能有逗号,建议用竖线
|或制表符; - 文本限定符要选好,不然数据里的引号会把行结构打崩;
- 控制文件把空字符串和 NULL 的区别处理清楚。
-- SQL Server 侧导出 bcp "SELECT col1, col2, col3 FROM db.dbo.tbl" queryout data.csv -S server -U user -P pass -c -t "|" -r "\n" -- MySQL 侧导入 LOAD DATA INFILE '/path/to/data.csv' INTO TABLE tbl FIELDS TERMINATED BY '|' ENCLOSED BY '"' LINES TERMINATED BY '\n' (col1, col2, col3);注意LOAD DATA INFILE在 MySQL 8 默认对文件路径有secure_file_priv限制,文件必须放在指定目录下,否则报The MySQL server is running with the --secure-file-priv option。这是被问得最多的问题之一。解决办法是把文件放到secure_file_priv指向的目录,或者用LOAD DATA LOCAL INFILE(允许客户端本地文件),但后者需要客户端和服务端都开启local_infile选项,云数据库实例通常默认关闭。
4.2 百万到千万行:DataX / Kettle
数据量中等的情况下,手工导出就不好使了,原因有两个:一是 bcp 导出的文件太大,传输和导入时间不可控;二是停写窗口可能只有几小时,手工操作容易超时。这时候上 ETL 工具比较合适。
我比较推荐阿里开源的数据同步工具 DataX,原因在于它的插件机制非常成熟,同时支持 SQL Server Reader 和 MySQL Writer,对数据类型的处理经过了大量生产环境的验证。写个 job.json 配置文件就能跑起来,逻辑简单,也适合在迁移过程中反复执行(比如先试迁,再全量)。
DataX 配置示例:
{ "job": { "content": [ { "reader": { "name": "sqlserverreader", "parameter": { "username": "sa", "password": "***", "connection": [ { "table": ["dbo.tbl"], "jdbcUrl": ["jdbc:sqlserver://host:1433;DatabaseName=db"] } ], "column": ["col1", "col2", "col3"] } }, "writer": { "name": "mysqlwriter", "parameter": { "username": "root", "password": "***", "connection": [ { "table": ["tbl"], "jdbcUrl": ["jdbc:mysql://host:3306/db"] } ], "column": ["col1", "col2", "col3"], "preSql": ["DELETE FROM tbl"], "batchSize": 2048 } } } ], "setting": { "speed": { "channel": 8 } } } }有几个参数值得展开说:
channel是并发通道数,决定读取和写入的并发度。不要一上来就拉满,先在测试环境上跑一次,观察 SQL Server 的 CPU 压力和目标 MySQL 的写入吞吐。我通常先从 4 起步,最高加到 16,再高反而会遇到数据库自身的锁竞争。batchSize是每次事务提交的行数。太大容易导致事务日志膨胀,太小则网络往返太频繁。2048 是我比较常用的值,你可以根据字段宽度调整:字段少的可以加大到 5000,字段多的大字段(有 TEXT/BLOB)就得降下来。preSql里的 DELETE 是用来做幂等重跑的。迁移过程难免要试跑几次,没有这个配置,第二次跑的时候就全是主键冲突了。
4.3 千万行以上:分片迁移 + 增量同步
数据量上到千万甚至亿级,一次性全量同步就不现实了,因为全量搬运期间源库还在持续写入,搬完的那一刻数据已经不一致。这种场景标准的做法是"全量 + 增量"两步走:
第一步,在全量迁移开始前,记录一个一致性位点(可以简单记为某个时间点或某个大表的主键边界)。然后跑全量同步工具(DataX 或其它)。
第二步,全量跑完后,开启增量同步。增量同步通常依赖解析 SQL Server 的事务日志来捕获增量变更。这里我先给你交个底:SQL Server 和 MySQL 之间没有官方的实时同步工具,像 Oracle 的 GoldenGate 那样开箱即用的东西,在 SQL Server→MySQL 方向上不是免费的。实际项目中常见的解决路径是:在应用层做双写(改代码同时写 SQL Server 和 MySQL),或者基于日志解析工具(比如 Debezium 对 SQL Server CDC 的适配)做变更捕获,再通过消息队列写入 MySQL。
双写看上最省事,但它要求应用层把两个库的写入都当成事务来处理,一旦 MySQL 写入失败还容易导致业务整体回滚,对高可用来说是个不小的隐患。而配 Debezium 要定期清理日志读取位点,设计不好也会引发 SQL Server 事务日志无限膨胀。所以在项目启动阶段就必须明确增量同步实施方案,并且预留出至少两倍的调研和排错周期,否则大概率会拖到上线当天还在调同步延迟。
全量迁移过程中,几个容易忽略的点:
- 临时关闭 MySQL 侧的外键约束检查。导入数据时如果子表先导、父表后导,会直接违反外键。最稳妥的办法是在导入前执行
SET FOREIGN_KEY_CHECKS=0,导完再恢复。 - 关闭唯一性检查(
SET UNIQUE_CHECKS=0),数据先导进来再说。 - InnoDB 引擎导入期间调大
bulk_insert_buffer_size和innodb_buffer_pool_size,数据库配置好点的机器能明显提升导入速度。
5. 迁移后的应用适配与性能调优
数据搬过去了,结构也建好了,很多人以为这就完事了。实际上迁移后真正的战场是应用适配和性能调优。
5.1 连接层与应用代码适配
首先是连接串。SQL Server 的 JDBC 连接串长这样:
jdbc:sqlserver://host:1433;databaseName=mydb;user=sa;password=pass;MySQL 的 JDBC 连接串长这样:
jdbc:mysql://host:3306/mydb?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai注意几个参数:useSSL=false在 MySQL 8 里如果不开 SSL 必须显式标明,有些驱动版本不写会警告甚至报错;serverTimezone必须设,否则连接时报The server time zone value 'XXX' is unrecognized;高版本 MySQL 驱动要求useCursorFetch等参数的配置要合理,否则大数据量查询结果集默认全量加载到内存。
ORM 框架层面,Hibernate/MyBatis 都同时支持两种数据库,但方言(Dialect)配置不同。Hibernate 要改成org.hibernate.dialect.MySQL8Dialect,分页查询的自动生成语句从OFFSET变成LIMIT,其实反而是省事了。但如果项目里有人写了原生 SQL(比如在 Mapper XML 里写<select>的 SQL Server 特有语法),这些只能逐个改。
5.2 排序规则与大小写敏感
这块是迁移后最容易触发线上问题的地方。SQL Server 的数据库排序规则默认不区分大小写(取决于安装时选的 Collation,比如SQL_Latin1_General_CP1_CI_AS),而 MySQL 在 Linux 上默认表名和列名是区分大小写的,字符串比较区分大小写取决于字符集和排序规则。典型场景:代码里写select * from Users where name = 'admin',源库里匹配的是Admin,搬迁后查不到数据。排查方式是在 MySQL 里设置:
-- 数据库级别排序规则改为大小写不敏感(utf8mb4_general_ci 或 utf8mb4_0900_ai_ci) ALTER DATABASE mydb CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;如果需要逐表控制大小写敏感度,可以在建表时指定COLLATE。但必须记住一点:这个不一致会让应用埋雷,最好是做一次全局统一。如果没有特殊情况,迁移后一律用utf8mb4_0900_ai_ci(MySQL 8 默认)或utf8mb4_general_ci(MySQL 5.7 通用),统一为大小写不敏感和重音不敏感。遇到业务上真的有大小写敏感需求的列,单独在列级别指定utf8mb4_bin或者显式的_cs排序规则,不要整库混用。
5.3 常见性能问题排查
迁移后首轮压测通常会暴露几类典型问题:
第一,明明数据类型和数据量都一样,MySQL 就是比 SQL Server 慢。这类问题十有八九出在索引利用不充分。MySQL 的优化器能力相比 SQL Server 来说相对"朴素",复杂查询条件(比如 OR 多列、函数包裹索引列、隐式类型转换)很容易走全表扫描。
第二,查询WHERE date_col BETWEEN '2024-01-01' AND '2024-01-31'如果date_col是 DATETIME 且建了索引,两边性能都不错;但如果源库写的是WHERE CONVERT(VARCHAR, date_col, 112) = '20240101',迁移时顺手改成了WHERE DATE_FORMAT(date_col, '%Y%m%d') = '20240101',那索引就直接失效,全表扫描没跑。改写的时候要顺手把这类函数包裹索引列的模式改成范围查询。
第三,分页查询的性能差异大。SQL Server 的OFFSET...FETCH和 MySQL 的LIMIT offset, count在深分页(比如第 1000 页)时表现都差,但 MySQL 更明显。应对方案:记录上一页的最大 ID 做游标分页(WHERE id > last_id LIMIT 20),这个改写对前端分页逻辑几乎透明,但对数据库压力是数量级的下降。
第四,子查询与派生表的性能差异。SQL Server 对相关子查询的优化经验丰富,MySQL 在 5.7 之前对派生表会做物化(有derived_merge开关),8.0 之后优化器改善了不少。但如果迁移后碰到某个查询执行计划异常,优先手动改写为 JOIN 形式,实践里大多数情况下都更稳。
6. 容灾与回滚方案
迁移过程中"回滚"不是可选项,是必需品。我见过一个项目上线失败,因为没设计回滚路径,最后只能靠备份还原整库,业务中断了 4 个小时。
6.1 回滚预案设计
核心原则:在切换数据库之前,确保 SQL Server 源库一直保持"随时可写"的状态。具体操作建议:
- 迁移期间源库保持在线,不动表结构、不加索引、不改应用代码。
- 在正式切换前,做一次全量备份和差异备份,备份文件异地存放。
- 切换时间点选在业务低峰期,切换后保留一个观察窗口(我通常留 3 天),观察期间 MySQL 和 SQL Server 双写。方法:应用层同时写两个库,读 MySQL,如果 MySQL 侧出现严重问题,直接把读切回 SQL Server。
- 观察期结束后,再停掉 SQL Server 侧的业务写入,但库本身保留至少 30 天再下线。
"双轨运行"听起来不优雅,但它是在生产环境最稳妥的策略。很多团队嫌麻烦,直接一把梭切完就删源库,这是拿业务连续性赌博,一旦 MySQL 侧跑出逻辑 bug(比如存储过程改写时漏了个 ELSEIF),你就只能从一堆备份文件里碰运气了。
6.2 数据校验方法
切完后第一件事不是跑业务,而是验证数据一致性。我的习惯是做三层校验:
第一层,全表行数比对。用一条 SQL 同时查两边的行数,重点是有 WHERE 条件的分区子集,不能只看总行数。
-- SQL Server 侧 SELECT COUNT(*) FROM tbl WHERE create_date >= '2024-01-01'; -- MySQL 侧 SELECT COUNT(*) FROM tbl WHERE create_date >= '2024-01-01';第二层,抽样字段校验。取 1% 的随机行(至少要覆盖所有大表),逐字段对比关键业务字段(金额、时间、状态码)。
第三层,业务前置测试。让 QA 团队跑一遍核心业务链路(下单、支付、改单、查询),盯着每个接口的返回值和响应时间。
三层校验全过了,才算迁移闭环。
7. 迁移项目管理的几条实战经验
最后分享几条我在多个迁移项目里总结出来的管理经验,重要性不亚于任何技术细节。
第一,所有改过的存储过程、视图、函数,必须做回归对比测试。我的做法是准备一套脱敏数据,在源库和目标库上同时执行同名存储过程,自动比对结果集。不要嫌麻烦,存储过程改写是 bug 高发区,靠人工翻代码查错效率太低——写一个脚本自动比对,既能快速发现差异,又能当迁移后的验收依据。
第二,迁移不要追求一步到位。建议先从一个最小的业务模块开始跑通全流程(评估、迁移、验证、切换),形成一套可复用的 SOP,再推广到其他模块。这个试点模块的数据量不用大,但必须覆盖所有类型的数据变更场景(增删改查、批量导入、定时任务)。我第一次做这种项目时就是用一个小模块试点,结果把存储过程改写的坑全踩了一遍,后面大模块反而顺利多了。
第三,找一个人专门负责"迁移后性能基准对比"。迁移不是一个点状事件,很多性能问题在上线前压测场景下根本暴露不出来。上线后前两周,要有人持续对比相同业务在两边数据库的响应时间、慢查询数量、锁等待时间等指标。一旦发现 MySQL 侧的新增慢查询,立刻记录并优化,别等用户投诉到运维部门才去救火。
这些经验不是我凭空想出来的,是真的在一个个订单表、一张张日志表、一条条存储过程里踩出来的。数据库迁移这条路,永远不会像文档里写的那样顺滑,但只要你把每个环节的验收标准定清楚、把回滚路径留好,它就是一个可控的工程,而不是一场豪赌。