做数据库迁移这活儿,最怕的不是技术难度,而是"以为很简单"的心态。SQL Server迁到MySQL,表面上都是关系型数据库,表结构、SQL语法看着也差不多,可真动手的时候,坑全藏在细节里:类型映射、排序规则、自增列、存储过程语法、连接串驱动,随便一个没顾及到,就能让上线演练变成通宵排错。这篇指南就是把我实际搬迁过多次SQL Server到MySQL的完整流程、踩过的坑和顺手沉淀下来的工具方法整理出来,给正在做或准备做数据库迁移的同学一个可以直接参考的路线图。
我会从迁移前的规划讲起,把策略选型、资产摸底、数据类型差异、实操工具、大表优化到事后验证一个一个过一遍。不管你是几十张表的小业务,还是几百张表带存储过程、触发器、定时作业的老系统,这套方法论都能套得上。尤其是那些"文档里查不到"的隐性差异,我会专门拉出来说,这些才是决定迁移成败的关键。
1. 迁移前的整体规划与核心思路
1.1 先搞清楚"为什么迁"和"迁什么"
很多人一上来就问"用什么工具导数据",其实这是个次序问题。工具只是执行层,迁移之前必须先把动机和边界理清楚。我接触过的迁移项目,动机大致分三类:
- 成本驱动:SQL Server按CPU、按核数授权,企业版动辄几十万,MySQL开源免费,省下的license钱能覆盖整个迁移项目的投入。
- 技术栈统一:团队整体转向Linux + 开源生态,数据库跟着换,方便运维体系、监控告警、CI/CD全部拉齐。
- 业务拆分与重构:老系统要拆微服务,数据库顺便从SQL Server换到MySQL,一次性把历史包袱清掉。
动机决定了迁移的深度。如果只是"换个库把数据搬过去",那关注点集中在表结构和数据本身;如果是借着迁移做重构,那存储过程、视图、定时作业、甚至应用层的ORM映射都要一并翻新。我建议在立项时就把"迁移范围清单"定下来,避免做到一半发现作业、触发器没纳入范围,又回头补工单。
另外还有一件事要提前确认:目标MySQL的版本和部署方式。MySQL 5.7和8.0在SQL模式、排序规则、窗口函数支持上差异不小,8.0默认的utf8mb4_0900_ai_ci排序规则在5.7上根本不认识。如果生产环境是8.0,本地测试却用5.7,迁移脚本里凡是涉及collation的都容易踩坑。建议以生产目标版本为准做开发,不要赌"版本差不多就没事"。
1.2 迁移策略选型:停机搬迁、双写同步还是渐进式迁移
迁移策略主要看业务对停机时间的容忍度,我梳理成三档:
一次性停机搬迁:适合内部系统、月底跑批类业务,或者允许在凌晨维护窗口内停服4到6小时。流程简单,先把全量数据和对象迁过去,应用切换连接串即可。SQL Server导出、MySQL导入,切换后在维护窗口内验证,成功则收工,失败则立即回切。这种模式最省事,风险也可控。
双写加增量同步:适合核心交易类系统,停机窗口只有几十分钟甚至不允许停机。做法是先做全量迁移,然后在应用层同时写SQL Server和MySQL,或者通过日志解析工具做增量同步,最后在切换窗口内完成增量追平并切换。双写方案的复杂度在于一致性保证和冲突处理,通常还要配套一套对账脚本。
渐进式迁移:适合多模块大系统,按业务域分批迁。比如先把用户、订单等非核心模块迁过去,观察稳定后再迁核心模块。这种模式周期长,期间需要维护两套数据库之间的数据同步,但单次发布风险小,回滚也容易。
我个人在中小项目里用得最多的是第一种,理由很实际:大多数业务的停机窗口比想象中大,而且一次性迁移的排错成本远低于长期双写维护的成本。如果你的业务确实无法接受长停机,再考虑第二种,但要做好对账和回滚方案,不能只盯着"数据能同步过去"这一件事。
1.3 上线前的资产摸底清单
很多迁移翻车都是因为"不知道库里到底有什么"。SQL Server实例里除了业务表,还有作业、链接服务器、数据库邮件、SSRS报表数据源、甚至没人维护的临时库。我每次迁移前都会做一张资产摸底表,逐项确认:
- 表、索引、视图、存储过程、触发器、函数、自定义类型。
- SQL Server Agent作业及其调度计划、步骤内容,这些里可能藏着核心跑批逻辑。
- 外键约束、默认值约束、检查约束、唯一约束,导出结构时容易漏。
- 连接串分布,需要扫描配置文件、Web.config、appsettings.json,找出所有指向SQL Server的连接。
- 数据库大小和增长趋势,用于估算迁移窗口和数据传输时间。
- 敏感字段和加密逻辑,涉及列级加密的话MySQL方案完全不同。
摸底工具方面,SSMS自带的"生成脚本"向导可以一次把表结构和对象脚本导出,但对大库来说脚本体积会很大,建议按对象类型分批导;表清单可以用系统视图查询,比如sys.tables、sys.procedures、sys.triggers、sys.views,把对象名、类型、创建时间列出来核对。这一步虽然枯燥,但能避免迁移当天手忙脚乱。
2. 数据类型与语法差异对照,迁移前必须做好的功课
2.1 数据类型映射对照表与典型坑位
SQL Server和MySQL的数据类型体系整体相似,但细节差异不少。我把常用的映射关系整理成一张表,迁移改结构脚本的时候直接对照着改就行:
| SQL Server | MySQL | 迁移建议 |
|---|---|---|
| INT | INT | 直接对应 |
| BIGINT | BIGINT | 直接对应 |
| SMALLINT / TINYINT | SMALLINT / TINYINT | 直接对应 |
| DECIMAL(p,s) | DECIMAL(p,s) | 直接对应,注意p和s的精度定义一致 |
| MONEY / SMALLMONEY | DECIMAL(19,4) / DECIMAL(10,4) | MONEY存储为DECIMAL,避免浮点误差 |
| FLOAT / REAL | FLOAT / DOUBLE | 对应,注意精度损失场景 |
| CHAR(n) | CHAR(n) | 对应,注意Oracle式的定长空格填充在MySQL中也存在 |
| VARCHAR(n) | VARCHAR(n) | 直接对应;注意SQL Server中VARCHAR(n)的n是字符数,MySQL中VARCHAR(n)的n也是字符数,这个不坑 |
| NVARCHAR(n) | VARCHAR(n) | NVARCHAR按字符存储,MySQL用utf8mb4字符集时VARCHAR(n)同样按字符,可以安稳映射;但如果含大量中文,注意utf8mb4下每个中文字符最多占4字节,长度上限应根据行大小测算 |
| TEXT / NTEXT | TEXT / LONGTEXT | 大文本用LONGTEXT,最大4GB |
| IMAGE / VARBINARY(MAX) | LONGBLOB | 二进制大对象用LONGBLOB |
| DATETIME | DATETIME | SQL Server DATETIME精度约3.33ms,MySQL DATETIME默认精度0秒,建议迁移时指定DATETIME(3)对齐 |
| DATETIME2(p) | DATETIME(p) | 精度对齐,如DATETIME2(7)对应DATETIME(6),MySQL最高微秒精度其实是6位 |
| SMALLDATETIME | DATETIME | 直接对应,注意SMALLDATETIME精度到分钟,MySQL若用DATETIME(3)反而精度高了,不冲突 |
| DATE / TIME | DATE / TIME | 直接对应 |
| UNIQUEIDENTIFIER | CHAR(36) / VARCHAR(36) | MySQL没有原生GUID类型,一般用CHAR(36)存字符串;也有一部分人用BINARY(16),读取时要转换,会增加复杂度 |
| BIT | TINYINT(1) | 直接映射成BOOL或TINYINT(1),无论PHP、Java还是.NET驱动都能兼容 |
| XML | JSON / LONGTEXT | SQL Server的XML类型建议应用层改造为JSON或TEXT |
| ROWVERSION / TIMESTAMP | 无对应 | 建议迁移前在SQL Server侧去掉该列,由应用层或触发器实现版本控制 |
| GEOGRAPHY / GEOMETRY | GEOMETRY(MySQL空间扩展) | 功能不完全对齐,复杂空间运算需要改造 |
| SQL_VARIANT | 无对应 | 不迁移,需改造字段类型 |
这张表里我特别想强调两个坑。一个是DATETIME精度,SQL Server的DATETIME底层实际精度是1/300秒,也就是0、3、7毫秒这种值,迁到MySQL如果用默认的DATETIME(0),秒以下的时间会被抹掉,两个库的数据对不上。解决办法是迁移时显式用DATETIME(3)甚至DATETIME(6),把精度对齐。另一个是NVARCHAR映射,SQL Server里NVARCHAR是真正的Unicode存储,MySQL里只要库表字符集是utf8mb4,VARCHAR也能按字符存中文和emoji,所以映射本身没问题,但要注意单行大小限制——InnoDB默认单行最大约65535字节(具体取决于页大小和行格式),一个VARCHAR(5000)在utf8mb4下实际可能超过20000字节,几个大字段一组合就会触发行大小限制,建表时报"Row size too large"。
2.2 字符集、排序规则与时区差异
字符集是SQL Server迁MySQL最容易出鬼的地方。SQL Server默认排序规则常见的是Latin1_General_CI_AS或Chinese_PRC_CI_AS,MySQL这边最佳实践是库表字符集一律用utf8mb4,排序规则建议utf8mb4_general_ci或utf8mb4_0900_ai_ci。直接原因很简单:utf8mb4是真正的四字节UTF-8,能存全量Unicode字符,包括emoji和生僻汉字。SQL Server的NVARCHAR本身就是Unicode存储,迁过来如果不注意字符集,中文显示成问号、表情符号直接报错的情况我见过太多次。
排序规则的影响更隐蔽。utf8mb4_general_ci和utf8mb4_0900_ai_ci在排序权重上略有差异,如果用0900系列,MySQL 8.0特有,如果目标库是5.7,建库语句直接报"Unknown collation"。另外,MySQL的排序规则默认不区分大小写,但表字段可以单独指定BINARY或utf8mb4_bin,迁移时如果原系统对大小写敏感有依赖,就需要逐字段确认。我的建议是:统一用utf8mb4_general_ci跑线上,如果业务确实需要精确大小写匹配,在具体字段上加COLLATE utf8mb4_bin,不要为了个别查询把整库的排序规则改掉,不然后患无穷。
还有一个经常被忽略的是时区。SQL Server的GETDATE()返回服务器本地时间,MySQL的NOW()返回的是会话时区对应的时间。如果应用服务器和数据库服务器不在同一时区,或者MySQL全局time_zone设置不对,时间字段会整批偏移。迁移前要确认MySQL的global time_zone设置,建议显式设为'+08:00'或'SYSTEM'并确保系统时区正确。应用连接串里也应该加上serverTimezone参数(JDBC驱动)或TimeZone参数(.NET驱动),避免驱动层再转一次时区导致双重偏移。
2.3 存储过程、视图、触发器与作业的语法改造
结构迁移里最费人工的就是存储过程、触发器和作业。原因很直白:SQL Server的T-SQL和MySQL的存储过程语法虽然有SQL标准打底,但在变量声明、游标、错误处理、临时表这些地方差异很大,没法一键自动转换。
我列一下常见的语法差异点:
- 批处理符号:SQL Server用GO分隔批次,MySQL没有GO,需要去掉。
- 变量声明:SQL Server用DECLARE @var INT,MySQL用DECLARE var INT,且MySQL的DECLARE只能在BEGIN...END块开头集中声明,不能像T-SQL那样在过程体中间随手声明。
- 字符串拼接:T-SQL用+,MySQL用CONCAT()函数,一旦拼接涉及NULL,两者语义不同(T-SQL中NULL + 'abc'结果还是NULL,MySQL中CONCAT(NULL,'abc')结果是NULL,但如果用CONCAT_WS需另行处理)。
- 临时表:SQL Server的#tempTable,MySQL用CREATE TEMPORARY TABLE,作用域和生命周期不同,迁移时要小心。
- 游标:两者语法接近但FETCH NEXT FROM语法有差异,需要逐条调整。
- 错误处理:SQL Server用TRY...CATCH,MySQL用DECLARE ... HANDLER FOR SQLEXCEPTION,写法差别很大,涉及事务回滚逻辑的地方要重写。
- 分页:SQL Server用OFFSET...FETCH NEXT,MySQL用LIMIT offset,count,注意LIMIT的语义是"跳过offset行取count行",两者等价但写法不同。
- 默认值函数:SQL Server的GETDATE()、NEWID(),MySQL对应NOW()、UUID(),建表语句里要替换。
- 动态SQL:SQL Server的EXEC(@sql)和sp_executesql,MySQL用PREPARE、EXECUTE、DEALLOCATE PREPARE,参数化方式差异明显。
- 字符串函数:ISNULL()改成IFNULL()或COALESCE(),LEN()改成CHAR_LENGTH(),REPLICATE()改成REPEAT(),CHARINDEX()改成LOCATE()或INSTR()。
视图的改造相对简单,主要就是上述函数和分页语法的替换。触发器要注意MySQL的触发器语法在FOR EACH ROW之外没有SQL Server那种INSTEAD OF / AFTER的完全对应,且同一事件同一时机只允许一个触发器,如果原库同一个表上有多个AFTER INSERT触发器,迁移时要么合并,要么改造。作业(SQL Server Agent Job)最麻烦,MySQL没有原生调度器,通常用两种方案替代:一是部署在Linux上用crontab调mysql命令执行存储过程或脚本,二是在应用侧使用Quartz或xxl-job这类任务调度框架。迁移作业时不只是迁SQL,还得把调度依赖、失败重试、日志记录一并设计进去,这块工作量往往被低估。
3. 动手迁移:两种可落地的实操路线
3.1 路线一:Navicat数据传输工具的图形化迁移
如果你手头有Navicat,迁移小中型库最直观的方式就是"数据传输"功能。我实际用下来的操作流程:
- 先在MySQL侧建好目标库,字符集utf8mb4、排序规则utf8mb4_general_ci。不建议让工具自动建库,因为工具建的库字符集很可能沿用工具默认值,回头还要再改。
- 在Navicat左侧连接树里同时展开源库(SQL Server)和目标库(MySQL),右键源库选"数据传输"。
- 左侧选SQL Server连接和数据库,右侧选MySQL连接和数据库,传输类型选"结构和数据",勾选需要迁移的对象。
- 高级选项里建议勾选"遇到错误时继续",同时把"批量插入大小"调大,比如1000到5000。小批量插入虽然稳妥,但大表上逐条插入的速度慢到让人怀疑人生。
- 点"开始",观察日志。视图、存储过程、触发器这些对象,如果传输失败Navicat会记录错误,但表数据一般会继续导。
Navicat这套方案的优点是真的省心,表结构、主键、索引、外键基本能自动转换,数据也能按批插入。但它不是万能的:一是存储过程、触发器、自定义函数的T-SQL语法并不会自动改写成MySQL语法,导过去往往是"对象创建失败";二是大表传输过程中如果网络抖动,中断续传的能力比较弱,只能重跑;三是目标表字段类型、长度可能有工具生成的默认映射,比如NVARCHAR(MAX)可能变成LONGTEXT,需要事后核查。
我的经验是:Navicat适合做"大而全"的首轮传输,先把表结构和数据搬过去,算清数据层面的底数;对象脚本(存储过程、触发器、视图)另开一个窗口手工改造。不要指望一个工具把所有事干完,干完的那个通常需要大量返工。
3.2 路线二:mysqldump与脚本改造的命令行迁移
命令行方式的迁移更适合大库和需要精细控制对象的场景。整体思路是三步:先用SSMS导出SQL Server的结构脚本并手工改造为MySQL方言,再通过BCP或导出查询把数据落地为CSV或SQL文件,最后用mysql和mysqldump导入。
第一步导出结构。SSMS里右键数据库→任务→生成脚本,勾选"表"、"视图"、"存储过程"、"触发器",脚本文件拿回来后用文本编辑器批量处理和逐段手工调整。类型替换我一般用正则预处理,比如:
- INT IDENTITY(1,1) → INT AUTO_INCREMENT(SQL Server的IDENTITY种子步长在MySQL里由AUTO_INCREMENT控制,建表后还要用ALTER TABLE ... AUTO_INCREMENT=n重置)。
- NVARCHAR(4000) → VARCHAR(4000)。
- DATETIME → DATETIME(3)。
- GETDATE() → NOW()。
- ISNULL → IFNULL。
- 删除GO、SET ANSI_NULLS ON这类SQL Server专属批处理片段。
正则替换能解决80%的机械替换,但约束名重复、默认值表达式差异、WITH (NOLOCK)这类查询提示(MySQL没有)还是要人工处理。
第二步导出数据,我推荐两个手段。小表直接查询生成INSERT脚本:SSMS里"任务"→"生成脚本"选"仅数据",或者用工具导出为INSERT语句。大表用BCP导出CSV,命令类似:
bcp "SELECT * FROM dbo.orders" queryout orders.csv -S <server> -U <user> -P <password> -c -t , -r \n注意-c参数使用字符数据类型导出,中文环境下建议配合代码页参数,导出后用文本编辑器抽查CSV的编码,推荐UTF-8。
第三步导入MySQL。如果数据文件是CSV,用LOAD DATA INFILE,比逐条INSERT快一个数量级:
LOAD DATA INFILE '/path/orders.csv' INTO TABLE orders FIELDS TERMINATED BY ',' OPTIONALLY ENCLOSED BY '"' LINES TERMINATED BY '\n' IGNORE 1 LINES;如果是INSERT脚本,直接用mysql客户端导入:
mysql -h <host> -u <user> -p <database> < data.sql导入前建议临时关闭外键检查:
SET FOREIGN_KEY_CHECKS=0; SET UNIQUE_CHECKS=0; SET AUTOCOMMIT=0;导入完成后记得改回来,否则后续应用运行时外键约束失效,数据校验通过也会有问题。
3.3 大表批量搬运的性能调优策略
迁移真正考验人的是大表,几百万到上亿行的表,工具默认配置跑完可能要十几个小时。我从源端、通道、目标端三个方向总结优化方法。
源端导出优化:
- 按主键或时间字段分片导出,每片100万行左右,不要一条SQL把整表SELECT出来,内存和网络都吃不消。
- 用BCP的-b参数指定批大小,比如-b 10000,减少事务日志压力。
- 导出期间关注源库的tempdb和日志增长,大批量SELECT会放大日志,建议放在业务低峰执行。
目标端导入优化:
- InnoDB导入时临时关闭唯一检查和外键检查,上面已经提过。
- 把innodb_flush_log_at_trx_commit暂时改为0或2,减少每个事务的刷盘频率。这个参数在迁移期间可以临时调整,迁移完成后要改回生产推荐值(通常为1)。
- 调大innodb_buffer_pool_size,尽量把热数据放内存。比如物理机32GB内存,可以考虑分配20GB给buffer pool,大批量索引重建时效果立竿见影。
- 批量插入时用multi-row INSERT,比如一次INSERT包含500到1000行。MySQL对多行插入的优化非常明显,比单行插入少了几十倍的日志和往返开销。
- 如果目标表有非聚簇索引,可以先导入数据,最后统一创建索引。顺序建索引比边导边建快得多,因为避免了每行插入时的索引维护开销。
- 用LOAD DATA INFILE替代INSERT,这是MySQL批量加载的黄金路径,比普通INSERT通常快5到10倍。LOAD DATA之前要确保数据格式和表结构严格对齐,字段顺序错位很难排查。
通道和网络层面,如果有条件尽量走内网或专线,避免公网传输大文件;压缩能省一半以上的时间,比如用gzip压缩CSV后传输再在目标端解压。我个人遇到过一次在公网传输2GB文件断线重传的惨痛经历,从那以后凡是超过500MB的数据文件,一律先压缩、加校验、再走内网。
4. 迁移后的验证体系与高频报错排查
4.1 数据一致性校验,逐项核对才算完
数据导完不等于迁移完成。我见过太多人数据倒完了,应用一启动就报错,或者跑了一段时间发现某张表的某些行不对。迁移完成的定义应该是:数据行数一致、字段内容一致、应用功能正常、定时任务正常。我每次上线前都会做一套校验脚本,核心是下面几项:
- 表清单和行数核对:用information_schema.tables对比源库和目标库每个表的行数。注意不同存储引擎的统计方式略微不同,MyISAM的行数是精确统计,InnoDB的information_schema行数是估算值,所以一律用COUNT(*)来精确核对。表多的时候,写一个动态SQL自动生成count语句,批次执行。
- 抽样逐字段比对:选几张大表,按主键抽样,比如每张表抽1000行,逐字段对比值。可以用MySQL的HEX()包裹字段避免字符集差异造成的假不等价,也可以在应用层写个简单的哈希对比脚本。
- 自增种子重置:SQL Server的IDENTITY在迁移后,MySQL端AUTO_INCREMENT需要手工设置。执行ALTER TABLE xxx AUTO_INCREMENT = 当前最大ID + 1,否则应用插入新数据可能主键冲突。
- 外键和约束验证:导入时关掉外键检查后,要把每个外键的引用完整性验证一遍,可以用一个LEFT JOIN查孤儿数据的SQL批量跑。
- 存储过程和触发器逐一执行测试:不能只看创建成功,要实际调用一遍,观察结果和日志。触发器尤其要注意,MySQL的NEW和OLD上下文与SQL Server的INSERTED和DELETED表语义有差异,逻辑上需要逐条验。
校验这件事没有捷径,唯一的技巧是把校验脚本化、参数化,第一次手工跑,之后每次演练都是同一套脚本,省心且可对比。我在项目里还习惯把校验结果输出成Excel或Markdown表格,作为迁移验收报告的附件,后续审计也用得上。
4.2 高频报错速查表与处理方案
迁移过程中反复出现的报错,我整理成一张速查表,基本都是亲测过的,遇到同类的直接对照处理:
| 报错信息 | 核心原因 | 解决方案 |
|---|---|---|
| Unknown collation 'utf8mb4_0900_ai_ci' | MySQL版本低于8.0,不认识8.0默认排序规则 | 库表排序规则统一改为utf8mb4_general_ci |
| Incorrect string value: '\xF0\x9F\x98\x80' for column | 字符集不是utf8mb4,无法存储emoji | 库表字段改为utf8mb4,连接串增加characterEncoding=utf8mb4 |
| Data too long for column 'xxx' | 源库VARCHAR长度与目标库不一致,或字符数/字节数误判 | 核对字段定义,SQL Server的VARCHAR(n)按字符,MySQL同样按字符,但行大小受65535字节限制,大字段考虑改TEXT |
| Row size too large. The maximum row size for the used table type | InnoDB单行字节数超限 | 将超大VARCHAR改TEXT/LONGTEXT,或调整行格式为DYNAMIC |
| Column 'xxx' cannot be null | 源库字段可空,但迁移脚本丢失了NULL属性 | ALTER TABLE xxx MODIFY COLUMN xxx 类型 NULL |
| Cannot delete or update a parent row: a foreign key constraint fails | 外键检查关闭后导入数据顺序问题 | 检查子表引用数据是否先导入,或临时SET FOREIGN_KEY_CHECKS=0导入后手工校验 |
| Unknown column 'xxx' in 'field list' | 列名大小写或反引号问题 | MySQL在Linux下表名字段名区分大小写,确认lower_case_table_names设置和列名书写 |
| FUNCTION xxx does not exist | SQL Server函数名在MySQL中不支持 | 改写为MySQL函数,比如ISNULL→IFNULL |
| PROCEDURE xxx already exists | 迁移脚本重复执行 | 建存储过程前先DROP PROCEDURE IF EXISTS |
| Lock wait timeout exceeded | 导入大批量数据时行锁等待超时 | 调大innodb_lock_wait_timeout,或分批提交减少锁持有时间 |
| The user specified as a definer ('xxx'@'%') does not exist | 视图/存储过程的DEFINER指向不存在用户 | 创建对应MySQL用户授权,或用root重建对象 |
| Table 'xxx' doesn't exist after mysqldump | 导入顺序错乱,外键依赖交叉 | mysqldump导入前关闭外键检查,导入后手工校验 |
这张表里的错误,我至少有一半是真刀真枪踩过的。特别是"Data too long"这个,第一次遇到时我盯着字段定义看了半天没想通,后来才意识到是字符集换算的问题,建议大家在迁移前统一跑一遍表结构对比脚本,把每个字段的类型、长度、可空性差异都列出来,从源头规避这类问题。
4.3 迁移后应用侧必须检查的隐藏项
数据库层面迁移完成了,应用不一定能直接跑起来。连接串和驱动是第一个拦路虎。SQL Server的JDBC驱动、.NET的SqlClient、PHP的sqlsrv,全都要换。Java应用建议用MySQL Connector/J,连接串里带上参数:useUnicode=true&characterEncoding=utf8mb4&serverTimezone=Asia/Shanghai&rewriteBatchedStatements=true。rewriteBatchedStatements这个参数很重要,它会自动把JDBC批量INSERT改写为多值INSERT,性能提升非常明显,不加上去批量写可能慢到超时。
ORM层也要查一遍。EF Core、NHibernate、Hibernate这些框架的方言配置、主键生成策略、字段类型映射都需要调整。EF Core的ValueGeneratedOnAdd在MySQL里映射为AUTO_INCREMENT没问题,但如果是GUID主键、序列等策略就要改。还有SQL Server的NOLOCK提示在MySQL没有,ORM生成的SQL里如果带了WITH(NOLOCK),MySQL会直接报语法错误,需要从代码里清理掉。
应用层的分页、排序、事务隔离级别、大小写敏感性全都可能和原来不一致。SQL Server默认读提交(READ COMMITTED),MySQL InnoDB默认可重复读(REPEATABLE READ),并发场景下的行为差异可能导致奇怪的问题,建议结合业务场景确认是否需要把隔离级别调整为读提交。这些内容不适合展开成一篇数据库迁移的文章,但我要提醒的是:迁移计划里一定要预留应用回归测试的时间,至少跑一遍冒烟用例、核心用例和性能基准,否则上线后出了问题,排查成本远高于迁移本身。
5. 迁移账本之外,我攒下来的几条实操心得
最后分享几条我反复验证过的经验,谈不上准则,但能帮你少走弯路。
第一,迁移脚本必须版本化。不管是表结构SQL、数据类型映射表还是转换脚本,全部放进Git仓库。每次迁移演练产生的改动、修复的问题、调整的参数都留痕。我见过太多迁移项目,昨天调通了一个存储过程,今天重新跑又不对了,查了半天发现是有人手改了一个小文件没提交。版本化能根治这种问题。
第二,双库并行期一定要有对账机制。如果迁移后不立即下线SQL Server,至少要保证MySQL和SQL Server的一致性可核对。简单的做法是每天跑一次行数对比,关键表再做抽样对比。账对不上就要查,不要抱有侥幸心理。
第三,迁移完成后不要急着删旧库。我建议保留SQL Server至少两到四个星期,期间业务稳定运行、备份正常,再走销毁流程。旧库保留期间继续执行每日备份,万一新库出现问题,还有回退的余地。
第四,备份演练要有。MySQL的备份方案(mysqldump、XtraBackup、binlog增量)和SQL Server的备份体系完全不同,迁移完成后要重新验证备份恢复流程,确保在意外情况下能恢复数据。这事属于"做的时候没感觉,出事的时候才救命"的类型。
数据库迁移是一次性项目,但它带来的架构变化是长期的。SQL Server到MySQL,不只是换一个数据库软件,还意味着应用层、运维层、监控层甚至团队技能栈的调整。把这些当作项目的一部分去规划,而不是只盯着一张张表的搬运,整个迁移过程会顺利很多。希望这篇实操记录能给你省下几个通宵,少踩几个我踩过的坑。