从 SQL Server 说实话,2024 年了还有人在纠结要不要从 SQL Server 迁到 MySQL,我挺意外的。但更意外的是,居然还有人在网上分享所谓的“一键迁移工具”,点进去一看全是广告。数据库迁移这种事情,永远没有一键完成,只有一套严谨的执行流程和足够的踩坑经验。
我自己带过好几个从 SQL Server 迁到 MySQL 的项目,有从老旧的 2008 R2 迁的,也有从 2016 标准版迁的,数据量从几十 G 到几个 T 都碰过。这个过程里踩过的坑、填过的洞、总结出的经验,我觉得值得写出来。这篇文章不是给你一个工具链接,而是给你一套完整的迁移方法论,从为什么迁、怎么迁、迁完怎么验证,到最后怎么调优,全部讲透。
1. 迁移前的全局认知:SQL Server 和 MySQL 的本质差异
很多人一上来就问“我该怎么导数据”,这是典型的思路错误。迁移的第一件事不是操作数据库,而是先搞清楚你要面对的两个数据库到底有多不一样。这就像搬家,你不可能直接从旧房子把所有家具堆到新房子,你得先量好新房子的大门尺寸、房间布局,再决定哪些家具可以保留、哪些必须拆掉重装。
1.1 为什么需要迁:商业授权之外的真实考量
先说动机。大部分公司选择从 SQL Server 迁到 MySQL,第一驱动力确实是成本——SQL Server 的商用授权费在很多中小企业里不是一笔小数目,尤其是当业务量上来之后需要做读写分离、高可用集群时,标准版和企业版的费用差距会让你想骂人。MySQL 社区版免费,Percona 分支也对生产环境很友好。
但除了钱,还有几个更实际的理由:
- 技术栈统一:如果团队本身就在用开源体系,比如 Java + Spring Cloud + Redis + Kafka,数据库层面用 MySQL 可以统一运维工具链、监控体系和中间件生态。比如你用 Prometheus 监控 MySQL 就很成熟,但对 SQL Server 的监控就要额外部署专门的 Exporter,配置复杂度完全不同。
- 生态工具链成熟:MySQL 周边有非常成熟的 Binlog 解析、读写分离中间件、分库分表组件,而 SQL Server 在某些场景下(比如分库分表)提供的原生方案比较重,或者需要额外的商业工具支持。
- 部署灵活性:MySQL 轻量,在 Docker 化、K8s 化的部署场景下非常友好;SQL Server 在容器里的表现近年来有进步,但内存管理、许可证绑定等问题还是让人头疼。
不过迁之前一定要清醒认识一点:从 SQL Server 迁 MySQL 不是一次简单的版本升级,而是一次架构级改造。你面对的是 T-SQL 和 MySQL SQL 两种方言的差异、事务隔离级别的差异、索引结构的差异、甚至日期处理方式的差异。做好了,后续维护成本大幅下降;做不好,线上事故分分钟教你做人。
1.2 语法与特性的隐性差异(重点讲数据类型、函数、分页、事务隔离)
这是真正决定你迁移工作量的部分。很多人以为导完数据就算迁完了,结果应用层一启动,报表接口、分页查询、存储过程全部报错,才发现麻烦刚开始。下面这些差异是我在做迁移时最先关注的点。
数据类型映射是最基础的差异,也是最容易被忽视的坑。
SQL Server 的DATETIME精度是 3.33 毫秒,MySQL 的DATETIME(不带括号)精度是秒。如果业务里有毫秒级时间戳需求,比如订单创建时间、日志记录时间,迁移时必须把目标字段定义为DATETIME(3),否则数据会被静默截断。我见过一个统计报表系统,因为时间精度丢失,导致某个时段的交易笔数对不上,排查了整整两天。
BIT类型在 SQL Server 里是 0/1,MySQL 里也是 0/1,看似一致,但如果你在 SQL Server 里有BIT字段参与算术运算,迁移后行为会因为 MySQL 的 0/1 行为不完全一致而出错。更稳妥的做法是用TINYINT(1)替代BIT。
NVARCHAR在 SQL Server 里是 Unicode 字符串,长度以字符数计算;MySQL 的VARCHAR长度也是以字符数计算,但字符集由表的默认字符集决定。如果你的表是utf8mb4,那VARCHAR(50)能存 50 个汉字,和 SQL Server 的NVARCHAR(50)行为基本一致。但如果你用的字符集是utf8(不是utf8mb4),那VARCHAR(50)只能存 50 个字节,也就是 16 个汉字左右,直接产生截断错误。
分页查询是应用层改造里最明显的一道坎。
SQL Server 的经典写法是:
SELECT * FROM Orders ORDER BY OrderId OFFSET 20 ROWS FETCH NEXT 10 ROWS ONLY;MySQL 从 8.0 开始支持OFFSET ... FETCH语法,但如果你还在用 5.7 及以下版本,只能写成:
SELECT * FROM Orders ORDER BY OrderId LIMIT 10 OFFSET 20;看起来只是语法差异,但性能差别很大。MySQL 的深分页问题(比如LIMIT 100000, 20)非常明显,因为它是先读 100020 行再丢弃前 100000 行。如果原 SQL Server 系统大量使用深分页并依赖OFFSET FETCH语法,迁移后建议顺手把分页查询改造为基于游标或基于上次最后 ID 的遍历方式,这样性能提升非常可观。
函数差异会让存储过程几乎全部重写。
SQL Server 的函数命名和执行语义与 MySQL 差异很大。比如:
ISNULL(a, b)在 SQL Server 里很常用,MySQL 对应的是IFNULL(a, b),但IFNULL只接受两个参数,如果嵌套使用多个默认值,逻辑要调整。GETDATE()在 SQL Server 里取当前时间,MySQL 对应NOW(),但 SQL Server 的GETDATE()返回DATETIME精度到毫秒,而NOW()默认到秒,不要混用。LEN()在 SQL Server 里算字符串长度,MySQL 对应CHAR_LENGTH()(字符数)。如果你用LENGTH()会得到字节数,utf8mb4下一个汉字是 3 字节,结果完全不对。- 字符串拼接:SQL Server 用
+拼接,MySQL 用CONCAT(),但在 MySQL 里用+实际上做的是加法运算,遇到非数字字符串会转成 0,结果是极其隐蔽的逻辑错误。 - 日期格式化:SQL Server 用
CONVERT(VARCHAR, date, 120),MySQL 用DATE_FORMAT(date, '%Y-%m-%d %H:%i:%s'),逻辑相似但语法完全不同。
事务隔离级别的差异会影响并发行为的判断。
SQL Server 默认隔离级别是READ COMMITTED,MySQL InnoDB 默认是REPEATABLE READ。这会导致一个非常隐蔽的问题:在 MySQL 默认隔离级别下,同样的 SQL 语句,读到的数据快照可能和 SQL Server 下不一样。比如在一个事务内先查订单状态,然后其它会话修改了该订单,再查一次,SQL Server 的READ COMMITTED会返回新值,MySQL 的REPEATABLE READ仍返回旧值。如果业务逻辑依赖“事务内读取最新已提交数据”,迁移后行为会变化。需要在连接池或 JDBC 连接串级别调整transaction-isolation-level,建议把关键业务连接设为READ COMMITTED来对齐 SQL Server 的行为。
2. 迁移方案选型:全量、增量还是同步工具
搞清楚了两边数据库的差异之后,下一步就是选方案。不同数据量级、不同停机窗口的容忍度,决定了你要用哪种迁移路径。很多教程只告诉你“用工具导数据”,完全不提迁移方案的选型逻辑,这是会出大问题的。
2.1 三种主流迁移模式的取舍
我把常见迁移模式归纳为三种:一次性全量迁移、全量+增量迁移、逐步切换的双写迁移。
一次性全量迁移适用于数据量较小(比如 100GB 以内)、业务可以接受数小时停机窗口的场景。做法是在停机窗口内停应用,导出 SQL Server 全量数据,导入 MySQL,验证数据完整性,然后切换连接串上线。这是最直观的方式,但对停机时间要求苛刻。
全量+增量迁移适用于数据量较大(几百 GB 到几 TB)、允许短暂停机(比如周末凌晨)但不想从零导入全部数据的场景。思路是先在正式切换前完成全量数据的导入,然后通过解析 SQL Server 的事务日志或者比对 Binlog(如果源库有捕获机制)持续同步增量变更,直到切换那一刻把增量补齐。
双写迁移是要求高可用、几乎不允许业务中断的场景。做法是应用层同时写入 SQL Server 和 MySQL,以 SQL Server 为基准数据源,先迁移历史数据到 MySQL,再通过一段时间的双写和定期比对让两边数据最终一致。这个方案实现复杂度最高,但对业务无感,适合银行、交易等场景。
我个人的建议是:如果你的业务允许停机,直接用全量+增量的中间方案。双写迁移听上去很稳妥,但应用层改动面积巨大,而且 MySQL 和 SQL Server 的事务行为不完全一致,双写造成的脏数据比对本身就是一大负担。现实中真正双写成功的案例很少,大多数最后都退化成“只切一次、依赖一致性校验”。
2.2 迁移工具对比与选择(mysqldump、数据同步中间件、脚本化方案)
工具永远是辅助,你得先理解它做到了什么、没做到什么。
mysqldump是全量迁移的基础工具。导出时建议加上几个关键参数:
mysqldump -u root -p --single-transaction --routines --triggers --events --databases your_db > your_db.sql如果你不写--single-transaction,MySQL 会在导出时对 MyISAM 表加锁,对 InnoDB 表不加锁但可能产生不一致快照。--routines和--triggers如果不带,存储过程、函数、触发器全不导出,非常容易忽略。还有一个容易被忽略的参数是--set-gtid-purged=OFF,如果你的目标 MySQL 启用了 GTID,导入的时候不带这个参数会导致 GTID 冲突。
SQL Server 侧导出不依赖特定工具,直接用 bcp 或者 SSMS 的导出向导把数据导出为 CSV,然后再用LOAD DATA INFILE导入 MySQL。这里有个经验:千万不要直接从 SQL Server Management Studio 里选中结果集然后右键“另存为 CSV”,用这个方式导出的 CSV 经常带 BOM 头,导入 MySQL 后第一列数据会多一个特殊字符,导致对比失败。
数据同步中间件方面,目前比较成熟的开源方案有基于 Binlog 解析的异构同步工具。这类工具一般支持 SQL Server 作为源端,配置好连接信息后,它能捕获源库的数据变更并同步到 MySQL。但这类工具对 SQL Server 的版本兼容性、对 DDL 变更的支持程度、对大数据量事务的吞吐能力都存在差异,选型时一定要先在测试环境压测。重点看三个维度:延迟指标、DDL 同步能力、事务一致性保证。
脚本化方案,也就是你自己写一个同步程序。如果数据模型非常简单、不做复杂表关联、表结构变更频率低,自己写脚本反而是最可控的。但一旦业务涉及几十张表、触发器、视图依赖链,写脚本的维护成本就爆炸了,不如直接用开源中间件。
3. 迁移实施的核心环节:从表结构到数据的完整落地
方案定了,工具选了,现在才进入真正动手的环节。这一章我按“先表结构、再数据、后对象”的顺序,把迁移实施拆开讲透。
3.1 表结构与数据类型的映射
在真正导出数据之前,先建立映射关系文档。把所有 SQL Server 的表、字段、类型、约束、默认值、索引、主外键关系整理出来,转换到 MySQL 对应的类型定义。这一步是迁移的“设计图纸”,别跳过。
我建议用下面的映射作为起点来制定你自己的对照表:
| SQL Server 类型 | MySQL 类型 | 备注 |
|---|---|---|
| INT | INT | 直接映射 |
| BIGINT | BIGINT | 直接映射 |
| SMALLINT | SMALLINT | 直接映射 |
| TINYINT | TINYINT | SQL Server 的 TINYINT 范围是 0-255,MySQL 的 TINYINT 范围是 -128-127,如果原字段存了超过 127 的值,必须改用 SMALLINT |
| DECIMAL(p,s) | DECIMAL(p,s) | 直接映射,但注意精度参数要一致 |
| FLOAT | FLOAT | 直接映射 |
| DATETIME | DATETIME(3) | 如果源数据有毫秒精度,必须加括号参数 |
| DATETIME2 | DATETIME(3) | 源库可能有更高的精度,目标库支持到微秒(6位),按业务需要取舍 |
| DATE | DATE | 直接映射 |
| TIME | TIME | 直接映射,注意 SQL Server 的 TIME 精度默认是 100ns,业务只需秒级就映射为 TIME |
| NVARCHAR(n) | VARCHAR(n) | 注意目标库表字符集必须为 utf8mb4 |
| VARCHAR(n) | VARCHAR(n) | 注意源库 VARCHAR 是字节计数非字符计数,如果源库存了中文,目标库长度要放宽 |
| TEXT | TEXT | 直接映射 |
| NTEXT | TEXT | SQL Server 的 NTEXT 被标记为废弃,MySQL 用 TEXT 对应即可 |
| IMAGE | LONGBLOB | 二进制数据使用 LONGBLOB |
| UNIQUEIDENTIFIER | CHAR(36) 或 VARCHAR(36) | MySQL 没有原生的 UUID 类型,适合用 CHAR(36) 保存带横杠的 UUID 字符串 |
| MONEY | DECIMAL(19,4) | MONEY 是定点数,DECIMAL 能保证金额不丢精度 |
| BIT | TINYINT(1) | 替代方案,避免 MySQL BIT 类型在 ODBC/JDBC 驱动下行为不一致 |
| XML | TEXT 或 JSON | 如果业务依赖 XML 函数,建议转 JSON 后用 MySQL JSON 类型 |
这个映射表不是一个可执行的 DDL 生成器,而是一个“翻译基准”。你用它来指导建表脚本的编写,不能指望自动转换工具帮你做类型改写,因为工具不会理解业务语义。
3.2 脚本改写与存储过程兼容性处理
这是整个迁移中最费力的一环,也是最容易低估工作量的一环。凡是用了 SQL Server 存储过程、视图、触发器的系统,迁移时都要逐一改写。
我总结出几个高频改写模板,直接拿去改就行。
TOP 与 LIMIT 的改写
SQL Server 的SELECT TOP 10 * FROM users在 MySQL 里对应SELECT * FROM users LIMIT 10。但TOP在 SQL Server 里可以带PERCENT,比如TOP 10 PERCENT,MySQL 的LIMIT不支持百分比,需要走到用子查询算总量或者用ROW_NUMBER()窗口函数替代。
IF EXISTS 语法
SQL Server 里判断记录是否存在常用的写法是:
IF EXISTS (SELECT 1 FROM orders WHERE order_id = 100) BEGIN -- do something ENDMySQL 的存储过程里没有IF EXISTS的用法,需要用IF (SELECT COUNT(*) FROM orders WHERE order_id = 100) > 0 THEN,或者在存储过程外先查一次再判断。注意COUNT(*)在大表上性能堪忧,更建议用EXISTS子查询配合SELECT 1:
IF EXISTS (SELECT 1 FROM orders WHERE order_id = 100) THEN -- do something END IF;MySQL 8.0 支持IF EXISTS作为过程语句的一部分,但很多团队还是停留在 5.7,所以我按 5.7 兼容写法来。
游标处理
SQL Server 的游标声明和取数的写法与 MySQL 差异很大。在 MySQL 存储过程中处理游标的写法是:
DECLARE cur CURSOR FOR SELECT id, name FROM users; DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = TRUE; OPEN cur; read_loop: LOOP FETCH cur INTO v_id, v_name; IF done THEN LEAVE read_loop; END IF; -- process each row END LOOP; CLOSE cur;这个CONTINUE HANDLER FOR NOT FOUND是 MySQL 存储过程最重要的控制机制,很多人第一次写都漏掉它,导致游标循环死循环读取最后一条记录。SQL Server 里游标用@@FETCH_STATUS判断是否还有数据,机制完全不同,改写时务必测试边界情况。
动态 SQL
SQL Server 的动态 SQL 用EXEC或sp_executesql,MySQL 里是PREPARE、EXECUTE、DEALLOCATE PREPARE。如果原来的存储过程大量使用动态 SQL,改写时要注意 MySQL 的预编译语句在存储过程内不能直接用变量做表名,必须拼接字符串。拼接的时候一定用CONCAT()而不是直接加号,这个错误极其隐蔽。
3.3 数据导出导入与字符集检查
表结构建好后,就是数据的搬运。这个环节的关键词是“字符集错位”,90% 的迁移后乱码问题都出在字符集配置上。
导出时的字符集设置
如果你的 SQL Server 侧数据是简体中文,导出 CSV 时建议设置编码为UTF-8。但 SSMS 里默认导出 CSV 是 ANSI 编码(Windows 下通常是 GBK),直接导入 MySQL 后中文乱码。解决方法是在导出时用 bcp 命令指定编码:
bcp "SELECT * FROM your_database.dbo.users" queryout "users.csv" -c -C 65001 -t "," -r "\n" -S server_name -U user -P password-C 65001是代码页 65001,代表 UTF-8。如果不用 bcp,你也可以导出后在 Linux 上执行iconv -f GBK -t UTF-8 users.csv > users_utf8.csv做一次转码,也能解决问题。
导入时的参数设置
MySQL 导入大批量数据最推荐用LOAD DATA INFILE,速度远超逐条INSERT。导入前把表上的索引和约束临时去掉,导入完成后重新建立,能省好几个小时:
SET FOREIGN_KEY_CHECKS = 0; SET UNIQUE_CHECKS = 0; SET sql_mode = ''; LOAD DATA INFILE '/tmp/users.csv' INTO TABLE users FIELDS TERMINATED BY ',' ENCLOSED BY '"' LINES TERMINATED BY '\n' IGNORE 1 LINES (id, name, email, created_at); SET UNIQUE_CHECKS = 1; SET FOREIGN_KEY_CHECKS = 1;导入时另一个重要参数是sql_mode。如果你的源数据里日期字段包含0000-00-00,默认 MySQL 的严格模式会直接拒绝导入。把sql_mode设为空,或者至少移除NO_ZERO_DATE和NO_ZERO_IN_DATE,才能让导入顺利完成。
字符集检查清单
建完库后第一步不是导数据,而是检查目标库字符集:
SHOW CREATE DATABASE your_db; SHOW CREATE TABLE users;确认DEFAULT CHARACTER SET = utf8mb4,COLLATE = utf8mb4_general_ci或utf8mb4_unicode_ci。我推荐utf8mb4_unicode_ci,排序规则更符合 Unicode 标准,虽然处理速度稍慢于general_ci,但数据准确性更重要。
4. 迁移后的验证与调优
数据导完了,连接串改了,业务看起来跑起来了,很多人就以为大功告成。但数据库迁移真正的工作量在验证和调优这一阶段。数据有没有丢、有没有多、有没有错、SQL 跑得慢不慢,这些都在上线后第一个晚上集中爆发。
4.1 数据一致性校验方法(不只靠 COUNT)
最基础也是最不可靠的校验方式是COUNT(*)对比。两张表行数一致不代表数据一致,因为导入过程中可能因为类型转换把两个不同的值变成了同一个值(比如日期精度丢失、字符串截断),行数还是那么多数,但内容已经错了。
我常用的校验思路有三种:
校验和比对(Checksum 比对),对每张有唯一主键的表,算出关键列的 MD5 校验和,两边分别算了一遍再对比结果。SQL Server 侧可以写一段 T-SQL 用HASHBYTES('MD5', CONCAT(...))算出每行摘要,然后聚合;MySQL 侧用MD5(CONCAT(...))。两边聚合结果一致,基本能断言数据一致。这个方案对大表耗时较久但准确率高。
抽样比对加业务规则校验,适合大表。随机抽取一定比例的主键,两边查出来逐列比对。同时找出业务上能印证数据完整性的规则,比如总订单金额、用户总数、最新一条记录的时间戳等。这些“业务断点”可以快速暴露方向性错误。
主键序列校验,看自增主键的最大值和连续性。如果 SQL Server 主键是IDENTITY,导到 MySQL 后建议保留原值,不要重新自增。方法是建表时不指定AUTO_INCREMENT,插入全部数据后再用ALTER TABLE users AUTO_INCREMENT = 原始最大值 + 1补上。
4.2 索引与 SQL 语句的适配优化
迁移后性能变差是常态,尤其是原来依赖 SQL Server 优化器的查询计划已经按源库的统计信息和索引结构做优化,换到 MySQL 后一切都得重新来。
索引适配的第一个动作是检查丢失索引。
把 SQL Server 原库里的索引清单导出,比对目标库。但不要照搬,因为 MySQL 的索引策略和 SQL Server 不一样。比如 SQL Server 的INCLUDE列在 MySQL 里没有对应概念,MySQL 的辅助索引天然包含主键列。如果原来的非聚集索引是为了覆盖查询,你需要手动把INCLUDE列并进索引列中。
-- SQL Server CREATE INDEX idx_orders_user ON orders(user_id) INCLUDE (status, total_amount); -- MySQL 要写成 CREATE INDEX idx_orders_user ON orders(user_id, status, total_amount);注意 MySQL 对索引列总长度有限制(InnoDB 单索引长度默认 3072 字节),如果原来 SQL Server 的复合索引里含有超长VARCHAR列,需要拆掉或者用前缀索引(column(20))代替。
SQL 语句的适配优化必须回归到慢查询日志。
上线前压测阶段开启慢查询日志:
slow_query_log = 1 slow_query_log_file = /var/log/mysql/slow.log long_query_time = 1跑一轮完整业务回归后,直接查慢日志,把每个慢 SQL 拿出来用EXPLAIN分析。最典型的问题有三个:
- 原来 SQL Server 用
FORCESEEK或WITH (INDEX(...))强制索引的地方,MySQL 用FORCE INDEX(idx_name)。但不要盲目强制索引,MySQL 的优化器在某些条件下会比 SQL Server 更智能,先跑没有强制索引的版本,必要时再加FORCE INDEX。 - 隐式类型转换会导致索引失效。比如 MySQL 里
WHERE user_id = '123'在user_id是 INT 类型时不会走索引(因为字符串和 INT 比较时,MySQL 会将字符串转成数字,这本身不是索引失效的原因,但如果列上有函数或者语法不同,情况会变)。比较稳妥的做法是应用层保证参数类型与列类型一致。 - 排序和分组用了
filesort。MySQL 对ORDER BY的处理经常出现Using filesort,如果排序字段是组合索引的一部分,调整索引顺序能让排序直接走索引而不是临时文件。
5. 常见问题与排查经验实录
最后这部分是真正的“避坑指南”。以下问题我几乎在每次迁移中都会遇到,把它们整理成一个速查表,你遇到相同问题可以直接对照排查。
5.1 数据类型转换引发的隐蔽错误
自增主键溢出问题。如果源库某张表数据量超过 21 亿行,INT自增主键在 MySQL 里可能不够用。SQL Server 的INT最大到 2,147,483,647,MySQL 也是这个范围,但要考虑 MySQL 的AUTO_INCREMENT机制在批量插入和回滚时不会回收已分配的值,也就是说实际可用的 ID 会比最大值更早耗尽。预防方法是建表时直接用BIGINT AUTO_INCREMENT。
UNSIGNED 整型带来的比较陷阱。SQL Server 没有无符号整型,MySQL 里INT UNSIGNED可以存到 42 亿。如果原来的业务判断“小于 0 的 id 是错误数据”,迁到 MySQL 无符号整型后,条件WHERE id < 0永远不成立,逻辑失效。建表时如果不需要负数,建议不用UNSIGNED,保持有符号类型。
5.2 自增主键与事务隔离的差异处理
MySQL 的AUTO_INCREMENT在批量插入时先分配一段值,如果事务回滚,这一段值就浪费了,后续插入从更大的 ID 开始。SQL Server 的IDENTITY也有类似行为,但差距在于 MySQL 的默认隔离级别是REPEATABLE READ,批量插入期间其它会话看不到未提交的数据,可能导致 ID 空洞更明显。业务上如果对主键的顺序有依赖,尽早调整并使用innodb_autoinc_lock_mode参数控制。
5.3 字符集与乱码问题
导入后发现乱码,先用SELECT HEX(name) FROM users WHERE id = 1查看数据的十六进制编码。如果是E4B8AD(“中”字的 UTF-8 编码),说明数据本身没问题,是客户端连接没设置好字符集。如果是D6D0(“中”字的 GBK 编码),说明源导出的文件就是 GBK,导入前没有正确转码。
排查时检查三个位置:
提示:乱码问题 90% 是连接层字符集没设对,而不是数据本身坏了。JDBC 连接串一定要加
characterEncoding=utf8参数,且连接建立后执行SET NAMES utf8mb4确认会话级字符集。
5.4 存储过程迁移后的性能断崖
存储过程在 SQL Server 下执行得飞快,迁到 MySQL 后突然慢了十倍,这种案例太多了。原因通常是 MySQL 的优化器对复杂查询的处理能力和 SQL Server 不同,特别是包含多个子查询嵌套、多个OR条件、跨表关联时达到优化器能处理的复杂度上限就会退化为全表扫描。
建议是把复杂存储过程拆分到多个小的存储过程,或者直接用视图 + 物化表的方式替代。MySQL 的综合能力在 OLTP 场景做简单的单表/少表查询非常出色,但涉及上百行的复杂 T-SQL 存储过程,迁移后都要考虑在应用层用多条简单查询组装,而不是在数据库层干大活。
5.5 迁移后业务接口超时的排查路径
上线后第一批报警通常是接口超时。不要急着加索引,先按以下路径排查:
- 先从慢查询日志里确认是不是真的有慢 SQL,很多时候慢的不是 SQL 而是应用服务器故障转移。
- 再确认连接池配置。SQL Server 连接池和 MySQL 连接池的参数语义不一样,如果原来连接池的
maxActive照搬过来但 MySQL 的max_connections没调大,高并发下会直接拒绝连接。 - 最后看主从延迟。如果你迁移后做了主从架构,业务读写没有分离,或者读流量走了从库但延迟没监控到,数据不一致也会导致应用层重试风暴,接口灵异超时往往就是这个问题。
一些额外的经验沉淀
做迁移这些年,我最大的体会是:数据库迁移这个事,方案选型只占两成,剩下八成是执行力——细致的类型映射、严格的字符集检查、全面的 SQL 改写、可靠的数据校验。每一项都不复杂,但每一项都必须做到位。
我个人在实际项目中最后还会做一件事:迁移后的第 30 天之内保持源库 SQL Server 的备份和数据库实例在线,不要急着释放资源。这不是不信任迁移结果,而是给自己留一个回滚的余地。数据库迁移没有“绝对成功”,只有“验证充分”的说法。两个库并行观察一个业务周期,确认所有报表、批处理、定时任务都稳定了,再逐步下线源库资源。这份余量,能让你在遇到未知问题时从容应对,而不是半夜三点朝着手机发呆。