尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
SQL Server迁移MySQL完整指南:从评估准备到性能调优
SQL Server 数据库迁移到 MySQL 的完整指南1. 迁移前的评估准备1.1 为什么要做迁移以及先摸清家底再动手这几年把 SQL Server 迁到 MySQL 的需求越来越多我接到过的场景大致有这么几类成本压力大想换开源方案、业务要部署到云端且云上 MySQL 生态更成熟、还有团队统一技术栈避免两套数据库都要维护。不管哪一类迁移本身都不只是把数据搬过去那么简单背后牵涉的是表结构、数据类型、存储过程、视图、权限体系、应用侧 SQL 语句的全链路改造。我在开始任何一次迁移之前一定会先做一次完整的现状盘点列一份检查清单挨个确认下面这些信息数据库版本SQL Server 2008R2 和 2019 的兼容性差距很大MySQL 这边 5.7 和 8.0 的行为差异也不少先把版本对齐。库表数量多少个库、多少张表、哪些是分区表分区表在 MySQL 里的处理方式完全不同。数据总量核心大表的数据行数和预估存储空间这决定后续导数据用哪种策略。对象清单除了表和索引还有多少视图、存储过程、函数、触发器、作业/定时任务。这些是迁移工作量的大头。应用侧技术栈业务代码是 .NET 还是 JavaORM 用的是 EF、Hibernate 还是 MyBatis这直接影响 SQL 兼容性改造成本。1.2 兼容性分析与风险评估迁移最怕的不是数据搬不过去而是搬过去之后应用跑出各种诡异问题。我习惯把兼容性风险分成三档低风险普通表结构、基础数据类型、简单增删改查。这类直接转换工具或脚本就能搞定基本不会有意外。中风险数据类型边界不一致比如 datetime 范围、decimal 精度、内置函数差异比如日期函数、字符串函数、隐式类型转换规则不同。这类需要仔细对照映射关系。高风险复杂存储过程、动态 SQL、游标、事务隔离级别依赖、序列Sequence使用、死锁处理逻辑。这类往往要人工重写并做充分的业务逻辑验证。我记得有个某电商系统的订单库迁移项目单库 200 多张表存储过程 60 多个其中最复杂的月结存储过程跑了上千行业务逻辑。事前评估阶段我们花了两周但正是这个评估让后面的正式迁移少踩了很多坑。提示迁移前一定要确认 MySQL 版本。如果目标库是 8.0那么WITH RECURSIVE、窗口函数、CHECK约束这些语法能力会强很多SQL Server 迁移过来的很多 SQL 可以直接用。如果目标库是 5.7部分写法就得另想办法。2. 迁移方案选型2.1 工具迁移和手工迁移各自的适用场景迁移方案没有银弹我见过太多人盲目依赖工具结果迁完才发现视图和存储过程全是半成品。也有人纯手工从零建表费时费力还不讨好。正确的做法是根据迁移对象的复杂度来决定组合策略。目前市面上可选择的路子大概有三条第一条使用商业化迁移工具如某大型数据库迁移服务、某数据传输服务。优点是有图形化界面、支持全量加增量同步、能自动转换大部分对象定义。缺点是对复杂存储过程、动态 SQL 的支持有限而且在大数据量下容易出现卡死或中断。适合小规模、结构简单的库。第二条开源工具加手工脚本结合。先用开源工具把表结构和基础数据导出来再补充处理那些工具搞不定的对象。这种方式我用的最多既保留了自动化带来的效率又能靠人工保证复杂对象的迁移质量。适合中大型项目。第三条完全手工迁移。先导表结构再逐个处理数据类型和建表语句然后导数据最后重建对象。虽然慢但每一步都在掌握之中适合核心系统、不允许出任何差错的场景。2.2 我推荐的工具组合和分工策略实际操作层面我会按下面这套组合来干第一步用 SQL Server 自带功能生成建表脚本然后手工调整 MySQL 语法。具体来说是在 SSMS 中选中数据库右键任务-生成脚本。注意步骤里选择仅架构文件里选择“保存到文件”编码维持默认即可。拿到的脚本是CREATE TABLE加各种约束的完整版本接下来就是逐条手工改写。第二步用数据比较工具或自写导出脚本来搬数据。数据量小的库可以直接生成INSERT INTO语句数据量大的库建议用命令行工具例如 CSV 或管道导入速度会快几个量级。这里不太推荐直接用某些 GUI 工具的“数据传输”功能把数据另存为插入语句因为数据量大时打开文件就能卡死编辑器效率很低。第三步视图、存储过程、函数、触发器要单独导出单独处理。SSMS 里同样可以针对每个对象生成脚本但这些脚本基本上不能直接运行于 MySQL必须逐条改写。我强烈不建议在一个项目里混用太多工具。最好的方式是确定一种主数据迁移工具连同手工脚本整合成一套流水线这样排查问题的时候才不会搞不清是工具的问题还是脚本的问题。3. MySQL 环境准备与基础配置3.1 安装版本选择和字符集设定目标环境我会优先选择 MySQL 8.0。原因不复杂8.0 对窗口函数、通用表表达式、CHECK约束的支持已经比较完整SQL Server 迁移过来的对象改动面会小很多。字符集这件事要放在建库之前定下来迁移之后想改字符集极其痛苦。SQL Server 的NCHAR、NVARCHAR对应 MySQL 的utf8mb4字符集这个必须用utf8mb4而不是utf8因为 MySQL 的utf8实际是utf8mb3只有基本多文种平面连 emoji 也存不了更不要说生僻字。推荐的建库语句CREATE DATABASE target_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;排序规则可以选择utf8mb4_unicode_ci或utf8mb4_general_ci。前者对 Unicode 排序更准确后者性能略好。一般业务场景我选择utf8mb4_unicode_ci。3.2 关键参数调整MySQL 默认配置走的保守路线不会给大迁移预留足够的资源。在启动迁移前有几个参数必须调整max_allowed_packet默认值偏小如果导入的 SQL 文件里有大的INSERT语句或者包含大对象的写入会直接报PacketTooLarge。建议至少调到 512M。innodb_buffer_pool_size这是 InnoDB 缓存索引和行数据的缓冲池物理内存的 60%~70% 是一个比较稳妥的起步值。如果你在专用服务器上跑迁移内存 64G 就设 40G迁移大表时性能差距非常明显。group_concat_max_len如果迁移的存储过程中有用到GROUP_CONCAT函数默认 1024 字节很容易被截断建议设置到 1M。sql_mode这是迁移中很容易踩坑的一项。SQL Server 对0000-00-00这样的日期是允许写入的但 MySQL 默认模式下会拒绝。建议按下面方式设置给迁移留出余地SET GLOBAL sql_mode STRICT_TRANS_TABLES,NO_ENGINE_SUBSTITUTION;当然如果业务上不允许非法日期迁移完成后可以再切回严格模式并做数据清洗。time_zoneSQL Server 直接存本地时间MySQL 的CURRENT_TIMESTAMP和连接时区相关两边如果不一致就会出现时间偏差。建议确认应用服务器和数据库服务器时区一致。注意lower_case_table_names这个参数在 Linux 上默认是 0也就是大小写敏感SQL Server 上建的表如果用了大小写混合的表名迁移后应用到 Linux 环境下会找不到表。如果你希望 MySQL 像 Windows 环境那样大小写不敏感必须在初始化时就指定--lower-case-table-names1这个参数后期修改风险很高极容易导致数据字典对不上。4. 表结构和数据类型迁移实操4.1 生成建表语句并转换为 MySQL 方言基于 SQL Server 生成的架构脚本把每张表的建表语句逐条改写成 MySQL 能识别的版本。差异体现在这些方面自增列、默认值、索引命名、约束命名、字符类型长度计算方式。举例来说SQL Server 的一张订单表脚本通常长这样CREATE TABLE [dbo].[Orders]( [OrderID] [int] IDENTITY(1,1) NOT NULL, [OrderNo] [nvarchar](50) NOT NULL, [CustomerID] [int] NULL, [OrderDate] [datetime] NULL, [TotalAmount] [decimal](18,2) NULL, [Remark] [nvarchar](max) NULL, CONSTRAINT [PK_Orders] PRIMARY KEY CLUSTERED ( [OrderID] ASC ) )改写后对应的 MySQL 建表语句CREATE TABLE Orders ( OrderID INT NOT NULL AUTO_INCREMENT, OrderNo VARCHAR(50) NOT NULL, CustomerID INT NULL, OrderDate DATETIME NULL, TotalAmount DECIMAL(18,2) NULL, Remark TEXT NULL, PRIMARY KEY (OrderID) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COLLATEutf8mb4_unicode_ci;这里有几个细节值得多说一句。nvarchar(50)在 SQL Server 中按字符数计算MySQL 的VARCHAR(50)也是按字符数所以长度可以直接对应不需要按字节数换算。nvarchar(max)要转成 MySQL 的TEXT类型但如果字段长度可能非常大也可以用LONGTEXT。datetime类型两边都有但要注意 SQL Server 的datetime精度是 3.33 毫秒MySQL 的DATETIME默认精度是秒如果业务上有毫秒级时间精度需求建表时建议显式写成DATETIME(3)或DATETIME(6)。4.2 常用数据类型映射对照表我一直建议迁移前先整理一张类型映射操作表发给团队成员谁改哪张表都对照这个表来避免各写各的。下面是我积累的对照表覆盖面比较广SQL Server 类型MySQL 类型备注bigintBIGINT直接对应intINT直接对应smallintSMALLINT直接对应tinyintTINYINT注意 SQL Server 的tinyint是 0~255MySQL 的TINYINT有符号范围是 -128~127如果原表存了超过 127 的值需要改成TINYINT UNSIGNEDbitBOOLEAN/BIT(1)推荐用BOOLEAN实际是TINYINT(1)遇到位运算场景再仔细验证decimal(p,s)DECIMAL(p,s)直接对应精度不会丢numeric(p,s)DECIMAL(p,s)转换规则同上money/smallmoneyDECIMAL(19,4)/DECIMAL(10,4)money是定点数不要用浮点类型替代float/realDOUBLE/FLOAT注意精度差异金融类字段不要用dateDATE直接对应datetimeDATETIME / DATETIME(N)按业务精度需求加括号位数datetime2DATETIME(N)精度最高可到 7 位MySQL 最高 6 位截断后要验证业务影响smalldatetimeDATETIME直接对应datetimeoffsetVARCHAR(45)没有原生对应类型建议原样存字符串或拆成 UTC 时间加偏移char(n)CHAR(n)直接对应注意排序规则继承问题varchar(n)VARCHAR(n)直接对应varchar(max)LONGTEXT大字段统一用LONGTEXT避免长度截断nvarchar(n)VARCHAR(n)字符数对应配合utf8mb4ntext/textTEXT/LONGTEXT老类型统一转成TEXT系列binary(n)BINARY(n)直接对应varbinary(n)VARBINARY(n)直接对应varbinary(max)LONGBLOB大二进制字段用LONGBLOBimageLONGBLOB直接对应uniqueidentifierCHAR(36)或存BINARY(16)但为了可读性建议直接转字符串xmlTEXT/LONGTEXT应用侧解析数据库侧存文本4.3 索引、约束和默认值的重建策略索引在迁移中容易被忽视。SQL Server 的聚集索引是物理排序InnoDB 的主键索引也是聚集索引结构上很相似。但索引命名时要小心SQL Server 的索引名在库内唯一MySQL 的索引名在表内唯一即可。导出脚本中每一行索引名如果带有全局性的拼接前缀到了 MySQL 环境也可能冲突。默认值方面最常见的坑就是日期函数和newid()SQL Server 写法CREATE TABLE dbo.Orders( CreateTime DATETIME NOT NULL CONSTRAINT DF_Orders_CreateTime DEFAULT (getdate()), RowGUID UNIQUEIDENTIFIER NOT NULL CONSTRAINT DF_Orders_RowGUID DEFAULT (newid()) )MySQL 写法CREATE TABLE Orders ( CreateTime DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, RowGUID CHAR(36) NOT NULL DEFAULT (UUID()) )需要注意 MySQL 8.0 开始才支持默认值用表达式加括号写法5.7 里DEFAULT (UUID())是不合法的。约束的迁移相对简单但命名规则有差异。SQL Server 的约束名在 schema 内唯一MySQL 的约束名在库内唯一为了移植方便建议把约束名改短统一加前缀例如PK_、FK_、UK_、CK_。5. 核心语法的兼容性改造5.1 SELECT 查询语句的差异与改写SELECT TOP n是 SQL Server 使用频率最高的语法之一。MySQL 的等价写法是LIMIT n。如果同时带ORDER BY语义是等价的。SQL Server 写法SELECT TOP 10 OrderID, OrderNo, TotalAmount FROM Orders ORDER BY OrderDate DESC;MySQL 写法SELECT OrderID, OrderNo, TotalAmount FROM Orders ORDER BY OrderDate DESC LIMIT 10;分页查询方面SQL Server 较新的版本用法是SELECT OrderID, OrderNo FROM Orders ORDER BY OrderID OFFSET 40 ROWS FETCH NEXT 20 ROWS ONLY;MySQL 的写法是SELECT OrderID, OrderNo FROM Orders ORDER BY OrderID LIMIT 20 OFFSET 40;注意 MySQL 的LIMIT 20 OFFSET 40表示跳过 40 行取 20 行顺序是很容易搞反的。实际参数化的时候我也见过同事把两个数字写反导致查出来的页码错位。建议在代码层做封装统一传 pageIndex 和 pageSize 再换算。字符串拼接差异也是一个重灾区。SQL Server 用拼接SELECT FirstName LastName AS FullName FROM Customers;MySQL 用CONCAT函数SELECT CONCAT(FirstName, , LastName) AS FullName FROM Customers;这里注意只有两个值拼接时可以用CONCAT三个及以上建议都走函数因为 MySQL 里在字符串场景会被当成数学运算符遇到非数字字符串结果会出乱子。5.2 常用内置函数差异对照日期函数是迁移中改动量最大的部分。SQL Server 的GETDATE()在 MySQL 中对应NOW()或SYSDATE()。这里有一个细微差别需要注意NOW()返回的是语句开始执行的时间SYSDATE()返回的是函数被调用时的时间。长事务场景下两者可能会不同。绝大多数业务用NOW()就好。日期格式化上SQL Server 的CONVERT(VARCHAR(10), OrderDate, 120)在 MySQL 中对应DATE_FORMAT(OrderDate, %Y-%m-%d)。获取年份和月份的字段提取差异-- SQL Server SELECT YEAR(OrderDate), MONTH(OrderDate) FROM Orders; -- MySQL SELECT YEAR(OrderDate), MONTH(OrderDate) FROM Orders;这个两边一致。但如果是日期差计算差异就大了。-- SQL Server SELECT DATEDIFF(day, CreateTime, GETDATE()) FROM Orders; -- MySQL SELECT DATEDIFF(NOW(), CreateTime) FROM Orders;MySQL 的DATEDIFF只返回天数差而 SQL Server 的DATEDIFF可以指定day、hour、minute、second等多种单位。如果业务里需要小时差MySQL 要用TIMESTAMPDIFF(HOUR, CreateTime, NOW())。这两个函数参数顺序都不一样改写时必须逐行核对。ISNULL对应 MySQL 的IFNULL这一点很容易改但要注意嵌套场景。NULLIF两边都有语法一致。ISNULL在 SQL Server 里也能当判断语法使用这种上下文 MySQL 不支持需要改成IS NULL。5.3 存储过程、视图和触发器迁移存储过程的改造是迁移工作量最大的环节没有之一。SQL Server 的过程开头有AS关键字MySQL 的CREATE PROCEDURE则要求设置DELIMITER并且过程体用BEGIN...END包起来。一个简单的示例SQL Server 版本CREATE PROCEDURE usp_GetOrderSummary CustomerID INT AS BEGIN SELECT CustomerID, COUNT(*) AS OrderCount, SUM(TotalAmount) AS TotalAmount FROM Orders WHERE CustomerID CustomerID GROUP BY CustomerID; END;MySQL 版本DELIMITER // CREATE PROCEDURE usp_GetOrderSummary(IN p_CustomerID INT) BEGIN SELECT CustomerID, COUNT(*) AS OrderCount, SUM(TotalAmount) AS TotalAmount FROM Orders WHERE CustomerID p_CustomerID GROUP BY CustomerID; END // DELIMITER ;这里面有几个隐性的改造点参数名和列名冲突问题。SQL Server 的参数用前缀天然避开了列名冲突。MySQL 的参数就是普通标识符强烈建议用p_前缀来做参数命名避免过程体内WHERE CustomerID CustomerID这种逻辑完全失效的情况。局部变量声明时机。SQL Server 允许在一个BEGIN...END块内任意位置声明变量MySQL 的DECLARE必须放在BEGIN之后。SET NOCOUNT ON。SQL Server 过程里常见的SET NOCOUNT ON在 MySQL 中没有对应语义。如果应用依赖结果集的数目来触发后续逻辑迁移后要调整应用侧。游标方面SQL Server 的FETCH NEXT FROM cur INTO var和 MySQL 的游标语法差异不算太大但 MySQL 的游标有一个限制是只能向前遍历不支持SCROLL需求里如果要用绝对定位来回跳转的游标在迁移时就得重写逻辑。触发器在 MySQL 里语法上改动不大但要注意触发时机。SQL Server 有INSTEAD OF和AFTER两大类MySQL 有BEFORE和AFTERINSTEAD OF没有直接对应。碰到INSTEAD OF触发器只能把它改写为BEFORE处理逻辑或者拆到应用侧实现。我自己在处理一批存储过程时通常先把所有PRINT语句删掉或换成SELECT因为 MySQL 没有PRINT。接着把所有临时表#temp换成 MySQL 的临时表注意 MySQL 临时表在存储过程中不能多次用同一名称且连接会话结束后自动销毁。5.4 事务、锁和隔离级别的差异SQL Server 默认隔离级别是READ COMMITTEDMySQL InnoDB 默认是REPEATABLE READ。两者差距在并发场景下比较明显。迁移后如果发现应用出现不可重复读相关问题是因 MySQL 在REPEATABLE READ下用快照读行为与 SQL Server 不一致导致的。处理方式有两个方向。第一个方向是业务层避免长事务缩短事务内耗时第二个方向是使用 MVCC 快照读而不是当前读。一条早期在 SQL Server 上很正常的 UPDATE 语句迁移后在 MySQL 上出现锁等待超时大概率就是隔离级别带来的行锁范围扩大。排查时优先看有没有目标列上缺少索引没有索引时 InnoDB 会锁住全表扫描到的行。MySQL 的死锁检测默认是开启的事务遇到死锁会有报错信息输出到错误日志。迁移后我一贯的做法是重新梳理事务边界让每个事务更快提交降低锁冲突概率。6. 数据迁移的完整流程6.1 小数据量库怎么安全导出导入数据量在 1GB 以内、表数量不超过几十张的情况下最简单稳妥的方案是把每张表的数据导出为 SQL 文件再导入 MySQL。SQL Server 端用bcp工具或者直接做成插入脚本都可以。我比较喜欢的方式是用bcp导出成 CSV再用LOAD DATA INFILE导入。bcp导出命令大概是这个样子bcp SELECT OrderID, OrderNo, CustomerID, OrderDate, TotalAmount FROM Orders queryout orders.csv -S server_ip -U sa -P password -d source_db -c -t , -r \n导入到 MySQL 的命令mysql -h target_host -u root -p target_db \ --local-infile1 \ -e LOAD DATA LOCAL INFILE orders.csv INTO TABLE Orders FIELDS TERMINATED BY , ENCLOSED BY \ LINES TERMINATED BY \n IGNORE 1 ROWS;LOAD DATA INFILE的速度比逐条INSERT快很多实测下来通常快 5~10 倍这在迁移场景里很关键。提示如果原表字段中含有分隔符或换行符CSV 格式一定要指定ENCLOSED BY选项并且选择不常用的分隔符建议用 CHAR(1) 形式的特殊字符比如\x01。否则导入时会错位。6.2 大数据量库的分批迁移策略数据量在数十 GB 甚至上百 GB 时单次导出导入不现实。分批策略我有两个切身经验第一个经验是不要用SELECT *一次性全量拉取而是用主键或唯一键做分段。比如按OrderID区间逐段导出bcp SELECT * FROM Orders WHERE OrderID BETWEEN 1 AND 1000000 queryout orders_1.csv -S server_ip -U sa -P password -d source_db -c -t \x01 -r \n第二个经验是不建议迁移过程中强开事务。迁移大表时我把导入过程切成若干小段每段百万行级别按段提交。这样任何一个段失败都可以单独重来不至于整张表回滚耗尽时间和资源。外键约束在数据导入时建议先关闭。迁移完数据再统一加索引和外键可以提高 30% 以上的导入效率。6.3 数据一致性校验数据导完之后必须要做校验。我常用的校验方法有三层第一层行数校验。统计源库和目标库每张表的行数两边必须一致。这里我要强调按表逐一验证不要只抽查大表小表也有隐蔽问题。第二层抽样校验。用查询语句随机抽取若干条记录比对字段值。重点抽日期/时间、金额、大文本、空值等容易出问题的字段。第三层业务汇总校验。对不同维度的 SUM、COUNT、MAX、MIN 做对比比如订单总金额、会员总数、商品库存总和。我曾经遇到过一个案例行数完全一致但某张表的金额列在导出时全部被截断成整数丢掉了小数部分。如果没有汇总校验这个问题要等业务对账时才暴露那时候排查成本就非常高了。6.4 自增列和环境类特殊问题SQL Server 的IDENTITY(1,1)迁移到 MySQL 后在表面上没有差异但有一个隐含问题如果数据库有大量删除操作SQL Server 不会主动回收自增值MySQL 的AUTO_INCREMENT在重启后会根据最大当前值继续递增。如果业务对自增 ID 有顺序语义要求比如某个单据号必须连续那迁移后可能出问题。这类业务建议提前在应用侧改为发号器或者迁移前手动设置AUTO_INCREMENT起始值。uniqueidentifier类型也要单独处理。SQL Server 的NEWID()生成 36 位带连字符的字符串如果用CHAR(36)保存本身没问题。但要注意 MySQL 的UUID()默认生成小写带短横线的格式如果没有特殊要求直接存字符串即可。7. 迁移后验证与性能调优7.1 统计信息更新与算子优化数据导入完成不代表迁移结束。MySQL 在导入大量数据后表的统计信息可能不准确这时候执行计划的生成会出现偏差。我迁移完成后第一件事就是对所有表做一次ANALYZE TABLEANALYZE TABLE Orders;如果有多张表可以拼接批量执行。注意 MySQL 8.0 在导入大表后需要手动刷新统计信息否则OPTIMIZER可能选择错误的索引。紧接着要处理查询性能问题。原来 SQL Server 上跑得飞快的语句在 MySQL 上可能需要重新审视索引设计。虽然表结构和索引名都是原样迁移但两个数据库的优化器行为完全不同索引失效的场景也完全不同。最常见的场景是隐式类型转换。SQL Server 对字符列查询条件会自动转换MySQL 则可能直接放弃索引。这类问题出现后用EXPLAIN看执行计划就能快速定位EXPLAIN SELECT * FROM Orders WHERE OrderNo 123;如果OrderNo是VARCHAR类型而传入参数是数字MySQL 会放弃索引走全表扫描。应用侧改法是把参数固定为字符串类型。7.2 数据库侧参数调优迁移完成后MySQL 实例层面的参数也不能按照默认配置跑业务。我一般会在迁移验证通过后按这几步调整innodb_buffer_pool_size调整到物理内存的 60%~70%业务高峰期前完成大量的热数据预热。innodb_io_capacity和innodb_io_capacity_max根据磁盘类型设置。SSD 上分别设为 2000 和 4000 比较稳妥机械硬盘维持默认即可。binlog_format如果是默认的 ROW 模式大事务的 binlog 会膨胀同步到备库的压力会很大。如果业务允许大事务尽量拆小。max_connections不能盲目调大过高的连接数会让线程切换成为瓶颈。配合应用侧连接池上限调一般经验值是应用实例数乘以连接池大小再加 20% 余量。7.3 应用侧代码改造数据库迁到 MySQL 之后最容易被忽略的是应用侧连接串和驱动调整。SQL Server 的System.Data.SqlClient要换成 MySQL 的驱动连接串参数比如Integrated Security、Encrypt这些都需要移除。驱动换了之后还要检查 SQL 语句的占位符风格。SQL Server 的param风格在 MySQL 需要变为?占位符具体依 ORM 框架而定。对于 .NET 应用可以用 MySqlConnector 驱动做兼容。对于 Java 应用MyBatis 的#{}写法本身兼容两边但如果你原来在 SQL Server 里用了TOP分页或GETDATE()这些在 MyBatis 的 XML 里也要同步改掉。迁移后的测试我建议分三个阶段先跑通冒烟测试用例再跑完整回归测试最后模拟生产流量压测。没有经过这三个阶段的系统我不建议直接上生产。8. 常见问题与排查技巧实录8.1 乱码问题和字符集冲突这是排行榜第一的经典问题。表现是目标库里中文全部变成问号或者乱码。排查步骤很固定先看数据库、表、列三层的字符集定义必须都是utf8mb4。再确认连接串里写了characterEncodingutf8mb4或charsetutf8mb4。最后看SET NAMES utf8mb4是否在会话初始化时执行过。有一点容易被忽略导入 CSV 文件时文件本身的编码对导入结果影响极大。Windows 下生成的文件经常是 GBK 编码导入前应统一用iconv转换成 UTF-8iconv -f GBK -t UTF-8 source.csv source_utf8.csv其实就连 Linux 下用bcp导出的文件默认编码也可能是操作系统的 locale导出前最好在bcp命令里指定编码选项。8.2 主键冲突和重复数据迁移时经常出现导入失败报主键冲突原因大多是源库表本身存在重复主键或从 CSV 导入时未按正确顺序执行。排查办法很简单导入前先做一次源库的SELECT 主键, COUNT(*) GROUP BY 主键 HAVING COUNT(*) 1检查发现重复后先清理脏数据再迁移。还有种情况是自增列的值没有正确传递。比如源表有 100 万行但删除过很多记录自增当前值已经是 200 万直接导入到 MySQL 后新插入数据的自增 ID 可能从 100 万 1 开始如果其他表有基于外部 ID 的引用关系就会错乱。解决方式是把自增列的显式值一起导入导入完成后用ALTER TABLE Orders AUTO_INCREMENT 2000001;重新指定起点。8.3 大表导入超时和中断恢复使用mysql命令行导入大 SQL 文件时容易遇到超时断开的提示。需要确认两个超时参数SET SESSION net_read_timeout 600; SET SESSION net_write_timeout 600;如果在导入过程中发生了中断优先确认导入是不是幂等的。如果 SQL 脚本里的INSERT没有去重逻辑重新执行会导致重复数据。更稳妥的做法是脚本里加INSERT IGNORE或先用临时表再合并但我建议直接采用 CSV 分片导入每次导入前清空对应分片区间这样重试是无状态的。8.4 迁移后性能变差怎么定位这是个开放性话题但掌握一个排查思路基本不会抓瞎。第一步EXPLAIN看执行计划确认索引是否被用上。第二步看表行数和索引统计信息是否过期过期的直接ANALYZE TABLE。第三步确认是不是有隐式转换导致索引失效检查条件列的类型是否匹配。第四步看应用侧是否还跑着 SQL Server 习惯的写法比如FULL OUTER JOIN在 MySQL 中不支持就可能导致查询出错或性能骤降。在我的经验里位列前三位的问题分别是索引失效、统计信息过期、分页写法没有改造成LIMIT导致全部全表扫描。这三类问题解决后80% 的迁移后性能问题都能缓解。9. 迁移项目收尾的额外建议最后分享一个我在多个迁移项目里养成的习惯迁移完成后的第一个业务周期数据库侧开启慢查询日志阈值设到 1 秒每天观察一轮。SET GLOBAL slow_query_log ON; SET GLOBAL long_query_time 1;慢查询日志会在迁移初期帮我们发现那些测试阶段没覆盖到的低效 SQL。我经历过不止一次应用测试都通过了但上线第二天慢查询日志里出现了好几个执行时间几十秒的语句都是开发侧没注意 SQL Server 和 MySQL 语法差异写出来的。这类问题越早发现越好解决。另外一个习惯是保留迁移过程的全部脚本和日志。导出脚本、转换脚本、导入日志、校验结果全部按日期归档。一旦业务侧出现数据对不上的情况可以从归档里快速定位是迁移哪一步出了问题。如果做完迁移后这些过程材料全部扔掉后续排查只能靠回忆代价极高。提示大表数据迁移时迁移期间业务侧停写的时间窗口要提前规划好。确定迁移窗口时至少预留出 30% 的时间余量因为几乎每次迁移都会冒出意外情况需要处理。迁移这件事做到最后考验的其实已经不是 SQL 语法熟不熟而是对两个数据库各自行为特性的把握。你越了解 SQL Server 哪些写法是它特有的就越能预见 MySQL 里有哪些对应坑。希望这篇指南能帮你少走一些弯路迁移过程顺顺利利。
RELATED

相关推荐

大数据情感分析实战:从标注规范到模型上线的全链路经验

大数据情感分析实战:从标注规范到模型上线的全链路经验

情感分析(Sentiment Analysis)在大数据圈子里经常被当成一个“入门级NLP Demo”,好像加载一个预训练模型、对几条评论跑一下就能出结果。但等你真正把模型丢到每天百万级甚至亿级的数据流里,就会发现挑战根本不在模型本身&#xf…

📅 2026/10/22 10:35:04
数字人舱五年运维成本优化与选型指南

数字人舱五年运维成本优化与选型指南

在商业空间数字化升级的浪潮中,数字人交互舱已成为商场导览、银行接待及政务大厅的标配设施。然而,许多项目负责人在采购初期往往陷入一个常见的误区:过度关注设备的初始报价,而忽视了长达数年的运营隐性成本。这种“重首购、轻运…

📅 2026/10/22 10:30:00
链式队列从实现到避坑:指针边界与内存管理的完整指南

链式队列从实现到避坑:指针边界与内存管理的完整指南

前段时间我帮一位朋友调链式队列的代码,他看着控制台上飘出的段错误提示,有点懵:“逻辑不复杂啊,怎么就崩了?”我瞄了一眼,问题出在出队删除最后一个节点之后,rear 指针还指着被释放的内存。链式…

📅 2026/10/22 10:30:00
MORE NEWS

更多资讯

📰

Gemini 4 Argon 深度解析:Android 开发者的端侧推理与多模态实战

1. 这次更新到底动了哪些底层能力1.1 从开发者视角看“Argon”这个版本号先把话说在前头:网上关于“Gemini 4 Argon”的讨论,一半是参数党在比跑分,一半是营销号在喊“安卓要变天了”。但作为一个天天跟Android工程打交道的人,我更…

📰

DeepAgents插槽化设计:Agent的模型、工具、记忆等五大扩展点解析

1. 从散件到整机:DeepAgents 与 Agent 身上为什么要预留“插槽”1.1 DeepAgents 视角下的 Agent 到底是什么?组装过台式机的朋友都知道,整机体验的好坏往往不取决于单个配件有多顶级,而在于每一块配件是不是留对了接口。内存条有标…

📰

数学建模竞赛实战:学生面试问题的组合优化建模与算法解析

2006年研究生建模D题“学生面试问题”是我印象很深的一道题。当年做这道题时,我们队一度以为它就是个排队问题,画出几张甘特图就能交差。结果越做越发现,题目表面上是“排一排面试顺序”,实际上塞进了约束满足、多目标优化和算法工…

📰

Hadoop+Spark+Django构建足球数据可视化大屏完整实战

拿这种项目的人,十有八九是在做毕业设计、课程实训,或者想往大数据方向投简历需要撑场面的作品。我当年也是从这种“三件套”项目起步的:Hadoop负责把数据存起来,Spark负责算,Django把结果变成网页上能看的大屏。今天就…

📰

WebStorm配置ESLint:Vue项目代码规范实战指南

先说句题外话,标题里“WebStrom”这个拼法我见过太多次了,正主其实是WebStorm,中间是大写的 S。不过这不影响我们今天聊的内容:在 WebStorm 里写 Vue 项目,到底怎么把 ESLint 这个代码风格检查工具装得明明白白、真正用…

📰

.NET 构建发布革新:中央包版本管理、AOT 与容器直出实践

如果你和我一样,从 .NET Core 时代就开始把dotnet build和dotnet publish当日常命令来敲,你会明显感觉到:构建和发布的方式每隔一两个大版本,就会被刷新一次。这个系列前两篇,我分别聊过 SDK 风格项目的基线配置&#…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬