1. 为什么需要从SQL Server迁移到MySQL?
十年前我刚入行时,SQL Server还是企业级应用的首选,但这些年情况发生了明显变化。最近帮一家电商客户完成了从SQL Server 2016到MySQL 8.0的迁移,整个过程就像给一栋老房子做整体搬迁——既要保证所有家具(数据)完好无损,又要适应新房子(MySQL)的格局。
成本因素是推动迁移的首要原因。那位电商客户原先每年需要支付约15万的SQL Server企业版授权费用,而迁移到MySQL后这笔支出直接归零。性能方面也有惊喜,他们的订单查询性能平均提升了23%,这得益于MySQL对高并发读操作的优化。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 迁移前的关键准备工作
2.1 环境评估与兼容性检查
在动手之前,我通常会先用Microsoft的Data Migration Assistant(DMA)工具做全面扫描。最近一个案例中,这个工具帮我们发现了三个关键问题:
- SQL Server特有的T-SQL窗口函数(如ROW_NUMBER() OVER)
- 存储过程中使用的临时表
- 触发器中的特殊语法
对于函数兼容性问题,我的经验是建立转换对照表。比如:
| SQL Server函数 | MySQL等效方案 |
|---|---|
| GETDATE() | NOW() |
| ISNULL() | IFNULL() |
| TOP N | LIMIT N |
2.2 制定迁移策略
根据数据量大小,我一般推荐两种方案:
- 小型数据库(<50GB):使用Navicat Premium的传输工具直接迁移
- 大型数据库:采用ETL工具(如Talend)+ 增量同步的组合方案
最近一个1.2TB的数据库迁移项目,我们是这样分阶段的:
- 先用Schema Conversion Tool转换表结构
- 用SSIS抽取静态数据
- 配置CDC捕获变更数据
- 最后进行48小时的增量同步验证
3. 实际迁移操作详解
3.1 模式对象转换实战
转换存储过程是最棘手的部分。上周处理的一个案例中,客户有个复杂的库存管理SP,包含:
sql复制-- SQL Server原版
CREATE PROCEDURE UpdateInventory
AS
BEGIN
DECLARE @temp TABLE(item_id INT, qty INT)
INSERT INTO @temp
SELECT item_id, SUM(quantity)
FROM orders
WHERE status = 'pending'
GROUP BY item_id
UPDATE i
SET i.stock = i.stock - t.qty
FROM inventory i
JOIN @temp t ON i.id = t.item_id
END
转换后的MySQL版本:
sql复制-- MySQL适配版
DELIMITER //
CREATE PROCEDURE UpdateInventory()
BEGIN
DECLARE done INT DEFAULT FALSE;
DECLARE v_item_id, v_qty INT;
-- 使用临时表替代表变量
CREATE TEMPORARY TABLE temp_inventory (item_id INT, qty INT);
INSERT INTO temp_inventory
SELECT item_id, SUM(quantity)
FROM orders
WHERE status = 'pending'
GROUP BY item_id;
-- 游标方式处理更新
DECLARE cur CURSOR FOR
SELECT item_id, qty FROM temp_inventory;
DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = TRUE;
OPEN cur;
read_loop: LOOP
FETCH cur INTO v_item_id, v_qty;
IF done THEN
LEAVE read_loop;
END IF;
UPDATE inventory
SET stock = stock - v_qty
WHERE id = v_item_id;
END LOOP;
CLOSE cur;
DROP TEMPORARY TABLE temp_inventory;
END //
DELIMITER ;
3.2 数据迁移的三种可靠方案
方案一:使用MySQL Workbench迁移向导
适合中小型数据库(<20GB),操作步骤:
- 在Workbench点击"Database"→"Migration Wizard"
- 配置源数据库连接(需要安装SQL Server ODBC驱动)
- 选择要迁移的表和视图
- 设置类型映射(特别注意datetime精度问题)
- 执行迁移并验证结果
方案二:使用开源工具Flyway
对于需要持续集成的场景,我推荐以下配置:
yaml复制# flyway.conf
flyway.url=jdbc:mysql://localhost:3306/target_db
flyway.user=root
flyway.password=****
flyway.locations=filesystem:/migrations
flyway.sqlMigrationPrefix=V
flyway.sqlMigrationSeparator=__
迁移SQL文件示例:
sql复制-- V1__Create_customers_table.sql
CREATE TABLE customers (
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(100) NOT NULL,
-- 将SQL Server的uniqueidentifier转为CHAR(36)
external_id CHAR(36)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
方案三:自定义Python迁移脚本
当需要复杂转换逻辑时,我会写这样的脚本:
python复制import pyodbc
import pymysql
from datetime import datetime
def convert_sqlserver_to_mysql():
# 源数据库连接
src_conn = pyodbc.connect('DRIVER={SQL Server};SERVER=src_host;DATABASE=src_db;UID=user;PWD=password')
# 目标数据库连接
dst_conn = pymysql.connect(host='mysql_host', user='user',
password='password', database='target_db')
# 分页读取数据
batch_size = 5000
offset = 0
while True:
with src_conn.cursor() as src_cur:
src_cur.execute(f"SELECT * FROM orders ORDER BY id OFFSET {offset} ROWS FETCH NEXT {batch_size} ROWS ONLY")
rows = src_cur.fetchall()
if not rows:
break
# 特殊字段处理
converted_rows = []
for row in rows:
# 处理SQL Server的bit类型转MySQL的tinyint
new_row = list(row)
if isinstance(new_row[5], bool):
new_row[5] = 1 if new_row[5] else 0
# 处理datetimeoffset类型
if hasattr(new_row[7], 'datetime'):
new_row[7] = new_row[7].datetime
converted_rows.append(new_row)
# 批量插入
with dst_conn.cursor() as dst_cur:
placeholders = ','.join(['%s'] * len(converted_rows[0]))
dst_cur.executemany(f"INSERT INTO orders VALUES ({placeholders})", converted_rows)
dst_conn.commit()
offset += batch_size
4. 迁移后的关键验证步骤
4.1 数据一致性检查
我开发了一个自动化验证脚本,核心逻辑包括:
- 行数比对:确保源表和目标表记录数一致
- 抽样校验:随机抽取0.1%的记录进行字段级比对
- 聚合验证:检查SUM、AVG等聚合值差异是否在允许范围内
python复制def verify_data(src_conn, dst_conn, table_name, pk_column):
# 检查行数
src_count = src_conn.execute(f"SELECT COUNT(*) FROM {table_name}").fetchone()[0]
dst_count = dst_conn.execute(f"SELECT COUNT(*) FROM {table_name}").fetchone()[0]
if src_count != dst_count:
raise ValueError(f"行数不一致: 源表{src_count}条, 目标表{dst_count}条")
# 随机抽样验证
sample_size = max(100, int(src_count * 0.001))
sample_ids = src_conn.execute(
f"SELECT TOP {sample_size} {pk_column} FROM {table_name} ORDER BY NEWID()"
).fetchall()
for id in sample_ids:
src_row = src_conn.execute(
f"SELECT * FROM {table_name} WHERE {pk_column} = ?", id
).fetchone()
dst_row = dst_conn.execute(
f"SELECT * FROM {table_name} WHERE {pk_column} = %s", id
).fetchone()
if src_row != dst_row:
# 详细字段比对逻辑...
4.2 性能基准测试
迁移后必须进行三类测试:
- 查询响应时间:选取20个典型业务查询进行对比
- 并发处理能力:使用sysbench模拟50/100/200并发
- 事务吞吐量:TPC-C基准测试对比
这是我常用的测试脚本片段:
bash复制# sysbench OLTP测试
sysbench oltp_read_write \
--db-driver=mysql \
--mysql-host=127.0.0.1 \
--mysql-port=3306 \
--mysql-user=test \
--mysql-password=test \
--mysql-db=sbtest \
--tables=10 \
--table-size=1000000 \
--threads=64 \
--time=300 \
--report-interval=10 \
prepare
5. 常见问题解决方案
5.1 字符集问题处理
上周遇到一个典型案例:迁移后中文显示为问号。解决方案是:
- 确认SQL Server的原始编码(通常为CP936或UTF-8)
- 在MySQL中创建数据库时指定字符集:
sql复制CREATE DATABASE target_db
CHARACTER SET utf8mb4
COLLATE utf8mb4_unicode_ci;
- 在连接字符串中添加参数:
code复制jdbc:mysql://host:3306/db?useUnicode=true&characterEncoding=UTF-8
5.2 自增ID冲突处理
对于需要保持原ID的情况,迁移后需要重置自增计数器:
sql复制-- 获取原表最大ID
SELECT MAX(id) FROM customers;
-- 在MySQL中设置
ALTER TABLE customers AUTO_INCREMENT = 新起始值;
5.3 日期时间差异
SQL Server的datetime2精度为100ns,而MySQL的datetime精度为1秒。对于需要高精度时间的场景,有两种解决方案:
- 使用timestamp(6)支持微秒级精度
- 将日期存储为bigint类型的时间戳
6. 性能优化建议
6.1 索引策略调整
MySQL的索引机制与SQL Server有显著差异。最近优化过的一个案例中,通过以下调整使查询性能提升40%:
- 将原聚集索引改为非聚集索引
- 添加复合索引时把区分度高的字段放在前面
- 对长文本字段使用前缀索引:
sql复制ALTER TABLE products ADD INDEX idx_name_desc (name(20), description(50));
6.2 参数配置优化
关键的my.cnf配置项(针对16核64GB服务器):
ini复制[mysqld]
innodb_buffer_pool_size = 48G
innodb_log_file_size = 2G
innodb_flush_method = O_DIRECT
innodb_read_io_threads = 16
innodb_write_io_threads = 16
query_cache_type = 0 # 对OLTP系统建议关闭
6.3 监控方案实施
推荐使用Percona Monitoring and Management(PMM)监控以下指标:
- 慢查询比例(>2秒的查询)
- 连接池使用率
- InnoDB缓冲池命中率
- 复制延迟(如果配置了主从)
配置方法:
bash复制docker run -d -p 80:80 \
-e SERVER_USER=admin \
-e SERVER_PASSWORD=secret \
--name pmm-server \
percona/pmm-server:latest
7. 高级迁移场景处理
7.1 大型表的分批迁移
对于超过100GB的单表,我采用以下策略:
- 按主键范围分批导出
- 使用LOAD DATA INFILE替代INSERT语句
- 禁用索引→导入数据→重建索引
具体操作:
sql复制-- 导出时
SELECT * INTO OUTFILE '/tmp/chunk1.csv'
FIELDS TERMINATED BY ',' OPTIONALLY ENCLOSED BY '"'
LINES TERMINATED BY '\n'
FROM huge_table
WHERE id BETWEEN 1 AND 1000000;
-- 导入时
SET foreign_key_checks = 0;
SET unique_checks = 0;
ALTER TABLE huge_table DISABLE KEYS;
LOAD DATA INFILE '/tmp/chunk1.csv'
INTO TABLE huge_table
FIELDS TERMINATED BY ',' OPTIONALLY ENCLOSED BY '"'
LINES TERMINATED BY '\n';
ALTER TABLE huge_table ENABLE KEYS;
SET foreign_key_checks = 1;
SET unique_checks = 1;
7.2 高可用环境迁移
对于需要最小停机时间的生产系统,建议方案:
- 先做结构迁移
- 配置增量同步(使用Debezium捕获SQL Server变更)
- 在维护窗口期切换流量
Debezium配置示例:
properties复制name=sqlserver-connector
connector.class=io.debezium.connector.sqlserver.SqlServerConnector
database.hostname=sqlserver-host
database.port=1433
database.user=sa
database.password=****
database.dbname=source_db
database.server.name=migration_project
table.include.list=dbo.orders,dbo.customers
database.history.kafka.bootstrap.servers=kafka:9092
database.history.kafka.topic=schema-changes
8. 迁移后的长期维护
8.1 定期健康检查清单
我为客户制定的月度检查项包括:
- 索引碎片率(>30%需要重建)
sql复制SELECT table_name, index_name,
ROUND(stat_value * @@innodb_page_size / 1024 / 1024, 2) AS size_mb,
ROUND(stat_value * 100 / table_rows, 2) AS frag_ratio
FROM mysql.innodb_index_stats
WHERE stat_name = 'size' AND database_name = 'your_db';
- 表空间使用情况
- 长事务监控
- 备份验证测试
8.2 备份策略优化
推荐采用物理备份+逻辑备份的组合:
- 物理备份:每周全量(使用Percona XtraBackup)
bash复制xtrabackup --backup --host=127.0.0.1 --user=backup_user \
--password=**** --target-dir=/backups/full_$(date +%Y%m%d)
- 逻辑备份:每日差异(使用mysqldump导出关键表)
- Binlog:保留7天的二进制日志用于PITR
8.3 团队技能转型
建议开展以下培训:
- MySQL与SQL Server的语法差异速成
- EXPLAIN执行计划解读
- 性能优化工具使用(pt-query-digest等)
- 常见故障排查流程
我通常会准备这样的对比手册:
事务处理差异:
- SQL Server:BEGIN TRAN / COMMIT TRAN
- MySQL:START TRANSACTION / COMMIT(需要设置autocommit=0)
锁机制差异:
- SQL Server:丰富的锁提示(WITH NOLOCK等)
- MySQL:主要通过事务隔离级别控制
分页查询差异:
- SQL Server:OFFSET-FETCH
- MySQL:LIMIT offset, count
经过十几次迁移项目的锤炼,我发现最关键的还是测试环节。曾经有个项目因为没充分测试存储过程,上线后才发现有个关键计算逻辑出错,导致财务报表错误。现在我的原则是:测试时间至少要占整个迁移项目的40%。
