因成本、架構調整需將 SQL Server 數據庫全量遷移至 MySQL。調研兩條主流路徑:
MySQL ODBC 8.0 Unicode Driver (匹配 MySQL 版本)MySQL_DSN,指向目標 MySQL 實例MySQL_DSN → 選表/視圖 → 運行| 問題 | 現象 | 根因 | 影響 |
|---|---|---|---|
| 大對象類型不兼容 | ntext、image、xml 導出失敗或截斷 | ODBC 驅動/導出向導對 LOB 類型映射缺陷 | 業務關鍵文檔/圖片/日志數據丟失 |
| 時間類型衝突 | SQL Server datetime → MySQL INT (時間戳) 導入報錯/數值錯誤 | 類型映射邏輯固化,無法自定義轉換函數 | 所有時間字段遷移失效 |
| 目標表結構不受控 | 自動建表生成 VARCHAR(MAX)、DATETIME 等非預期類型 | 向導根據源類型推斷,不符合 MySQL 最佳實踐 (utf8mb4, InnoDB, 索引優化) | 上線後性能差、存儲浪費、需重構 |
| 編碼亂碼 | 中文欄位變「???」或亂碼 | ODBC DSN 未正確設置 charset=utf8mb4、SQL Server 列排序規則衝突 | 數據不可用,需重導 |
| 無法斷點續傳 | 大表導入中斷需全量重來 | 向導無事務/批次控制 | TB 級數據遷移風險極高 |
結論:僅適用於**小數據量、結構簡單、類型標準、容錯率高**的測試/開發環境遷移。生產環境**強烈不推薦**。
datetime → UNIX_TIMESTAMP()、ntext → LONGTEXT、bit → TINYINT(1)、uniqueidentifier → CHAR(36)ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=COMPRESSED、分區表、覆蓋索引
PDO::ERRMODE_EXCEPTION,
PDO::ATTR_EMULATE_PREPARES => false,
PDO::MYSQL_ATTR_USE_BUFFERED_QUERY => false // 流式讀取大結果集
]);
// 1. 獲取源表列信息 (含類型、長度、精度、標度)
$cols = $src->query("
SELECT COLUMN_NAME, DATA_TYPE, CHARACTER_MAXIMUM_LENGTH, NUMERIC_PRECISION, NUMERIC_SCALE
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_NAME = 'users' AND TABLE_SCHEMA = 'dbo'
")->fetchAll(PDO::FETCH_ASSOC);
// 2. 構建目標表 DDL (預建優化版)
$ddl = "CREATE TABLE IF NOT EXISTS `users` (\n";
foreach ($cols as $c) {
$mysql_type = map_type($c['DATA_TYPE'], $c['CHARACTER_MAXIMUM_LENGTH'], $c['NUMERIC_PRECISION'], $c['NUMERIC_SCALE']);
$ddl .= " `{$c['COLUMN_NAME']}` $mysql_type,\n";
}
$ddl .= " PRIMARY KEY (`id`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=COMPRESSED;";
$dst->exec($ddl);
// 3. 流式讀取 + 批次寫入
$select = $src->prepare("SELECT * FROM dbo.users ORDER BY id");
$select->execute();
$insert_sql = "INSERT INTO `users` (" . implode(',', array_map(fn($c)=>"`{$c['COLUMN_NAME']}`", $cols)) . ") VALUES (" . str_repeat('?,', count($cols)-1) . "?)";
$ins = $dst->prepare($insert_sql);
$batch = [];
$batch_size = 2000;
$total = 0;
while ($row = $select->fetch(PDO::FETCH_ASSOC)) {
// 數據轉換/清洗
foreach ($row as $k => $v) {
if ($v instanceof DateTime) {
$row[$k] = $v->getTimestamp(); // datetime → timestamp
} elseif (is_resource($v) && get_resource_type($v) === 'stream') {
$row[$k] = stream_get_contents($v); // ntext/image → 文本/二進制
} elseif ($v === null) {
$row[$k] = null;
}
}
$batch[] = array_values($row);
if (count($batch) >= $batch_size) {
$dst->beginTransaction();
foreach ($batch as $vals) $ins->execute($vals);
$dst->commit();
$total += count($batch);
$batch = [];
echo "已遷移 $total 行\n";
}
}
// 剩餘批次
if ($batch) {
$dst->beginTransaction();
foreach ($batch as $vals) $ins->execute($vals);
$dst->commit();
$total += count($batch);
}
echo "遷移完成,共 $total 行。";
// 類型映射函數 (簡化版)
function map_type($sql_type, $len, $prec, $scale) {
static $map = [
'int' => 'INT', 'bigint' => 'BIGINT', 'smallint' => 'SMALLINT', 'tinyint' => 'TINYINT',
'bit' => 'TINYINT(1)',
'decimal' => "DECIMAL($prec,$scale)", 'numeric' => "DECIMAL($prec,$scale)",
'money' => 'DECIMAL(19,4)', 'smallmoney' => 'DECIMAL(10,4)',
'float' => 'DOUBLE', 'real' => 'FLOAT',
'datetime' => 'BIGINT', 'datetime2' => 'BIGINT', 'smalldatetime' => 'BIGINT', 'date' => 'DATE', 'time' => 'TIME',
'char' => "CHAR($len)", 'nchar' => "CHAR($len)",
'varchar' => "VARCHAR($len)", 'nvarchar' => "VARCHAR($len)",
'text' => 'TEXT', 'ntext' => 'LONGTEXT',
'image' => 'LONGBLOB',
'uniqueidentifier' => 'CHAR(36)',
'xml' => 'LONGTEXT',
];
return $map[strtolower($sql_type)] ?? 'LONGTEXT';
}
| 階段 | 關鍵動作 | 驗收標準 |
|---|---|---|
| 準備 | 目標庫預建表、索引、分區;凍結 Schema 變更 | DDL 執行無錯誤,SHOW CREATE TABLE 符合預期 |
| 全量遷移 | 腳本跑批、斷點續傳、進度監控、錯誤重試、數據校驗 (行數、MD5、抽樣對比) | 全表行數一致,抽樣 1000 行字段值逐個一致 |
| 增量同步 | CDC (Debezium/Maxwell/Canal) 或觸發器/時間戳輪詢捕獲增量,應用至目標 | 延遲 < 5 分鐘,無數據丟失/重複 |
| 雙寫驗證 | 應用層雙寫 / 讀流量鏡像 / Shadow Table 對比 | 連續 7 天零差異 |
| 切換上線 | 停寫源庫 → 最後一增量 → DNS/配置切換 → 觀察監控 | 業務零感知,回滾預案就緒 |
異構遷移的本質是 **「數據類型映射 + 業務語義保持 + 運維可控性」**。ODBC 直導看似省力,實則將最關鍵的類型轉換、清洗、錯誤處理交給了不透明的向導,風險不可控。投入精力編寫/複用遷移腳本框架,才是專業團隊的選擇。記住:**預建目標表、流式讀寫、批次事務、校驗對比、增量同步、雙寫驗證**——這六步走穩,遷移才不會成「遷坟」。