拒绝停服,随时回退:MS SQL 到 ≈ 的无缝数据库双向迁移方案

前篇回顾:《Sybase 到 PostgreSQL 的无缝数据库双向迁移方案》

  • 客户背景:某公营机构,负责管理地区医疗数据与公共卫生信息

  • 需求解析

    本项目旨在将关键业务应用从 MS SQL 数据库平滑迁移至 PostgreSQL。

    • 从组织战略层面:希望统一所有数据库到 PostgreSQL,简化架构、降低运维复杂性;

    • 从成本层面:PostgreSQL 为开源数据库,无需像 MS SQL、Sybase 那样付费购买高昂的许可证(license);

    • 从生命周期管理角度:MS SQL Server 2016 已进入"延长支持"阶段(Mainstream 支持已于 2021 年结束,延长支持将于 2026 年结束),意味着无法获得新功能,升级则需重新采购新版本许可证,进一步推动了客户的替代意愿。

项目概述

在成功完成 Sybase ASE 替代项目后,该公营机构持续推进其数据库平台的统一与现代化。为降低长期运维成本、减少多数据库并行带来的架构复杂性,机构决定将各业务系统逐步迁移至 PostgreSQL 平台,实现集中管理、统一技术栈和完全开源化,从而不再依赖多个商用数据库的许可证采购与升级开销。

本次项目聚焦于一套基于 Microsoft SQL Server 2016 的关键业务系统。该数据库版本自 2021 年起已停止主流支持,目前处于"延长支持"阶段,仅提供安全补丁,无新功能更新,预计将于 2026 年彻底停止支持。若继续使用或升级,将面临额外的许可证采购成本。基于性能、可维护性与成本等多重考量,客户决定将该系统迁移至 PostgreSQL,并希望迁移过程具备"低中断、强可控、可回退"的能力。

为了确保迁移过程中的数据完整性、准确性与系统可用性,并尽可能缩短业务中断时间,我们为客户设计并实施了一套具备实时同步与容灾能力的数据迁移方案。通过建立 MS SQL 到 PostgreSQL 的实时复制链路,完成数据验证后将应用系统切换至新平台,并反转同步方向,实现 PostgreSQL 到 MS SQL 的双向保持同步,确保在并行运行期内可随时回退。这一方案既保障了业务连续性,也为后续更多数据库的统一迁移提供了可复制的技术路径。

步骤1:安装 TapData 并配置数据库

TapData 与 MongoDB 部署在三台服务器上,构建了一套高可用、负载均衡的三节点TapData集群架构。该架构的核心特性包括:

  1. 高可用性:若任一节点故障,剩余节点可自动接管服务,避免单点故障导致的系统停机;

  2. 负载均衡:流量通过均衡机制均匀分配至所有节点,防止单节点过载,确保整体性能不会受到影响,从而提升系统响应速度与稳定性;

  3. 数据一致性:通过集群内同步机制保障节点间数据一致,即使节点故障或切换时,数据仍可完整保留,确保业务连续性与可靠性。

📖 相关文档:部署高可用 TapData Enterprise(三节点)

步骤2:数据库对象迁移(从 MS SQL 到 PostgreSQL)

基础表结构的迁移外,这里还涉及一系列数据库对象的迁移工作,并需充分评估这些对象对数据同步链路的影响,主要包括:逻辑视图、物化视图、存储过程、触发器、索引、约束、序列、自定义默认值等。即便是基础表结构本身,也需要对自增列(identity columns)、主键、外键以及字段类型映射等细节进行精细调整。尽管 MS SQL 与 PostgreSQL 在 SQL 语法层面具有较高的相似性,但仍存在一些兼容性差异,需在迁移中加以处理。

在本项目中,我们使用 TapData 工具完成大部分对象的自动迁移,包括字段类型转换、索引与序列同步等。需要人工迁移的对象主要包括视图、存储过程、触发器和检查约束(check constraints)。TapData 提供的自动化能力显著降低了人工干预成本,确保迁移过程的准确性与高效性。

字段类型映射

在进行跨数据库的数据同步时,不可避免地会遇到源库和目标库之间某些字段类型无法精确对应的情况,此时需要选择一个尽可能接近的目标类型来替代。这类"非精确映射"可能导致两个数据库之间的数据取值范围或精度存在差异。

由于原系统基于 MS SQL 构建,我们在设计 PostgreSQL 字段类型时优先选择了范围或精度更大的类型,以确保从 MS SQL 迁移到 PostgreSQL 时不会发生数据精度的损失。然而,这也带来了一个潜在风险:当同步方向反转(即从 PostgreSQL 写回 MS SQL)时,可能出现精度截断的问题。

考虑到 PostgreSQL 的写入数据仍由同一套业务系统(经适配后运行在 PostgreSQL 上)生成,因此理论上不会产生超出原 MS SQL 模式定义范围的数据,从而在一定程度上降低了这一风险。

不过,我们在实践中仍发现一个例外情况:MS SQL 的 Datetime2(7) 精度高于 PostgreSQL 的 Timestamp(6),若保持原精度,将导致字段无法一一对应。为确保时间精度在迁移前后完全一致,我们最终选择在迁移前主动将源库字段精度从 Datetime2(7) 降至 Datetime2(6),从而实现与 PostgreSQL Timestamp(6) 的精确对齐。

主键 / 唯一性字段的精度考量

当某些字段被用作主键(Primary Key)或用于数据匹配时,需格外注意字段精度带来的影响。因为即便字段类型在 MS SQL 和 PostgreSQL 中名称相同,若精度存在差异,也可能导致主键匹配失败。例如,两个系统都支持 DateTime2Timestamp 类型,但它们的默认精度并不一致。

在 MS SQL 中,DATETIME2 默认使用 7 位精度(即 0.0000001 秒,0.1 微秒),但用户可以通过 DATETIME2(n) 指定精度范围为 0 到 7 位。而在 PostgreSQL 中,无论是 TIMESTAMP WITHOUT TIME ZONE 还是 TIMESTAMP WITH TIME ZONE,默认精度为 6 位(即 0.000001 秒,1 微秒),精度范围为 TIMESTAMP(n) 中的 0 到 6 位。

因此,MS SQL 的默认精度比 PostgreSQL 多一位(DATETIME2(7) vs TIMESTAMP(6))。这种差异意味着当从 DATETIME2(7) 迁移至 TIMESTAMP(6) 时,将丢失最末一位精度。如果该字段参与主键或唯一性约束,则可能引发两个问题:一是主键冲突的微小风险;二是更常见的情况------迁移后难以精确匹配原始记录,影响数据一致性。

考虑到业务系统本身并不依赖 7 位时间精度,而且迁移后也计划使用 6 位精度,我们最终决定在迁移前对源端 MS SQL 模式结构进行手动调整,将 DATETIME2(7) 字段修改为 DATETIME2(6),实现与 PostgreSQL 的字段精度完全一致。该变更在测试环境中进行验证后再在生产环境中实施,对应用逻辑没有任何影响,系统运行亦无需修改。

执行该字段精度调整的 SQL 语句如下:

sql 复制代码
ALTER TABLE your_table_name ALTER COLUMN your_column_name DATETIME2(6);

这一简单的模式优化,有效规避了因字段精度不一致而带来的主键匹配异常,提升了整个迁移链路的稳定性与可靠性。

字段类型映射表

以下是本项目中 TapData 默认的字段类型映射规则(详见下表)。虽然该映射表并未涵盖 MS SQL 中所有可能的字段类型,但已经覆盖了本项目中实际使用的全部字段类型。需要说明的是,TapData 支持自定义字段类型映射配置,如默认映射不满足实际需求,可灵活调整以适配不同业务场景。

类型类别

MS SQL 类型

PostgreSQL 类型

Character

CHAR

CHAR

NCHAR

CHAR

NVARCHAR(n), n<4000

VARCHAR

NVARCHAR(n), n>=4000

TEXT

TEXT

TEXT

VARCHAR(n), n<4000

VARCHAR

VARCHAR(n), n>=4000

TEXT

Integer

BIGINT

BIGINT

INT

INT

SMALLINT

SMALLINT

TINYINT

SMALLINT

Numeric, decimal, float

NUMERIC

NUMERIC

DECIMAL

DECIMAL

Date/Time

DATE

DATE

DATETIME

TIMESTAMP(3)

DATETIME2(n), 0<=n<=7

TIMESTAMP(n), 0<=n<=6

SMALLDATETIME

TIMESTAMP(0)

TIME

TIME

Money

MONEY

MONEY

SMALLMONEY

MONEY

Binary

BINARY

BYTEA

VARBINARY

BYTEA

IMAGE

BYTEA

TIMESTAMP

BYTEA

Bit

BIT

BOOLEAN

XML

XML

XML

部分字段类型映射还涉及数据值的转换,例如 BIT 类型在 MS SQL 中使用 0/1 表示布尔值,而在 PostgreSQL 中则对应 BOOLEAN 类型的 true/false。对于这类情况,TapData 会自动完成数据值的转换,无需额外人工处理。

字符类型映射及大小写处理

1.VARCHAR(n) / NVARCHAR(n),n < 4000 映射为 PostgreSQL 的 VARCHAR(n)

  • 在 MS SQL 中,VARCHAR(n) 用于存储单字节 ASCII 字符,最多可容纳 n 个字符。

  • NVARCHAR(n) 则用于存储双字节 Unicode 字符,同样最多 n 个字符。

  • 在 PostgreSQL 中,VARCHAR(n) 使用 UTF-8 编码,支持多字节字符(每个字符占用 1--4 字节),最多 n 个字符。

因此,将 MS SQL 中的 VARCHARNVARCHAR 映射到 PostgreSQL 的 VARCHAR 是兼容的,原有数据在 PostgreSQL 中能够被完整存储。而在同步方向反转的场景中(即 PostgreSQL 写回 MS SQL),虽然可能需要将字符重新编码为单字节或双字节格式,但由于使用的仍是同一个应用系统,字符编码变化风险极低,基本不会导致问题。

2. MSSQL 的 TEXT 映射为 PostgreSQL 的 TEXT

需要注意的是,MS SQL 的 TEXT 类型从 SQL Server 2005 起就已不推荐使用 ,官方建议替代类型为 VARCHAR(MAX)

  • MSSQL 的 TEXT 类型不支持 Unicode ,而 PostgreSQL 的 TEXT 类型采用的是UTF-8 编码

  • MSSQL 的 TEXT 字段理论最大长度约为 2GB ,而 PostgreSQL 的 TEXT 最大支持 1GB

这意味着在极端场景下(例如一个字段中塞入超过 1GB 的文本,相当于 1000 本 400 页小说的内容),可能出现超出 PostgreSQL 字段限制的情况。但在本项目中,并未遇到如此大体量的文本字段,因此未构成实际风险。除非业务非常特殊,一般也无需担心该类极端情况。

数值类型映射

  1. BIGINT、INT、SMALLINT、NUMERIC 和 DECIMAL 类型 这些常见的数值类型在 MS SQL 与 PostgreSQL 之间的编码方式、数值范围和精度均保持一致,因此可以直接进行一对一映射,无需额外转换或处理。

  2. MSSQL 的 TINYINT → PostgreSQL 的 SMALLINT 在 MSSQL 中,TINYINT 是一种占用 1 字节 的整数类型,数值范围为 0 到 255 。由于 PostgreSQL 并不提供与 TINYINT 完全对应的类型,我们采用将其映射为 SMALLINT 的策略。PostgreSQL 的 SMALLINT 占用 2 字节 ,支持的范围更广,为 -32,768 到 32,767 ,因此完全覆盖了 TINYINT 的取值范围。这种映射虽然在存储上略有冗余,但在功能上是安全的,能够确保数值不会因范围差异而丢失或溢出。

日期 / 时间类型映射

1.DATE 类型

PostgreSQL 的 DATE 类型比 MS SQL 的 DATE 类型支持更广泛的日期范围。

  • 在 MSSQL 中,DATE 类型的取值范围是从 公元 0001 年 1 月 1 日9999 年 12 月 31 日 ,采用 3 字节编码

  • 而 PostgreSQL 的 DATE 类型使用 4 字节编码 ,支持从 公元前 4713 年公元 5874897 年 的日期范围。

尽管 PostgreSQL 的取值范围更大,但在大多数业务场景中(包括本项目),这种超长日期范围并无实际需求,因此该差异不构成迁移障碍。

2.DATETIME 与 TIMESTAMP(3)

PostgreSQL 的 TIMESTAMP(3)(精确到毫秒)相比 MSSQL 的 DATETIME(约 3.33 毫秒精度)精度更高。在迁移方向从 MSSQL 到 PostgreSQL 时不会有问题,但在反向同步时(PostgreSQL 写回 MSSQL),若该字段参与主键匹配,则存在因精度不一致而导致匹配失败的风险。

本项目中,该字段类型未用于主键,因此无需额外处理。但若该字段构成主键,建议在迁移前验证是否会引发主键冲突,并预设冲突处理机制。

3.SMALLDATETIME 映射

MSSQL 的 SMALLDATETIME 精度为 1 分钟 ,而 PostgreSQL 的 TIMESTAMP(0) 精度为 1 秒,同时支持更广泛的日期范围(同上所述)。由于此类型在项目中未用于主键,且迁移方向为从低精度到高精度,因此该映射是可行且安全的。

4.DATETIME2(n) 与 TIME(n)

MSSQL 的 DATETIME2(n)TIME(n) 与 PostgreSQL 中的 TIMESTAMP(n)TIME(n) 类型基本一致,均通过 n 指定小数秒位数,用于控制精度。但需注意两者对 n 的支持范围不同:

  • MSSQL 支持 n 的取值范围为 0~7 ,默认值为 7,精度高;

  • PostgreSQL 支持的取值范围为 0~6 ,默认值为 6

因此,当字段用于主键时,若未主动调整 MSSQL 端的精度,可能会在数据从 PostgreSQL 写回 MSSQL 时引发主键冲突或匹配失败------尤其是 PostgreSQL 使用 6 位精度的数据写回 MSSQL 默认的 7 位字段,会因为四舍五入而无法精准匹配原始记录。

项目中的处理方式如下:

为规避上述精度不一致问题,我们首先在 MSSQL 中运行了一组 SQL 查询,用于验证将 7 位精度的时间字段截断为 6 位后是否会导致主键冲突。示例如下:

scss 复制代码
SELECT CAST(DtmField AS datetime2(6)) AS TruncatedDtm, OtherPrimaryKeyField, COUNT(*) as cnt
FROM MyTable
GROUP BY CAST(DtmField AS datetime2(6)), OtherPrimaryKeyField
HAVING COUNT(*) > 1;

在本项目中,查询结果为零,说明无任何主键因截断精度而冲突。

随后,我们使用如下语句将 MSSQL 中的字段精度从 7 位降至 6 位,与 PostgreSQL 完全对齐:

sql 复制代码
ALTER TABLE MyTable
ALTER COLUMN DtmField DATETIME2(6);  // Add NOT NULL if needed

这一调整确保了两端在精度与编码上保持一致,实现了无损的数据匹配与同步,无需再引入额外的处理逻辑。同时,由于字段仍为 DATETIME2 类型,应用程序层无需做任何改动,整体方案简洁高效。

货币类型映射

1.MSSQL 的 MONEY / SMALLMONEY → PostgreSQL 的 MONEY(默认映射)

TapData 默认将 MSSQL 的 MONEYSMALLMONEY 映射为 PostgreSQL 的 MONEY 类型,但两者的底层实现存在较大差异,需引起注意。

虽然两种数据库中 MONEY 字段的存储范围大致相同,均采用 8 字节编码,但:

  • MSSQL 的 MONEY 类型具有固定精度,支持最多 19 位有效数字,小数点后保留 4 位;

  • PostgreSQL 的 MONEY 类型则具备区域敏感性(locale-aware),默认保留 2 位小数,并可能受本地格式、货币符号等影响;

  • 两者在舍入规则(rounding behavior)上也存在不同。

基于以上差异,实际项目中我们避免使用 PostgreSQL 的 MONEY 类型,而是选择使用 NUMERIC(20,4) 类型以保证精度和兼容性,并通过 TapData 的自定义字段类型映射功能完成调整。

2.MSSQL 的 MONEY / SMALLMONEY → PostgreSQL 的 NUMERIC(20,10)

从技术实现上看:

  • MSSQL 的 MONEY 类型采用 8 字节存储SMALLMONEY4 字节存储 ,均支持小数点后 4 位精度

  • PostgreSQL 的 NUMERIC(20,10) 类型采用变长编码 ,支持最多 20 位有效数字10 位小数 ,其数值范围和精度均优于 MSSQL 的 MONEY 类型

因此,在需要更高精度或控制货币计算准确性的场景下,NUMERIC(20,10) 是更优选择。本项目中,我们最终统一采用 NUMERIC(20,4) 类型来替代所有 MONEY 字段,以实现更稳定的一致性及跨系统数据对齐。

二进制类型映射

1. PostgreSQL 的 BYTEA 类型

PostgreSQL 使用 BYTEA 类型来存储二进制数据,该类型支持变长编码,最大容量为 1GB,适用于大多数常见的二进制存储场景。

2. MSSQL 的 BINARY(n)

MSSQL 的 BINARY(n) 类型用于存储固定长度的原始二进制数据,存储长度由参数 n 明确定义,长度固定不可变

3. MSSQL 的 VARBINARY(n)

VARBINARY(n) 是变长二进制类型,其最大长度取决于 n 的设置:

  • n <= 8000 时,字段最大容量为 n 字节;

  • 当设置为 VARBINARY(MAX) 时,最大容量可达 2GB,适用于大容量文件或图像的存储。

4. MSSQL 的 IMAGE 类型(已弃用)

IMAGE 是 MSSQL 中的旧版二进制字段类型,早已被微软标记为弃用,推荐使用 VARBINARY(MAX) 替代。两者的最大容量均为 2GB ,但在新项目或迁移场景中应尽量避免继续使用 IMAGE 类型。

5. MSSQL 的 TIMESTAMP 类型(特殊处理)

TIMESTAMP 字段在 MSSQL 中占用 8 字节存储空间 ,但其值为系统自动生成,不允许显式写入。在迁移过程中不应将其作为普通字段处理,具体映射策略将在下文"时间戳特殊处理"部分详述。

为防止数据在迁移过程中因字段容量不一致而发生截断或丢失,我们对 MSSQL 中所有 VARBINARY(MAX)IMAGE 字段进行了预检查,确保其单条数据不超过 PostgreSQL BYTEA 的 1GB 限制。这一步验证是确保迁移稳定性与数据完整性的关键环节。

TIMESTAMP 字段处理(ROWVERSION)

MSSQL 中的 TIMESTAMP 字段类型(现更推荐使用术语 ROWVERSION)是一种系统自动生成的 64 位二进制值,具有全库唯一性,即便是在不同表之间,该值也不会重复。它以单调递增的方式自动更新,常被用于乐观并发控制或数据变更追踪,以确保用户更新的数据自读取后未被其他事务修改。

需要注意的是,ROWVERSION 字段的值不能手动插入或更新,只能由数据库系统自动分配。

PostgreSQL 并没有与之功能完全对等的字段类型。如果应用程序原本依赖 TIMESTAMP/ROWVERSION 实现并发控制,则在 PostgreSQL 环境中需要采用其他替代机制,例如:

  • 使用 PostgreSQL 的系统隐藏列 XMIN 实现版本控制;

  • 或显式在表中增加一个 version 字段,手动维护版本号。

不过,XMIN 字段存在一些限制------其值不会在 VACUUM FULLCLUSTER 操作或某些数据复制场景中保留。因此并不适用于所有使用场景。

在本项目中,应用开发团队自行实现了替代机制,因此在 PostgreSQL 端无需构建新的版本控制逻辑。然而,出于故障排查与数据匹配的需求,我们仍需将 MSSQL 的 TIMESTAMP 字段同步至 PostgreSQL。

虽然可将其映射为 BINARY(8)BYTEA 类型,但我们最终选择将其映射为 BIGINT 类型,原因如下:

  • BIGINTROWVERSION 一样为 64 位;

  • 更易于 排序和建立索引;

  • 在排序行为上与 MSSQL 的 TIMESTAMP 字段保持一致。

此映射方案确保了从 MSSQL 到 PostgreSQL 的 前向同步精度一致,数据完全匹配,即便 TIMESTAMP 字段参与主键,也不会引发一致性问题,便于后续的数据校验与验证。

需要注意的是,当 TIMESTAMP 字段作为主键时,无法支持反向同步(即 PostgreSQL → MSSQL),因为 MSSQL 不允许写入该字段。除非在 MSSQL 表结构中新增一个用于匹配的 BIGINT 字段作为替代主键,否则无法实现 PostgreSQL 回写到 MSSQL 的匹配操作。

在本项目中,为避免修改 MSSQL 表结构,且这些包含 TIMESTAMP 主键的表主要用于日志记录,因此我们选择在反向同步过程中排除这些表。该策略兼顾了系统稳定性与开发代价,是在不影响业务前提下的合理取舍。

BIT 类型映射

MSSQL 中的 BIT 类型在本项目中被映射为 PostgreSQL 的 BOOLEAN 类型。两者本质上都只支持两个取值(加上 NULL),即具有相同的精度和表达能力:

  • MSSQL 中的 BIT 取值为 01

  • PostgreSQL 中的 BOOLEAN 取值为 FALSETRUE

尽管数据表达含义一致,但取值格式不同,因此在迁移和同步过程中需要对字段值进行转换。

TapData 在执行双向同步以及数据校验时,会自动完成 BIT 与 BOOLEAN 之间的取值转换,确保数据逻辑一致、迁移过程无需手动干预。

XML 类型映射

在 MSSQL 和 PostgreSQL 中,XML 都属于原生支持的数据类型,并均支持 XPath / XQuery 查询,因此在本项目中实现 XML 数据的同步与校验相对简单直接。

不过,两者在功能层面存在一定差异:

  • PostgreSQL 的 XML 类型主要支持 文本存储 与 DOM 解析 API;

  • MSSQL 的 XML 类型则提供更丰富的特性,例如 基于 XSD 的模式校验、XML 索引等高级功能。

由于这些差异并不影响数据复制和校验的正确性,因此本项目未对其做特殊处理,相关兼容性问题由应用开发团队在业务层面进行适配与调整。

空格填充处理

在 MSSQL 和 PostgreSQL 中,某些字段类型(如 CHAR(n)NCHAR(n))在存储时会自动填充空格以达到固定长度,这种行为在两个数据库中是一致的。

但如果将这类固定长度字段映射为可变长度类型(如 VARCHAR(n)),则填充的空格可能会在数据校验时表现为差异,从而影响验证结果。在本项目中,我们通过合理的字段类型映射策略规避了该类差异带来的问题。

此外,TapData 在同步过程中会自动处理此类空格问题:

  • 从 MSSQL 读取 CHAR()NCHAR() 数据时,会自动去除字段右侧的填充空格;

  • 写入 PostgreSQL 时,若目标字段类型仍为 CHAR(),则 PostgreSQL 会再次自动补足空格。

需要注意的是,在 应用层面的行为上存在一个重要差异:

  • MSSQL 在进行字符串比较时,会保留并考虑末尾的空格;

  • 而 PostgreSQL 在比较时会忽略尾部空格。

虽然这一差异在本项目中并未带来任何影响,但对于依赖精确字符串比较的业务逻辑,应用团队可能需要额外处理该行为差异以确保逻辑一致性。

主键与自增列处理

主键处理策略

在源数据库中,并非所有表都显式定义了主键。然而,对于基于 CDC(变更数据捕获)的同步机制而言,主键是识别插入与更新操作的关键,缺失主键会导致系统无法判断数据变化的类型。

在 TapData 中,这类用于标识记录更新的数据列被称为 "更新字段"(Update Fields)。当源表未定义主键时,可采取以下两种策略:

  1. 临时将表中所有字段作为联合主键,用于区分变更记录。此方法简单直接,适用于数据量较小或字段不多的场景。

  2. 添加哈希主键字段(HashKey) :为避免在 MSSQL 端修改表结构,我们在部分表的 PostgreSQL 目标表中新增了一个 hashkey 字段,并将其设置为主键。该字段由 TapData 自动生成,基于记录内容计算得出,确保每条记录具有唯一标识。

TapData 原生支持该功能,用户仅需在配置中启用相关选项,系统会自动在目标表中添加并维护 hashkey 字段,无需手动建表或脚本干预。此方法不仅保留了源端结构的完整性,也保障了同步链路的准确性与稳定性。

自增列(Identity Columns)

自增列是一种用于自动生成唯一数值的字段类型,通常用于主键。这一机制在 MSSQL 与 PostgreSQL 中均有支持,但在实现细节上存在差异。具体来说:

  • 在 MSSQL 中,自增列不仅可以使用 INTBIGINT,还可以使用 NUMERIC(20,0)DECIMAL(20,0) 类型,只要不包含小数部分即可;

  • 而在 PostgreSQL 中,不允许将 NUMERICDECIMAL 类型定义为自增列,否则会在建表或数据插入时抛出错误。

为解决这一兼容性问题,TapData 在识别到 MSSQL 的 NUMERIC(20,0) 字段被标记为 IDENTITY 时,不会将其直接映射为 PostgreSQL 的 NUMERIC(20),而是自动转换为 BIGINT 类型,以满足 PostgreSQL 的自增列定义规则。

换言之,若直接将 MSSQL 的 NUMERIC IDENTITY 字段映射为 PostgreSQL 的 NUMERIC IDENTITY 字段,将导致建表失败。而借助 TapData 的智能类型识别与映射机制,无需手动调整即可避免此类错误,保障迁移过程的顺利进行。

自增列与 CDC 的协同处理

由于本项目采用了 CDC(变更数据捕获)机制,从 MSSQL 捕捉变更并同步至 PostgreSQL,因此在处理自增列(Identity Columns)时,若不加以控制,可能会引发严重的数据一致性问题。例如:

如果目标端 PostgreSQL 使用自身的自增机制生成主键,而非复用 MSSQL 源端生成的 ID 值,那么一旦两端生成的 ID 不一致,就会导致各种异常情况。尤其当这些 ID 被用作外键存在于其他表中时,问题会进一步放大------这些关联表往往使用的仍是 MSSQL 生成的原始 ID,而不是 PostgreSQL 自动生成的新值。

常见导致 ID 不一致的场景包括但不限于:

  • 数据更新顺序发生错乱;

  • 某次变更未被正确捕捉或写入;

  • 两端的自增序列发生偏移(如手动调整了序列值而未同步)。

为避免此类风险,我们的策略是:确保所有自增列允许显式插入主键值,即在目标数据库中保留 MSSQL 生成的原始 ID。这也为将来可能发生的故障回退(fail-over)提供了基础,确保同步链路可逆。

在 PostgreSQL 中,可通过如下方式创建具备此能力的自增列:

vbnet 复制代码
GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY

这种方式允许用户显式插入主键值,同时保留自动生成的能力。

但在 MSSQL 中,自增列的处理更加复杂。若想在插入时覆盖自动生成的值,必须启用 SET IDENTITY_INSERT ON ,且一次仅能对一个表启用。如果开启该设置的同时尝试对第二个表操作,MSSQL 会自动关闭第一个表的设置。

幸运的是,TapData 已自动封装并处理了这一限制 :当检测到表中存在 IDENTITY 列时,会在写入前动态执行 SET IDENTITY_INSERT ON,确保 MSSQL 能正确接收来自 PostgreSQL 的主键值,且无须用户手动干预。

需要注意的是,MSSQL 中也支持更灵活的 SEQUENCE 机制用于生成唯一值,某些场景下可作为 IDENTITY 的替代。但本项目中未使用 SEQUENCE,因此不做展开说明。

自增主键编号的处理(Identity Numbers)

在实际迁移与同步过程中,如果对自增字段(Identity / Auto-increment)显式插入了主键值,那么数据库的自增序列并不会自动推进。这意味着下一次自动生成的值可能会重复,从而引发主键冲突。

因此,在某些场景下,我们需要手动更新目标库的序列值。以 PostgreSQL 为例,可以通过以下命令设置自增序列的起始值:

sql 复制代码
SELECT setval('EmployeeSeq', 50001);
ALTER TABLE Employee ALTER COLUMN EmployeeSeq SET GENERATED BY DEFAULT RESTART WITH 50001;

此外,在实际生产环境中,也需考虑故障切换(fail-over)场景:

当主库宕机、系统切换至备用库(新主库)时,原主库中某些未同步的事务可能尚未写入到新主库中。此时,如果新主库继续使用原主库最新 ID 的"顺延值"进行写入操作,就存在未来恢复原主库后发生主键冲突的风险。

为避免这种问题,推荐做法是:新主库的序列值不要紧跟原主库的最后 ID,而是跳过一段区间,以便将来恢复原主库后能够继续补数据而不冲突。

为了优雅处理这些细节问题,TapData 提供了一个非常实用的能力:支持为目标数据库的自增序列设置"页大小(Page Size)"同步策略。例如:

如果将页大小设置为 100,那么每当 TapData 从 MSSQL 读取一页新的变更日志时,它会在写入数据的同时,向 PostgreSQL 插入一条更新自增序列的 SQL 语句,将目标端序列值设置为源库当前序列值 + 100。

这样,当业务从 MSSQL 切换到 PostgreSQL 写入时,PostgreSQL 的序列值早已提前预留好一段"空档区",无需额外调整即可安全使用。虽然在主键编号上可能会出现跳号,但这不仅不会带来问题,反而可以在故障切换后更安全地恢复原主库未同步的记录,有效避免主键冲突。

通过这种机制,TapData 实现了以下优势:

  • 自动更新目标库的自增序列,无需人工维护;

  • 在主备切换时,序列值无需手动调整;

  • 有效降低故障恢复后的主键冲突风险;

  • 同步过程更加连续、可靠、具备容灾弹性。

这种内置的智能能力,极大简化了跨数据库自增字段同步与故障切换管理的复杂性。

外键与约束处理

在使用 CDC(变更数据捕获)进行跨表数据同步时,外键及其他跨表约束往往是问题高发的"雷区"。

这是因为,当源数据库执行涉及多行的更新、插入或删除操作时,这些操作在日志中被拆分记录为针对每一行的独立变更事件。在 CDC 驱动的复制过程中,每条变更通常被转换为一条独立的 SQL 操作在目标端执行。

为提升同步吞吐性能,TapData 提供"并行写入(Parallel Writes)"功能,可将日志中的数据变更操作分配到多个线程中并发执行。TapData 会确保:

  • 同一个表的所有更新操作由同一个线程顺序执行,以保持表内操作顺序一致;

  • 但跨表的写入顺序并不一定与源库完全一致。

这种并发机制虽然提升了性能,但若目标数据库中启用了外键约束,就可能因为某些表写入先后顺序不一致而触发外键校验失败,从而导致写入失败。

【PostgreSQL 中的解决方案】

为避免外键检查引发写入错误,TapData 在向 PostgreSQL 写入复制数据时,会自动在当前连接中临时关闭外键和其他约束校验,只对当前会话生效,不会影响其他连接。实现方式如下:

ini 复制代码
SET session_replication_role = 'replica';

这一机制使得即使启用了并行写入功能,数据也能顺利完成同步,无需手动干预。

【MSSQL 中的处理方式】

相比之下,MSSQL 不支持会话级别关闭约束 。因此,在将数据反向同步至 MSSQL 且涉及外键或触发器时,不建议启用并行写入功能,除非明确进行如下操作:

sql 复制代码
DISABLE TRIGGER ALL ON TableName;

该命令会关闭指定表上的所有触发器,适用于防止因触发器执行顺序不一致而导致同步失败。

但需注意:这类操作会影响整个数据库的所有用户和会话 ,因此 TapData 默认不会自动执行该命令,而是将控制权交给数据库管理员,由其根据实际情况决定是否启用或关闭。

当系统从 PostgreSQL 切换回 MSSQL 作为主库时,建议再执行如下命令以恢复原有触发器逻辑:

sql 复制代码
ENABLE TRIGGER ALL ON TableName;

通过以上机制,TapData 平衡了高性能并发同步数据完整性校验之间的关系,同时保留了足够的灵活性,便于企业按需配置数据复制策略。

默认值处理(Default Values)

理论上,默认值的迁移应是最为直接的一环,因为无论是 MSSQL 还是 PostgreSQL,默认值都只在插入(INSERT)操作时生效,不会对更新(UPDATE)操作产生影响。当然,用户也可以通过显式语句将字段更新为 DEFAULT,以还原为预设值,但这类用法不会带来兼容性问题。

举例说明:若源数据库中 quantity 字段的默认值为 1,目标库也设置为相同的默认值,以下几种场景的表现是可预测且一致的:

场景

源库行为

目标库行为

显式设置 quantity=3 插入

quantity=3

quantity=3

插入时未指定 quantity

quantity=1(使用默认值)

quantity=1(复制源库值)

直接在目标库插入空值

quantity=1(使用默认值)

显式设置 quantity=3 插入到目标库

quantity=3

从上述场景可以看出,默认值仅在插入时未指定字段值时才会生效,TapData 会将源端的数据如实同步至目标端,因此不会破坏数据一致性。

不过,MSSQL 与 PostgreSQL 在默认值的语法和功能支持方面仍存在一些差异。以时间字段为例,以下为两种数据库中定义默认值的语法差异:

MSSQL:

sql 复制代码
CREATE TABLE Employees (
    -- other fields...
    CreatedAt DATETIME2 DEFAULT SYSDATETIME()
);

PostgreSQL:

sql 复制代码
CREATE TABLE Employees (
    -- other fields...  
    CreatedAt TIMESTAMP DEFAULT NOW()
);

此外,PostgreSQL 在默认值的支持能力上更强,主要体现在以下几个方面:

  • 支持 ENUM 类型与数组(Array)类型字段的默认值;

  • 支持更复杂的表达式作为默认值;

  • 内建函数更丰富、灵活。

而 MSSQL 在上述方面的原生支持较为有限。

在本项目中,由于迁移方向为 MSSQL → PostgreSQL,从功能上是"从弱到强"的过程,因此默认值的兼容性并未成为阻碍。TapData 在迁移时保留源端字段默认值的定义,目标端可根据 PostgreSQL 的能力进一步扩展和优化。整体迁移过程无需特殊处理即可顺利完成。

索引处理(Indexes)

总体而言,索引的迁移过程相对简单,两个数据库之间的差异主要集中在支持的索引类型方面:

  • MSSQL 支持 CLUSTERED INDEX(聚簇索引),但 PostgreSQL 不支持

  • PostgreSQL 支持 表达式索引、函数索引以及部分索引(Partial Index),而 MSSQL 不具备这些功能

在本项目中,我们只需要处理 MSSQL 中的 CLUSTERED INDEX 相关内容。

PostgreSQL 提供了 CLUSTER 命令,可用于按某个索引对表进行物理排序,从而达到类似 MSSQL 聚簇索引的效果。但不同的是,PostgreSQL 的聚簇行为不会自动维护,而是需要定期手动执行 CLUSTER 操作。

由于 CLUSTER 操作只影响性能表现 ,不影响数据库的功能逻辑,因此可以在业务低峰时段由运维人员设定定时任务进行处理。

TapData 在迁移过程中不会自动执行 CLUSTER 命令,原因在于:

  • 对空表执行 CLUSTER 无实际效果;

  • TapData 无法判断何时执行最为合适;

  • 因此,聚簇操作的执行应由数据库管理员(DBA)自行安排调度。

在创建目标 PostgreSQL 表结构与索引时,TapData 会忽略 MSSQL 源端中与 CLUSTERED 相关的定义部分,以确保兼容性和创建流程的顺利执行。

另外一点需要说明的是,用于 CDC 数据更新的主键字段应当具备索引,以提升查找性能。对此,TapData 会自动检查并补充缺失的主键索引,无需用户手动操作。这一机制进一步提升了迁移后的目标数据库在增量同步过程中的执行效率与稳定性。

逻辑视图与物化视图(Logical and Materialized Views)

在数据库中,逻辑视图(Logical Views)和物化视图(Materialized Views)本质上是从基础表生成的派生结构,不会在数据库日志中记录更新操作,因此对 CDC(变更数据捕获)机制没有任何影响。

换句话说,视图的存在与否,并不会干扰 CDC 的变更捕捉流程。因此,无论是在源数据库还是目标数据库中,视图的创建与维护均可作为独立操作,无需与同步机制绑定。

本项目中,所有视图的迁移工作均采用手动方式完成,并未依赖特定工具自动迁移。TapData 在执行表结构和数据的同步时,会自动忽略视图定义,仅关注真实表和数据本身,从而避免在迁移过程中引入无效结构或报错。

视图的定义与重建建议交由数据库开发或维护人员按需完成,以确保业务逻辑在目标数据库中得以延续。

存储过程(Stored Procedures)

在 CDC(变更数据捕捉)机制下,存储过程本身不会被记录到数据库的变更日志中 。当执行包含调用存储过程的 SQL 时,CDC 捕捉到的仅是该存储过程所引发的实际数据变更(如 INSERT、UPDATE 或 DELETE 操作),这些操作才会被写入日志并用于数据同步。

因此,在大多数场景下,存储过程只需在源库中执行一次 ,其产生的数据变更会被 CDC 自动捕捉并同步至目标库。此时,目标库中虽然也可以定义相同的存储过程,但在未进行业务切换前,这些过程不会被实际调用或影响同步流程

需要注意的是,某些存储过程并不直接修改数据,而是用于执行 DDL(结构性变更)操作,如建表、改字段类型、删除索引等。

  • 如果系统配置了 CDC 同步结构变更(DDL replication),则此类操作与普通数据变更一样会被记录并同步;

  • 若未启用 DDL 同步,则需额外评估并手动执行对应的结构变更脚本,以确保目标库与源库保持结构一致。

由于 MSSQL 与 PostgreSQL 在存储过程的语法、能力和内置函数方面存在显著差异,因此每一个存储过程的迁移都需要单独分析和适配。这部分工作通常不由数据同步工具自动完成,而是由开发团队手动实现,因此超出了本项目文档的范围。

简而言之,TapData 关注的是数据层面的变更复制,而存储过程的迁移则需结合应用逻辑、数据库特性和目标环境进行逐一调整。

触发器(Triggers)

触发器本质上与存储过程类似,但其触发机制源于对数据的更新操作,这使得它们与 CDC(变更数据捕捉)机制之间的兼容性存在问题,因此在迁移过程中必须特别小心处理。

例如,如果在表 A 上定义了一个触发器,当表 A 被更新时会向表 B 插入一条记录:

  • 若该触发器在源库和目标库中同时存在,那么当某条变更被复制到目标库时:

    • 表 B 会收到一条来自 CDC 的同步记录;

    • 目标库的触发器也会再次插入一条记录;

    • 结果是重复插入两条数据,造成数据不一致。

因此,每个触发器都必须根据其业务逻辑进行分析,并选择恰当的处理方式。常见的三种处理策略如下:

方案 1:审计型触发器(Audit Trigger)

若触发器的唯一作用是将变更记录写入审计表(Audit Table),且该表不会被手动写入,只接受触发器的自动插入,那么可以在目标库中保留该触发器,但需要配置同步排除该审计表。这种方式下:

  • 源库执行变更,审计记录只写入源库;

  • 目标库根据自身触发器独立记录审计数据;

  • 避免因双重写入导致重复记录。

方案 2:混合更新型触发器(Mixed Updates)

若触发器会写入或更新一个同时被其他方式更新的表,则应:

  • 保留该表的 CDC 同步机制;

  • 在目标库中禁用相关触发器,以避免重复数据;

  • 当同步停止、目标库接替为主库时,再重新启用触发器。

TapData 在此场景下采用自动策略:在数据写入过程中临时在会话级别禁用触发器,这样既不会影响数据库中其他用户的触发器行为,又能避免数据冲突。

方案 3:多表写入型触发器(Multi-Table Impact)

若触发器会影响多个表,且其作用无法明确归类为方案 1 或 2,则建议:

  • 按方案 2 处理;

  • 或对触发器逻辑进行拆分,将不同影响范围的逻辑独立管理。

无论采用哪种策略,所有迁移后的触发器逻辑都必须经过充分测试,以确保触发逻辑在目标库中不会引发意外的数据错误或业务偏差。

正确处理触发器是保障 CDC 流程稳定可靠的关键一环。TapData 提供的会话级触发器控制功能,在大多数场景下可实现自动化屏蔽干扰,极大简化了同步链路下的复杂逻辑处理。

当然,以下是为整篇翻译后的技术博客撰写的总结部分,保持技术博客风格,语言简洁专业,适用于发布在官网或公众号结尾处:

总结

本项目通过 TapData 实现了从 MSSQL 到 PostgreSQL 的关键业务系统迁移,并辅以双向同步机制,确保了数据一致性与业务连续性。在实际操作中,我们针对字段类型映射、自增列处理、主键策略、触发器和存储过程迁移、外键约束规避、序列值管理等多个关键技术点进行了细致拆解与优化。

TapData 提供的 CDC 支持、字段自动转换能力、会话级约束控制、序列值跳跃控制等能力,有效降低了异构数据库迁移中的技术门槛和运维风险。整体架构设计为后续系统切换与容灾准备打下了坚实基础。

此次实践为企业从历史数据库向新数据库选型的演进提供了可复制、可落地的技术路径,也验证了在复杂场景下通过精细化控制依然可以实现平滑、安全的异构数据库替代。

相关推荐
why1515 小时前
微服务商城-商品微服务
数据库·后端·golang
柒间6 小时前
Elasticsearch 常用操作命令整合 (cURL 版本)
大数据·数据库·elasticsearch
远方16098 小时前
18-Oracle 23ai JSON二元性颠覆传统
数据库·oracle·json
jllllyuz9 小时前
如何为服务器生成TLS证书
运维·服务器·数据库
伍六星10 小时前
Flask和Django,你怎么选?
数据库·django·flask
杜哥无敌10 小时前
ORACLE 修改端口号之后无法启动?
数据库·oracle
远方160910 小时前
0x-4-Oracle 23 ai-sqlcl 25.1.1 独立安装-配置和优化
数据库·ci/cd·oracle
远方160910 小时前
0x-3-Oracle 23 ai-sqlcl 25.1 集成安装-配置和优化
数据库·ide·ai·oracle
喵叔哟11 小时前
第1章:Neo4j简介与图数据库基础
数据库·oracle·neo4j