SQL Server数据迁移这件事,远比你想的复杂——但也远比你想的简单(上)



兼容 是对前人努力的尊重 是确保业务平稳过渡的基石 然而 这仅仅是故事的起点


做数据库迁移这些年,我见过太多项目死在"最后一公里"。前面选型、测试、评审都过了,一到真刀真枪搬数据的时候,各种妖蛾子全冒出来了。特别是SQL Server数据迁移,说实话这活儿在国产化替代的大潮里绝对是个硬骨头。不是说你搬不过去,而是搬过去之后能不能跑得动、跑得快、跑的稳,那才是真正考验人的地方。

我之前一直觉得,国产数据库做兼容嘛,把语法对上就行了,顶多再处理一下数据类型映射,能有多难?后来真上手做了几个从SQL Server迁过来的项目,才知道自己太天真的。SQL Server这玩意儿它有一堆自己独有的东西,T-SQL那套语法体系跟标准SQL之间隔着不少沟壑,你光是把存储过程翻译过来就够喝一壶的,更别说那些用了十几年的老系统里藏着的各种"祖传写法"。

而且你说迁移吧,它不是一个人的事。DBA要管数据一致性,开发要改适配代码,业务方要等着验收,运维要准备接盘。这么多角色凑一块儿,沟通成本就够呛了。我见过有的项目光是对一个存储过程的改写方案就开了三次会,最后还是没定下来。这还只是一个存储过程啊兄弟,一个稍微大点的系统几百个存储过程你想想是什么概念。所以迁移这个事,技术难度只占一半,另一半是工程管理的难度。

先说说迁移这件事到底难在哪

我记得第一次做SQL Server到国产库的迁移,那会儿还是好几年前。当时团队里没有一个人有完整经验,就是摸着石头过河。拿到源库的DDL导出来一看,好家伙,几百个存储过程、几十个函数,里面全是MERGE语句、OUTPUT子句、PIVOT/UNPIVOT这些SQL Server特有的东西。当时用的那个迁移工具,碰到这些语法直接就报错了,也不告诉你怎么改,你就得一条一条手工去翻译。

举个最简单的例子,SQL Server里特别常见的MERGE语句:

sql 复制代码
-- SQL Server里的MERGE,干的是"存在就更新、不存在就插入"的活儿
MERGE INTO target_table AS T
USING source_table AS S
ON T.id = S.id
WHEN MATCHED THEN
    UPDATE SET T.name = S.name, T.update_time = GETDATE()
WHEN NOT MATCHED THEN
    INSERT (id, name, create_time)
    VALUES (S.id, S.name, GETDATE())
OUTPUT $action, inserted.id, deleted.id;

你看这段代码,在SQL Server里跑得好好的,但你要迁到别的库去,如果目标库不支持MERGE语法,你就得拆成两段------先UPDATE再INSERT,或者用UPSERT的写法。但问题是这个OUTPUT子句你怎么搞?它返回的是操作类型和影响的数据行,很多业务逻辑是依赖这个输出的。你要是手工改,不仅容易出错,而且改完之后逻辑是不是跟原来完全一致,谁也不敢打包票。

还有那个并行DML,SQL Server里你可以这么写:

sql 复制代码
-- SQL Server并行DML
INSERT INTO big_table WITH (TABLOCK) (col1, col2, col3)
SELECT col1, col2, col3
FROM huge_source_table WITH (NOLOCK)
OPTION (MAXDOP 4, HASH JOIN);

那个OPTION提示、TABLOCK、NOLOCK这些,在其他数据库里根本没有对应的东西。你迁过去之后,原来的执行计划全变了,性能可能天差地别。

那些年我们踩过的兼容性坑

现在市面上很多所谓"兼容SQL Server"的国产数据库,它的兼容是分层次的。

最浅层的兼容就是数据类型对上了,基本的SELECT/INSERT/UPDATE/DELETE能跑,这叫"能连上"。但这种兼容在真实业务场景里基本没啥用,因为任何一个稍微复杂点的业务系统,它的SQL都不会只是简单的CRUD。

再深一层是函数兼容,SQL Server里那些系统函数------GETDATE()、DATEADD()、CONVERT()、ISNULL()、STUFF()之类的------目标库是不是都有等价实现?别小看这些函数,我见过一个报表系统,迁移之后因为ISNULL和COALESCE的空值处理行为不完全一致,导致报表数据对不上,排查了三天才发现问题。

最深层的兼容,也是真正难的,是T-SQL的完整语法体系兼容。这包括但不限于:

sql 复制代码
-- SQL Server里的窗口函数,带RANGE选项的
SELECT 
    department_id,
    employee_name,
    salary,
    SUM(salary) OVER (
        PARTITION BY department_id 
        ORDER BY hire_date 
        RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
    ) AS running_total
FROM employees;

-- PIVOT操作,行列转换
SELECT *
FROM (
    SELECT category, month, amount
    FROM sales_data
) AS src
PIVOT (
    SUM(amount)
    FOR month IN ([1月], [2月], [3月], [4月])
) AS piv;

-- 带OUTPUT子句的DELETE
DELETE FROM archive_table
OUTPUT deleted.id, deleted.name, GETDATE() AS delete_time
INTO delete_log
WHERE expire_date < '2025-01-01';

这些写法在SQL Server里都是家常便饭,但你换到别的数据库,很多就不支持了。不支持怎么办?改呗。但改一个存储过程可能就要半天,几百个存储过程改下来,工期直接爆炸。

我后来接触到了KDMS这个迁移评估工具,情况才好了一些。它的工作原理跟手工翻译完全不同,不是让你去逐条改SQL,而是先把源库的对象结构采集上来------注意,它只采集结构对象,不碰业务数据,这点很重要,因为采集数据量大的时候会影响源库性能------然后在评估系统里做智能转换。说白了呢,就是机器帮你做翻译,你只需要看转换结果和评估报告。

它那个评估报告还挺有用的,能告诉你一共有多少表、多少存储过程、多少函数、多少触发器,自动转换率是多少。

真正让人焦虑的从来不是"能不能迁"

一个系统在SQL Server上跑了十年八年,执行计划早就稳定了,索引也调优过无数轮了,虽然不能说快得飞起,但至少业务方已经习惯了那个响应速度。你一迁移,换了个数据库内核,查询优化器不一样了,执行计划全变了,就算SQL语法完全兼容、数据完全一致,性能也可能出现大幅波动。

而且这种波动还不是简单的"变慢一点"或者"变快一点",而是可能出现极端的情况:原来1秒出结果的查询现在跑30秒,原来能扛100并发的接口现在20并发就卡死了。这种性能断崖式下降才是最要命的,因为业务方不会给你时间慢慢调优,上线就是生死线。

我记得有一次在做迁移POC的时候,碰到一个特别典型的场景。那是一个BI报表系统,有个查询是做多维分析的,大概长这样:

sql 复制代码
-- 这个查询在SQL Server上跑大概3秒出结果
SELECT 
    d.region_name,
    d.product_category,
    (
        SELECT SUM(s2.amount) 
        FROM sales s2 
        WHERE s2.region_id = d.region_id 
          AND s2.sale_date >= '2025-01-01'
    ) AS total_sales,
    (
        SELECT AVG(s3.discount_rate) 
        FROM sales s3 
        WHERE s3.region_id = d.region_id
          AND s3.product_category = d.product_category
    ) AS avg_discount,
    SUM(s.amount) AS current_month_sales
FROM dim_region d
INNER JOIN sales s ON d.region_id = s.region_id
WHERE s.sale_date BETWEEN '2025-06-01' AND '2025-06-30'
GROUP BY d.region_name, d.product_category, d.region_id
ORDER BY total_sales DESC;

这段SQL的特点是SELECT列表里嵌了两个标量子查询,每个子查询还都关联了主查询的字段。在SQL Server上,查询优化器很聪明,它会把这些标量子查询当作类似LEFT JOIN来处理,执行计划里你能看到它用索引查找的方式高效地完成了这些子查询。

但迁到目标库之后,同样是这段SQL,查询优化器的处理方式完全不同。它把每个标量子查询都当作独立的相关子查询来执行,也就是说对于外层查询返回的每一行,都要把子查询执行一遍。外层如果返回一万行,那两个子查询就各执行一万次。这性能能好才怪呢------直接从3秒变成了47秒。

类似的情况在实际业务中太常见了。SQL Server的T-SQL编程风格特别鼓励使用标量子查询,很多开发人员觉得这样写直观、好维护,一个SELECT里嵌三四个标量子查询是常事。但这些SQL一旦换了数据库内核,性能就可能暴雷。

这就是迁移后性能问题的核心矛盾:不是目标数据库不行,而是同一句SQL在不同数据库的查询优化器里,可能走完全不同的执行路径。而执行路径的差异,带来的性能差异可能是几十倍甚至上百倍的。

再聊聊迁移工具链的全貌

既然说到迁移工具,我觉得有必要把整个工具链的脉络理一下,免得大家以为迁移就是跑一个工具的事。实际上一个完整的SQL Server到国产库的迁移项目,工具链是要配合使用的,每个工具干不同的活。

首先是评估阶段,就是用KDMS这种工具做兼容性评估。它采集源库的结构信息,自动做语法转换,生成评估报告。这个报告的价值在于让你提前知道哪些对象能自动转、哪些需要手工处理、哪些有兼容性风险。相当于迁移之前先做个全身体检,心中有数再动手。

然后是结构迁移和数据迁移阶段。KDMS做的是结构迁移,就是把表、视图、存储过程这些对象搬到目标库去。数据迁移是另外的工具干的活,比如KDTS做全量数据迁移,KFS做增量数据同步。为什么需要增量同步?因为很多业务系统不能停机,你不能说"各位用户稍等,我们搬个数据,先停两天"。你得在源库还在正常跑业务的同时,把存量数据先搬过去,然后通过增量同步把搬迁期间的变更数据也追上,最后在一个很短的窗口内切换流量。

这个过程中还有一个很关键的环节是数据校验。搬完数据之后,你得确认源库和目标库的数据是一致的,不能搬丢了或者搬错了。通常的做法是对关键表做全量比对,或者对数据做hash校验。

再往后就是应用适配和联调阶段。虽然工具自动转换了大部分SQL,但总有一些需要人工调整的。

代码0改造指的是MERGE、并行DML、OUTPUT子句这些SQL Server特有的语法原生支持,存量T-SQL直接连上去就能跑。迁移0停机就是前面说的KDT/KFS工具链做全量+增量+校验。上线有保障是PITR最优路径恢复,故障后5分钟内可定位数据,HA组件自主运维。

思路是对的,但对我们这种实际做迁移的人来说,口号喊得再响不如实测数据来得实在。你说0改造,那我就要问那些用了SQL Server特有写法的存储过程真的能直接跑吗?你说性能好,那我就要看在真实负载下的TPS和响应时间数据。你说5分钟定位问题,那我就要问故障收集分析工具到底能抓到哪些信息、诊断报告够不够详细。

这种 skepticism 不是针对某一家厂商,而是整个国产数据库行业都需要面对的信任问题。太多项目迁移完之后性能暴雷,导致业务方对国产数据库失去了信心,这个信任修复起来比技术问题本身更难。

其实我还想到另外一个维度的事。有些团队做迁移的时候会走进一个误区------过度依赖工具,觉得工具说转换率98%就万事大吉了。但你想过没有,那2%没转成功的,往往是最复杂的、最关键的存储过程。因为简单的SQL谁都能转,工具能转人也能转。真正难的那些------涉及动态SQL、临时表、游标嵌套、错误处理的------工具往往搞不定。而这些复杂的存储过程恰恰是系统的心脏,出不得半点差池。

所以我的经验是:工具生成的转换结果一定要逐条review,尤其是存储过程和函数。工具可以帮你省80%的体力活,但剩下20%的脑力活得你自己来。不要偷懒,不要图快,否则上线后出的每个问题都会加倍奉还。

一个让我改观的实测

所以当我后来看到KES V9R4C019这个版本发布的时候,一开始也就是扫一眼,没太当回事。直到有一次机会实际测了一下,才开始重新审视。

那次测试的背景是这样的:有一个从SQL Server迁过来的业务系统,核心是一个BI分析平台,每天的报表查询量大概在几十万次,高峰期并发100左右。整体来看,100并发场景下复杂查询的TPS提升了大概60%,平均响应时间压缩到了原来的1/10左右。

这意味着什么呢?意味着那些原来跑30秒的查询现在3秒就出结果了,原来跑60秒的现在6秒左右。而SQL Server上这些查询的响应时间大概在3到5秒,也就是说迁移后的性能基本追平了甚至略微超过了SQL Server。

这个结果让我开始重新思考一个问题:也许国产数据库的"性能瓶颈"这个刻板印象,至少在某些场景下已经被打破了。当然一个版本的测试数据不能说明所有问题,但至少在这个特定的场景------含标量子查询的多表关联分析------V9R4C019确实交出了一份不错的答卷。

具体是怎么做到的?这涉及到查询优化器的一些底层变化。从公开资料来看,V9R2C14版本就已经开始对标量子查询做优化了,把包含相关标量子查询和包含等价性谓词及传递谓词条件的场景做了专门处理。V9R4C019应该是在这个基础上进一步深化了优化策略,可能在执行计划选择、子查询展开、连接顺序优化等方面做了改进。

我试着用EXPLAIN看了一下优化前后的执行计划对比,确实有变化。旧版本对那些标量子查询走的是InitPlan+相关子查询的方式,每一行外层结果都要重新执行一次子查询。新版本看起来做了子查询展开,把它转换成了某种形式的连接操作,这样就可以利用索引和批量处理来提高效率了。

大概是这样的差别(简化示意):

sql 复制代码
-- 旧版本执行计划(概念性示意)
Nested Loop
  -> Seq Scan on dim_region d
       (返回10000行)
  -> InitPlan
       -> Index Scan on sales s2
            Index Cond: region_id = d.region_id
       (对每一行外层结果执行一次)
  -> InitPlan
       -> Index Scan on sales s3
            Index Cond: region_id = d.region_id AND product_category = d.product_category
       (对每一行外层结果执行一次)

-- 新版本执行计划(概念性示意)
Hash Join
  -> Hash Join
       -> Seq Scan on dim_region d
       -> Hash
            -> HashAggregate
                 -> Index Scan on sales s2
                      Index Cond: sale_date >= '2025-01-01'
                 (一次性扫描,按region_id聚合)
  -> Hash
       -> Hash Join
            -> Seq Scan on sales s
            -> Hash
                 -> HashAggregate
                      -> Seq Scan on sales s3
                      (一次性扫描,按region_id和product_category聚合)

当然我这个示意图是概念性的,真实的执行计划比这复杂得多。但核心思路就是这样:把逐行执行的相关子查询转换成批量处理的连接操作,这中间的性能差距是数量级的。

上篇就先聊到这。下篇我会深入聊聊那个实测数据的细节、BI报表场景的具体体验变化、以及为什么我认为这次升级可能改变了一些人对国产数据库性能的固有印象。

相关推荐
JakeJiang1 小时前
抓到接口还不够:用 AIProxy 改返回、Mock 数据、切测试环境
前端·后端
YuePeng1 小时前
Java 开发者的 Django Admin,终于来了
后端·架构·github
XuCoder1 小时前
Redis 分片集群:它到底是怎么把数据分片又路由的?
后端
吃饱了得干活1 小时前
Java Map 核心原理:从数据结构到 put/get 执行,一篇彻底讲透
java·后端
不才不才不不才2 小时前
Spring 源码系列(17): HandlerMapping 与 HandlerAdapter 两大体系
java·后端·spring
Seven972 小时前
AI杂谈:别再问AI会不会替代你,先看你是不是驾驶员
人工智能·后端
ERD Online2 小时前
我们怎么设计 good first issue:让第一个 PR 两小时内合入
数据库·git·后端·开源·issue
IT_陈寒2 小时前
Vite热更新失效?我的几个犯傻操作害我debug两小时
前端·人工智能·后端
凤山老林2 小时前
Spring Boot 大文件处理实战:分片上传、断点续传与 OSS 集成
java·spring boot·后端·大文件上传·分片上传·断点续传