CRM 如何整合三个 mssql database 的实时数据
结论
不建议通过 trigger 将三个品牌的数据同步到 CRM。具体方案要看业务对“实时”的要求:
- 三个数据库位于同一个 SQL Server 实例,且数据量不大时,可以通过跨数据库视图和
UNION ALL直接读取,实现成本最低。 - 数据库位于不同 SQL Server 实例时,可以使用 Linked Server,但它更适合低频查询,不适合承载高并发的 CRM 页面请求。
- CRM 如果需要稳定、高性能地查询统一数据,建议建立 CRM 集成库,通过 SQL Server CDC 或 Change Tracking 增量同步,再由定时任务或消息管道写入统一模型。与 trigger 相比,这种生产方案通常更容易维护。
还需要先明确“实时”的含义。如果业务允许几秒到几分钟的延迟,CDC 增量同步通常更稳妥。如果必须在事务提交后立即读取结果,就需要直接查询源数据库,同时接受源库故障和性能波动对 CRM 的影响。
统一数据时先解决主键冲突
三个数据库中的 Customer.id = 1 很可能对应三个不同的客户。因此,统一数据时不能只保存原始 ID,还必须记录数据来源。
统一表可以这样设计:
CREATE TABLE dbo.Customer
(
id bigint IDENTITY PRIMARY KEY,
source_brand varchar(20) NOT NULL,
source_id bigint NOT NULL,
customer_name nvarchar(200) NOT NULL,
source_updated_at datetime2 NULL,
synced_at datetime2 NOT NULL DEFAULT SYSUTCDATETIME(),
CONSTRAINT UQ_Customer_Source
UNIQUE (source_brand, source_id)
);
CREATE TABLE dbo.Invoice
(
id bigint IDENTITY PRIMARY KEY,
customer_id bigint NOT NULL,
source_brand varchar(20) NOT NULL,
source_id bigint NOT NULL,
source_updated_at datetime2 NULL,
synced_at datetime2 NOT NULL DEFAULT SYSUTCDATETIME(),
CONSTRAINT FK_Invoice_Customer
FOREIGN KEY (customer_id) REFERENCES dbo.Customer(id),
CONSTRAINT UQ_Invoice_Source
UNIQUE (source_brand, source_id)
);
CREATE TABLE dbo.Payment
(
id bigint IDENTITY PRIMARY KEY,
invoice_id bigint NOT NULL,
source_brand varchar(20) NOT NULL,
source_id bigint NOT NULL,
amount decimal(18, 2) NOT NULL,
payment_date datetime2 NULL,
source_updated_at datetime2 NULL,
synced_at datetime2 NOT NULL DEFAULT SYSUTCDATETIME(),
CONSTRAINT FK_Payment_Invoice
FOREIGN KEY (invoice_id) REFERENCES dbo.Invoice(id),
CONSTRAINT UQ_Payment_Source
UNIQUE (source_brand, source_id)
);
source_brand + source_id 唯一标识源记录,CRM 自己的 id 用于内部关联。这样可以避免主键冲突,也便于追查每条数据来自哪个数据库。
方案一:在同一实例中直接跨数据库查询
如果三个数据库都在同一个 SQL Server 实例,可以使用三段式对象名:
DatabaseName.SchemaName.TableName
先创建统一视图:
CREATE VIEW dbo.AllCustomers
AS
SELECT
'Brand1' AS source_brand,
id AS source_customer_id,
customer_name,
updated_at
FROM Brand1.dbo.Customer
UNION ALL
SELECT
'Brand2',
id,
customer_name,
updated_at
FROM Brand2.dbo.Customer
UNION ALL
SELECT
'Brand3',
id,
customer_name,
updated_at
FROM Brand3.dbo.Customer;
Invoice 和 Payment 可以用同样的方式处理:
CREATE VIEW dbo.AllInvoices
AS
SELECT
'Brand1' AS source_brand,
id AS source_invoice_id,
customer_id AS source_customer_id,
updated_at
FROM Brand1.dbo.Invoice
UNION ALL
SELECT 'Brand2', id, customer_id, updated_at
FROM Brand2.dbo.Invoice
UNION ALL
SELECT 'Brand3', id, customer_id, updated_at
FROM Brand3.dbo.Invoice;
CREATE VIEW dbo.AllPayments
AS
SELECT
'Brand1' AS source_brand,
id AS source_payment_id,
invoice_id AS source_invoice_id,
amount,
payment_date,
updated_at
FROM Brand1.dbo.Payment
UNION ALL
SELECT 'Brand2', id, invoice_id, amount, payment_date, updated_at
FROM Brand2.dbo.Payment
UNION ALL
SELECT 'Brand3', id, invoice_id, amount, payment_date, updated_at
FROM Brand3.dbo.Payment;
组合查询时,关联条件必须同时包含品牌和源 ID:
SELECT
c.source_brand,
c.source_customer_id,
c.customer_name,
i.source_invoice_id,
p.source_payment_id,
p.amount,
p.payment_date
FROM dbo.AllCustomers AS c
INNER JOIN dbo.AllInvoices AS i
ON i.source_brand = c.source_brand
AND i.source_customer_id = c.source_customer_id
LEFT JOIN dbo.AllPayments AS p
ON p.source_brand = i.source_brand
AND p.source_invoice_id = i.source_invoice_id;
这里使用 LEFT JOIN,以便保留尚未付款的 invoice。原问题中的连接语法也应写成 INNER JOIN ... ON ...,不能把关联条件放在 WHERE 后面。
如果需要让 SQL Server 直接生成嵌套 JSON,可以使用 FOR JSON PATH:
SELECT
c.source_brand,
c.source_customer_id AS customer_id,
c.customer_name,
JSON_QUERY(
(
SELECT
i.source_invoice_id AS invoice_id,
JSON_QUERY(
(
SELECT
p.source_payment_id AS payment_id,
p.amount,
p.payment_date
FROM dbo.AllPayments AS p
WHERE p.source_brand = i.source_brand
AND p.source_invoice_id = i.source_invoice_id
ORDER BY p.payment_date
FOR JSON PATH
)
) AS payments
FROM dbo.AllInvoices AS i
WHERE i.source_brand = c.source_brand
AND i.source_customer_id = c.source_customer_id
FOR JSON PATH
)
) AS invoices
FROM dbo.AllCustomers AS c
FOR JSON PATH, ROOT('customers');
这种方式直接读取源数据库中当前已提交的数据,不需要复制数据。不过,CRM 的查询性能和可用性也会直接依赖三个源数据库。
方案二:不同实例使用 Linked Server
如果三个数据库位于不同实例,可以配置 Linked Server,并使用四段式名称:
LinkedServer.DatabaseName.SchemaName.TableName
例如:
SELECT
'Brand1' AS source_brand,
c.id,
c.customer_name
FROM Brand1Server.Brand1.dbo.Customer AS c
UNION ALL
SELECT
'Brand2',
c.id,
c.customer_name
FROM Brand2Server.Brand2.dbo.Customer AS c;
Linked Server 部署比较简单,但使用时要注意以下问题:
- 网络延迟会直接影响页面响应时间。
- 任意一个源服务器不可用,都可能导致整个查询失败或超时。
- 跨服务器 join 可能传输大量数据。
- 权限、加密连接和凭据映射需要分别管理。
- 查询优化器未必能将过滤条件高效地下推到远程服务器。
如果必须使用 Linked Server,应尽量先在远程端完成过滤和聚合。也可以通过 OPENQUERY 执行明确的远程查询:
SELECT *
FROM OPENQUERY(
Brand1Server,
'SELECT id, customer_name, updated_at
FROM Brand1.dbo.Customer
WHERE updated_at >= ''2026-09-01T00:00:00'''
);
不建议让每个 CRM 页面请求都临时连接三个远程实例,再执行复杂的 join。
推荐的生产方案:使用 CDC 增量同步到 CRM 集成库
对于需要统一查询、报表和高并发访问的 CRM,可以采用以下数据流:
Brand1 ─┐
Brand2 ─┼─ CDC / Change Tracking ─ 增量同步任务 ─ CRM 集成库 ─ CRM API
Brand3 ─┘
SQL Server CDC 可以记录表中的 insert、update 和 delete。同步程序定期读取新增变更,再对 CRM 表执行幂等 upsert。这样无需在业务表上维护同步 trigger,也不会让源系统的业务事务直接承担远程写入。
能否启用 CDC,取决于实际使用的 SQL Server 版本、部署方式、权限和运维策略,需要结合具体环境确认。
同步顺序通常如下:
- 同步
Customer。 - 同步
Invoice,通过source_brand + source_customer_id查找 CRM 中的customer_id。 - 同步
Payment,通过source_brand + source_invoice_id查找 CRM 中的invoice_id。 - 处理源表中的删除记录或作废记录。
- 保存每个数据库、每张表的同步检查点。
- 整个批次成功后再推进检查点。
向统一表写入数据时,可以使用显式的 UPDATE 和 INSERT:
BEGIN TRANSACTION;
UPDATE target
SET
target.customer_name = source.customer_name,
target.source_updated_at = source.updated_at,
target.synced_at = SYSUTCDATETIME()
FROM dbo.Customer AS target
INNER JOIN #CustomerChanges AS source
ON source.source_brand = target.source_brand
AND source.source_id = target.source_id;
INSERT INTO dbo.Customer
(
source_brand,
source_id,
customer_name,
source_updated_at
)
SELECT
source.source_brand,
source.source_id,
source.customer_name,
source.updated_at
FROM #CustomerChanges AS source
WHERE NOT EXISTS
(
SELECT 1
FROM dbo.Customer AS target
WHERE target.source_brand = source.source_brand
AND target.source_id = source.source_id
);
COMMIT TRANSACTION;
同步任务必须支持重复执行。否则,任务在完成写入后、保存检查点前发生故障,重新运行时就可能产生重复记录。
如果环境不适合使用 CDC,还可以考虑:
- SQL Server Change Tracking:它只通知同步程序哪些主键发生了变化,程序随后回源读取当前记录。
- Transactional Replication:适合数据库级复制,但需要提前评估目标表结构、冲突处理方式和运维要求。
- SSIS、Azure Data Factory 或自建 Worker:用于调度、转换、重试和监控。
- 应用事件或 Outbox Pattern:如果可以修改源系统,这种方式的业务语义最清楚,但改造成本通常高于 CDC。
如何定义“最新 Payment”
“查询三个数据库”本身不能保证取到业务意义上的最新记录。必须先明确哪个字段代表最新状态,例如:
updated_atmodified_atrowversion- CDC 的变更位置
- 支付系统提供的事件序号
不要只依赖 MAX(payment_id)。自增 ID 通常只能表示插入顺序,无法可靠反映某笔支付最后一次修改的时间。
如果要查询每张 invoice 最后更新的一笔 payment,可以使用窗口函数:
WITH RankedPayments AS
(
SELECT
p.*,
ROW_NUMBER() OVER
(
PARTITION BY p.source_brand, p.source_invoice_id
ORDER BY p.updated_at DESC, p.source_payment_id DESC
) AS rn
FROM dbo.AllPayments AS p
)
SELECT *
FROM RankedPayments
WHERE rn = 1;
这段查询要求源系统可靠地维护 updated_at。如果多次修改可能使用相同的时间戳,还要增加一个稳定的第二排序字段。
实施时需要注意的问题
即使三个数据库目前使用相同的表名,其字段类型、字符集规则和业务含义也可能不同。整合前应逐项确认:
- 同名字段是否具有相同的业务语义。
- 金额的币种和精度是否一致。
- 时间字段保存的是本地时间还是 UTC。
- Customer 是否可能跨品牌重复,以及应该合并还是保持独立。
- Invoice 和 Payment 的删除采用物理删除还是状态变更。
- 不同数据库的 collation 是否一致。
- CRM 是否可以显示同步延迟,以及业务能接受的最大延迟。
- 源系统发生故障时,CRM 应返回旧数据、部分数据,还是直接报错。
如果三个数据库位于同一实例,且查询量有限,可以先用跨数据库视图验证数据模型,实施起来最简单。如果 CRM 需要长期运行并承载高并发查询,则更适合通过 CDC 或 Change Tracking 建立增量同步,将统一数据保存在 CRM 集成库中。这样可以减少 trigger 带来的维护负担,也能更清楚地处理重试、监控和故障隔离。
备注:内容仅供参考。
