整体云迁移

数据库迁移最难的不是搬数据,是搬存储过程

同构迁移(MySQL→RDS MySQL)通常一周内能搞定。异构迁移(Oracle→PostgreSQL)的工作量主要在代码转换和回归测试上。

迁移复杂度评估

场景复杂度主要工作典型周期
MySQL → RDS MySQLDMS 全量 + CDC,参数对齐1-2 周
PostgreSQL → RDS PostgreSQL同上,注意扩展兼容性1-2 周
SQL Server → RDS SQL Server备份还原或 DMS,许可确认1-3 周
Oracle → RDS OracleBYOL 许可、参数与特性对齐2-4 周
SQL Server → PostgreSQLT-SQL 转 PL/pgSQL、类型映射、应用改造8-16 周
Oracle → PostgreSQL很高PL/SQL 转换、包与触发器重写、性能回归12-24 周

异构迁移的周期主要由存储过程数量和业务逻辑复杂度决定。SCT 能自动转换 60%-80%,剩下的必须人工处理。

异构迁移六阶段

  1. 01

    评估与转换分析

    用 SCT 扫描源库,生成转换报告,统计自动转换率与需人工处理的对象清单。

    交付物:SCT 评估报告、人工改造工作量清单、风险项列表

    1-2 周

  2. 02

    Schema 转换

    自动转换 + 人工修正表结构、索引、约束、序列。注意数据类型精度差异(如 NUMBER 到 NUMERIC)。

    交付物:目标 Schema DDL、类型映射表、差异说明文档

    2-4 周

  3. 03

    代码改写

    存储过程、函数、触发器逐个转换。这是最耗时的部分,需要开发深度参与。

    交付物:转换后的 PL/pgSQL 代码、单元测试用例

    4-12 周

  4. 04

    应用适配

    改数据库驱动、连接串、方言相关的 SQL、分页语法、序列获取方式。

    交付物:应用改造代码、配置变更清单

    2-6 周

  5. 05

    数据迁移与校验

    DMS 全量 + CDC 增量。校验分三层:行数、校验和、业务关键字段抽样比对。

    交付物:数据一致性报告、校验脚本、差异处理记录

    2-4 周

  6. 06

    性能回归与切换

    对比关键 SQL 的执行计划与耗时,补索引、调参数,达标后按演练流程切换。

    交付物:性能对比报告、优化记录、切换 Runbook

    2-4 周

数据一致性校验脚本

python verify_migration.py
"""迁移数据一致性校验:行数 → 校验和 → 抽样比对,三层逐步收紧。"""

from __future__ import annotations

import hashlib
from dataclasses import dataclass, field
from typing import Any, Sequence


@dataclass
class TableCheck:
    table: str
    key_column: str
    compare_columns: Sequence[str]
    source_rows: int = 0
    target_rows: int = 0
    checksum_match: bool = False
    sample_diffs: list[dict[str, Any]] = field(default_factory=list)

    @property
    def passed(self) -> bool:
        return (
            self.source_rows == self.target_rows
            and self.checksum_match
            and not self.sample_diffs
        )


def count_rows(conn, table: str) -> int:
    with conn.cursor() as cur:
        cur.execute(f"SELECT COUNT(*) FROM {table}")
        return cur.fetchone()[0]


def table_checksum(conn, table: str, columns: Sequence[str], key: str) -> str:
    """按主键排序后计算整表内容的 MD5,用于快速判断是否完全一致。

    注意:跨引擎的类型格式化差异会导致校验和不同,
    所以要统一转成文本并处理 NULL 与数值精度。
    """
    col_expr = ", ".join(f"COALESCE(CAST({c} AS CHAR), '~NULL~')" for c in columns)
    with conn.cursor() as cur:
        cur.execute(f"SELECT {col_expr} FROM {table} ORDER BY {key}")
        h = hashlib.md5()
        for row in cur:
            h.update("|".join(row).encode("utf-8"))
    return h.hexdigest()


def sample_compare(
    src_conn, tgt_conn, table: str, key: str,
    columns: Sequence[str], sample_size: int = 1000,
) -> list[dict[str, Any]]:
    """随机抽样逐行比对,捕捉校验和无法暴露的精度类问题。"""
    col_list = ", ".join(columns)
    with src_conn.cursor() as cur:
        cur.execute(
            f"SELECT {key}, {col_list} FROM {table} "
            f"ORDER BY RAND() LIMIT {sample_size}"
        )
        src_rows = {r[0]: r[1:] for r in cur.fetchall()}

    if not src_rows:
        return []

    keys = ", ".join(repr(k) for k in src_rows)
    with tgt_conn.cursor() as cur:
        cur.execute(
            f"SELECT {key}, {col_list} FROM {table} WHERE {key} IN ({keys})"
        )
        tgt_rows = {r[0]: r[1:] for r in cur.fetchall()}

    diffs: list[dict[str, Any]] = []
    for k, src_val in src_rows.items():
        tgt_val = tgt_rows.get(k)
        if tgt_val is None:
            diffs.append({"key": k, "issue": "目标端缺失"})
        elif tuple(map(str, src_val)) != tuple(map(str, tgt_val)):
            diffs.append({
                "key": k, "issue": "值不一致",
                "source": src_val, "target": tgt_val,
            })
    return diffs


def run_checks(src_conn, tgt_conn, checks: list[TableCheck]) -> tuple[bool, list[TableCheck]]:
    for chk in checks:
        chk.source_rows = count_rows(src_conn, chk.table)
        chk.target_rows = count_rows(tgt_conn, chk.table)

        if chk.source_rows != chk.target_rows:
            continue  # 行数就不对,没必要往下算

        src_sum = table_checksum(src_conn, chk.table, chk.compare_columns, chk.key_column)
        tgt_sum = table_checksum(tgt_conn, chk.table, chk.compare_columns, chk.key_column)
        chk.checksum_match = src_sum == tgt_sum

        if not chk.checksum_match:
            chk.sample_diffs = sample_compare(
                src_conn, tgt_conn, chk.table, chk.key_column, chk.compare_columns
            )

    all_passed = all(c.passed for c in checks)
    return all_passed, checks

校验必须在切换窗口内跑完,所以大表要提前测好耗时。超过 20 分钟的校验建议改成分区并行。

下一步

把云的复杂度交给我们,你只管做业务

留下需求,沐杉云的解决方案架构师会在一个工作日内联系你,提供免费的现状评估、迁移方案草案与 TCO 测算表。