SQL Server 2022迁移实战:T-SQL批处理兼容性改造与回归验证指南
项目启动会那天,技术总监开门见山问了我一句:"V9R4C019迁移到SQL Server 2022,存量那些T-SQL批处理,你预计要改多少?"我当时没敢拍脑袋,只说先做一轮资产盘点。结果扫下来,光Agent作业里跑批步骤就三百多个,加上文件服务器上历代运维留的.sql和.bat脚本,涉及T-SQL批处理的代码超过三千段。真正需要动手改的,最后统计是117处。这个数字怎么来的、哪些改动是白费劲、哪些坑只有切过去才会爆,就是我这次想完整复盘的内容。
如果你正面临类似的SQL Server数据库迁移,尤其是那种跑了快十年、背后挂着一大堆没人敢动的存量T-SQL批处理的系统,这篇东西应该能帮你省掉不少弯路。我会从资产盘点、兼容性雷区、执行顺序、回归验证、改造边界五个维度,把整个项目里最值钱的经验讲透。
1. V9R4C019的批处理资产盘点:先看清存量再谈迁移
1.1 谁在调用T-SQL批处理:作业、脚本、业务接口三条链路
接到任务后我做的第一件事,不是开DMA扫描,而是把"谁在调用批处理"这个问题彻底弄清楚。V9R4C019这套系统跑了将近十年,数据库侧的工作负载主要有三类。
第一类是SQL Server Agent作业。我查了msdb里的作业清单,一共214个作业,其中超过一半是每天凌晨跑的批处理任务:日志归档、数据归档、汇总表刷新、对账、月结。第二类是运维手工脚本。文件服务器上散落着历年DBA和开发留下的.sql、.bat、.ps1文件,有些是临时修复数据用的,有些是定期手工执行的批处理;这些脚本最危险,因为它们没有任何版本管理,谁也说不清哪些还在用、哪些已经废弃。第三类是业务系统内部调用的存储过程,虽然是应用层发起的,但运行逻辑和批处理没有本质区别。
我建议所有做迁移的人,第一周不要碰任何工具,先把这张清单拉出来,跟历任DBA和运维逐个确认。V9R4C019这个项目里就有三个作业,名字看起来像历史遗留,实际每个月还在跑财务对账,差点被当垃圾清理掉。盘点的核心不是脚本数量,而是搞清楚哪些批处理是"生产关键路径"上的,哪些改了也无所谓。
1.2 用DMA和脚本扫描代替"人肉翻代码"
盘点完调用关系后,我用了微软官方的Data Migration Assistant(DMA)做了一次全量兼容性评估。DMA可以直连源库,跑完会生成一份报告,把兼容性问题分成了阻断迁移的问题、需要修改的变更、以及行为变化三个级别。
对V9R4C019这个库,DMA一共报了800多个兼容性项,但真正需要人去看的远没有这么多。我的经验是,先把DMA报告的原始JSON导出,按对象类型和问题类型做一个优先级排序,而不是一屏一屏翻界面。比如报告中大量出现的"某些DBCC命令已废弃""系统存储过程行为变化"这类信息,对于实际运行影响极小,可以直接归类为低优先级。
光靠DMA还不够。很多存量T-SQL批处理根本不是数据库里的存储过程,而是躺在文件系统上的脚本。我写了个PowerShell脚本,把.sql、.bat、.ps1文件全部扫了一遍,用正则匹配那些典型的废弃写法。
这套扫描的产出是一份全量清单,最后汇总成一张统计表:哪些文件踩了废弃语法、哪些存储过程引用了系统表、哪些批处理依赖旧版行为。有了这张表,才能进行下一步分级。
1.3 扫描结果怎么分级:阻断项、警告项、噪声项
我把扫描出来的所有问题分成了三个等级,用Excel做了个简单的分级清单:
| 等级 | 定义 | 处理策略 | V9R4C019中的数量 |
|---|---|---|---|
| 阻断项 | 会导致编译失败或结果错误 | 必须在切换前改完并验证 | 21 |
| 警告项 | 行为或性能可能发生变化 | 切换前逐条确认,必要时修改 | 96 |
| 噪声项 | 不影响功能,仅是建议性信息 | 记录归档,不改 | 700+ |
这个分级帮了大忙。如果不分级,800多个问题会让团队陷入改不完的恐慌;分级之后,真正必须动手的只有21个阻断项。举个例子,V9R4C019里有十几个存储过程直接查sysobjects拿表元数据,虽然在SQL Server 2022里只要兼容级别合适还能跑,但这属于不保证长期可用的写法,加上正好在升温级窗口,我就顺手把它们改成了sys.objects。
还有一批脚本引用了sp_dboption,这个存储过程在2008 R2之后就已经标记为不推荐,到2022虽然在兼容级别100下还能执行,但输出和副作用在不同版本间有差异,像这种我全部优先改掉。噪声项里最多的是一堆索引缺失建议,这跟迁移没有任何关系,我直接忽略。
2. 老脚本换新环境容易踩的四个T-SQL雷区
2.1 兼容级别:既保护你也困住你
V9R4C019源库的数据库兼容级别是100,对应SQL Server 2008 R2。迁移到SQL Server 2022后,数据库本身默认会变成兼容级别160。这个"升级"会带来一个很微妙的问题:T-SQL的语法兼容性多数情况下不随兼容级别变化,但查询优化器的行为、某些内置函数的默认行为,是会随兼容级别改变的。
我当时的策略是,数据库迁移完成后,第一时间把兼容级别显式设回100,让新环境以"最接近老环境"的方式运行一段时间。用团队内部一句话说就是:"让SQL Server 2022先假装自己是2008 R2,等我们确认所有批处理都接住了,再让它做回自己。"
设回兼容级别100不是一劳永逸。它意味着你放弃了新版本查询优化器的很多改进,也放弃了Query Store里一些只对新兼容级别生效的智能处理功能。所以我的建议是,把它当成一个过渡手段而不是终点,设定一个明确的升级观察期,比如三个月,期间积累足够的基线和历史数据后,再评估升到160。
2.2 隐式转换和排序规则冲突:最阴的兼容性问题
隐式转换是迁移后最容易出"灵异事件"的根源。V9R4C019里有一个很典型的存储过程,WHERE条件用AccountID = @AccountID,但表里AccountID列是varchar(20),传入参数是INT。在老环境里因为统计信息和数据分布的原因,这个转换并没有导致严重问题。迁移到新环境后,因为兼容级别相同但统计信息变了、内存和并行度配置变了,执行计划生成方式就不同了。
这类问题最坑的地方在于——它不报错,只是慢。你能看到的表象是某个批处理从15分钟涨到1小时,但排查半天都找不到语法问题。我的诊断建议是:对迁移后明显变慢的批处理,把SQL语句的实际执行计划拉出来,看有没有黄色的隐式转换警告,或者把SET STATISTICS IO ON打开看扫描行数。那些谓词列类型不匹配的写法,在迁移项目里应该被当作头号嫌疑犯。
排序规则是另一个容易翻车的地方。V9R4C019源库是Chinese_PRC_CI_AS,新环境的实例级排序规则我特意没有用默认的,而是显式指定了和源库一样的排序规则。很多人会忽略的一点是:即使你备份恢复了数据库,数据库的排序规则会保留,但实例级别和tempdb的排序规则是新的。当批处理在用户库和tempdb之间做临时表关联时,如果排序规则不一致,SQL Server会报"cannot resolve collation conflict"。
排查到这类问题后,最省事的方案是给建临时表时显式指定COLLATE Chinese_PRC_CI_AS。但根本解法是确保实例级排序规则在安装时就与源系统一致,这个决策应该在迁移规划早期就定下来,而不是等报错再补。
2.3 废弃语法与系统表引用:编译能过不代表能跑
存量T-SQL里最容易被忽略的问题,是那些"编译能过,但行为已经和十年前不一样"的写法。E-R图里画不出来的依赖,往往就藏在这种代码里。
V9R4C019里有个很有意思的例子:一个老运维写的数据修复脚本,用RAISERROR('...', 16, 1)的旧式语法打印信息。这个语法在SQL Server 2012之后依然被支持,但微软官方已经建议用THROW替代。问题是,这个脚本里对RAISERROR输出信息做了字符串截取,用来判断执行进度。新版SQL Server虽然兼容旧语法,但对消息文本的处理在某些边界情况下会有一点差异,导致这个判断偶尔失效。这种问题DMA是查不出来的,只有靠代码评审和实际跑批才会暴露。
更常见的废弃写法是直接引用系统表。除了sysobjects,还有sysindexes、sysusers、sysdatabases。这些系统表在新版本里还在,但因为底层结构变化,某些列的含义和行为在不同版本间有差异。比如sysindexes里的rows列,在旧版本里还能反映一些统计信息,新版本里则可能因为索引结构不同而不准确。凡是批处理里依赖这些系统表列做判断的,都应该优先改造。
2.4 查询计划变化和参数嗅探:批处理变慢的隐形杀手
V9R4C019迁移过程中最折腾的一个问题,是一个凌晨跑批的存储过程。源库执行时间稳定在20分钟左右,切到新环境后第一次跑,直接跑了54分钟。当时数据库兼容级别还是100,数据量、索引、统计信息都一样,硬件规格甚至比旧环境还好一点,怎么就慢了?
后来抓实际执行计划才发现,优化器在24核的新机器上为其中一个核心查询选择了并行哈希连接,而旧环境是8核,优化器选了嵌套循环。数据分布本来就是偏斜的,哈希连接在探测阶段严重不均,加上并行度配置没有跟随硬件调整,性能就崩了。
这个问题的本质并不是SQL Server 2022变笨了,而是统计信息、硬件配置、并行阈值共同作用下,优化器做出了一个"理论上合理但对这个特定批处理不友好"的选择。参数嗅探也掺了一脚:这个存储过程首次执行时传入了一个筛选性极差的参数值,生成的计划对后续绝大多数参数都不适用。
处理方案我分了三步走。第一步,把cost threshold for parallelism从默认的5调到了50,让小查询别动不动就走并行;第二步,给这个关键存储过程加了OPTION (RECOMPILE),让每次执行都基于当前参数重新生成计划;第三步,重新统计所有大表的直方图信息,确保统计数据的采样率和新鲜度都优于旧环境。
这不是一个"标准答案",而是一个需要结合具体业务反复试的过程。但有一条经验是通用的:迁移后第一个月,凡是跑批延迟超过旧环境1.5倍的作业,都要拉实际执行计划出来对比,不要急着调代码,更不要急着加提示。
3. 迁移执行阶段的顺序安排:先搬数据还是先改脚本
3.1 数据迁移动线选择:备份恢复优先
V9R4C019的数据量在1.2TB左右,可接受的停机窗口是周末一个晚上。我最终选了备份恢复这条路,而不是事务复制或者AlwaysOn迁移。原因很简单:这个系统有大量跨库作业和定时批处理,任何基于同步的迁移方案都需要在切换瞬间处理同步延迟和对象一致性的问题,复杂度和风险都更高。
备份恢复完成之后,我立刻跑了一遍DBCC CHECKDB WITH DATA_PURITY确认物理和逻辑完整性。这一步不能省,因为在旧环境上跑了十年的库,很难说有没有积累下来隐藏的损坏。
接下来还有一个很多人容易漏掉的步骤:重新创建全部登录账号,并保留SID映射关系。如果直接迁移数据库而不迁移登录,应用层连上来会发现账号是孤立用户。V9R4C019的解决办法是用sp_help_revlogin这个脚本把所有登录和SID导出来,再到新实例上重建。这个脚本网上很多,核心原理是读取sys.sql_logins里的SID并通过sp_hexadecimal转成十六进制,然后用CREATE LOGIN ... WITH SID = 0x...重建,确保应用账号的权限映射跟旧环境一模一样。
3.2 对象重建与脚本修改的工作量拆分
V9R4C019的项目排期上,数据迁移和脚本改造是并行推进的两条线。数据迁移放在切换窗口做,但脚本改造在切换前两周就启动了。
改造过程用的是"迭代式发布":把需要改的脚本按类型分成三批。第一批是工具类脚本,比如数据修复、临时对账的脚本,这部分改起来风险最低,改完直接在测试环境跑一遍就行。第二批是Agent作业里非核心链路的步骤,比如日志清理、历史数据归档,这些可以在业务低峰期做验证。第三批是核心批处理,比如月结、财务对账,这些脚本改动后需要在完整的测试环境里用历史数据做全量回归,测试周期最长,必须提前安排。
对象重建方面,我用了SSMS的"Generate Scripts"功能生成全部存储过程、视图、函数、触发器的创建脚本,然后在新环境按依赖顺序执行。这里有个细节:生成脚本时一定要把目标服务器版本选成SQL Server 2022,这样生成的CREATE语句会包含新版本兼容级别的默认设置。对于不需要修改的对象,直接跑生成脚本即可;对于需要修改的对象,在生成后的脚本上改,能清晰对比出改动点。
3.3 切换窗口长度怎么估算:以批处理回放为标尺
很多人估算切换窗口只会算"备份多久加恢复多久加验证多久",但V9R4C019这个项目真正的时间大头不是数据传输,而是批处理的验证回放。
我们的做法是:切换前两周,在测试环境恢复了一个两周前的生产备份,然后按生产环境作业调度表,把凌晨的批处理完整回放了一遍。记录每个作业在目标环境的运行时长,跟生产环境的基线进行对比。这个回放结果直接决定了切换窗口的排程——如果核心批处理在目标环境上耗时比预期多,切换窗口就必须预留出足够的缓冲时间,而不是把所有任务压在一个窄窄的窗口里。
切换窗口最终排期是这样的:周五晚上22点开始停应用、做最后一次日志备份;23点前完成数据库恢复和一致性校验;周六凌晨0点到4点,回放核心批处理,验证关键数据表的结果差异;凌晨4点到6点,跑完整作业链,观察资源消耗和时长;周六上午10点前,应用侧做联调验证,确认无误后开放给最终用户。整个窗口预留了大约12个小时的裕量,实际只用了6个小时就完成了全部切换和验证。
4. 切换后的批处理回归验证:用对账思维替代"能跑就行"
4.1 结果集对比:批处理输出的黄金标准
"作业状态显示成功"和"批处理真的做对了"是两回事。V9R4C019迁移完成后,我要求所有核心批处理任务必须做结果集级验证,而不是只看Agent作业的状态灯。
做法是:切换前在生产环境把核心批处理涉及的数据表导出快照;切换后在新环境跑同样的批处理,然后对两张表的全部数据行做校验和对比。T-SQL里最实用的整表对比方式是用EXCEPT或者CHECKSUM_AGG(BINARY_CHECKSUM(*))。
如果两个方向(EXCEPT正向和反向)都没有行,表数据就完全一致。如果某一方向有差异,就要具体到行和列去分析是批处理逻辑变了、还是源数据本身因为时间差发生了合理变化。这个对账思维能逼着你在迁移前就把"什么算对"定义清楚,而不是切完了才拍脑袋。
4.2 跑批耗时与资源曲线的前后对照
我让团队做了一张"跑批基线对照表",把V9R4C019所有关键作业在旧环境过去30天的平均耗时、最大耗时、最小耗时都统计出来,然后跟新环境每个作业的实际耗时做对比。
| 作业名 | 旧环境平均耗时(分钟) | 新环境耗时(分钟) | 差异说明 |
|---|---|---|---|
| 夜间数据归档 | 18 | 17 | 无异常 |
| 订单汇总刷新 | 12 | 11 | 无异常 |
| 财务对账 | 23 | 21 | 无异常 |
| 月结-客户余额重算 | 20 | 54 | 已定位并优化 |
| 日志清理 | 5 | 4 | 无异常 |
除了时长,我还用性能监视器记录了新环境前两周的CPU、内存、磁盘IO曲线,重点观察每天凌晨批处理高峰期的资源水位。V9R4C019在旧环境里凌晨批处理跑起来几乎是满负荷,但新环境因为硬件规格提升,CPU峰值只有60%左右。这里要提醒一句:资源水位低不代表批处理就是健康的,如果某个批处理跑得比旧环境还慢,哪怕CPU只有10%,也要老老实实去查计划和锁等待。
4.3 Query Store和扩展事件:让"偶发报错"有据可查
迁移后第一周,V9R4C019遇到了一个很头疼的问题:每天凌晨3点左右,会有一个批处理偶发失败,但不是每次都失败,一天成功一天失败,完全没有规律。按常规思路根本没法复现,因为手动执行同一段SQL时永远是成功的。
这个场景正是Query Store和扩展事件的用武之地。我给这个数据库开启了Query Store,并把QUERY_CAPTURE_MODE设为AUTO,这样SQL Server会自动记录关键查询的执行计划和运行时统计信息。很快就在sys.query_store_query里找到了那个偶发失败查询,并对比了失败前后的执行计划差异。
同时我建了一个扩展事件会话,捕获error_reported、deadlock_report和blocked_process_report,专门记录批处理窗口期的错误和阻塞事件。跑了一个晚上就抓到了元凶——另一个归档作业和这个批处理在同一个时间点更新同一组数据页,形成了偶发的锁升级阻塞,导致批处理超时。
后来通过错峰调度,把两个作业的运行时间错开15分钟,问题就彻底消失了。如果没有扩展事件,这个"偶发失败"可能要排查两三个星期。
5. 存量脚本改造的边界:哪些要动,哪些别手痒
5.1 优先改"错误型"问题,别碰"风格型"问题
存量T-SQL批处理里面,大量代码的写法在今天是"不推荐"的,但它们在特定数据量和业务条件下运行得很好。V9R4C019里有一批存储过程,用游标循环逐行处理数据,理论上低效,但因为单次处理的数据量只有几百行,跑起来毫无压力。这种代码属于"风格型"问题,不应该在迁移项目里顺手重写。
我的线画得很清楚:只有在迁移后才可能引发编译失败、结果错误、性能量级恶化的"错误型"问题,才值得在切换窗口内修改。风格型问题记录到技术债务清单里,排期到后续的持续优化中去。一个很实际的教训是,我见过身边团队在数据库升级时,DBA顺手把一个跑了8年的游标存储过程改成了基于集合的写法,测试环境认为没问题,上了生产却因为参数嗅探触发了一个从未出现过的大表扫描,最后回滚了当晚的发布。迁移的第一原则是"接住现有的",不是"改造现有的"。
5.2 老的调优提示该怎么处理
存量批处理里经常能看到大量WITH (NOLOCK)、OPTION (MAXDOP 1)、WITH (INDEX(...))这类显式提示。这些提示大部分是过去解决某个具体问题留下的,虽然现在看可能不再必要,但在迁移后不要急着清理。
V9R4C019里有一个核心对账批处理,长期带着OPTION (MAXDOP 1),当时是因为旧环境下并行执行会跟另一个报表查询互相抢占CPU导致超时。新环境CPU核心数和并发能力都大幅提升,这个提示可能已经不需要了,但我选择先原样保留,让它在新环境里用原先的配置跑至少三个月,积累足够多的成功执行记录之后,再单独拿一个测试窗口验证去掉提示后的性能表现。
原因很简单:每一个老提示背后都有一段你不知道的历史和踩坑教训。在迁移项目里动它们,等于用一个不确定的收益去换取一个确定的风险。
5.3 给后期维护留一条退路:兼容级别与版本回退方案
最后就是兼容级别的管理策略。前面提到我把V9R4C019数据库的兼容级别设回了100,这相当于给整个系统穿了一层"防弹衣",让SQL Server 2022以最接近旧环境的方式运行。但这不是终点,我定了一个三个月后的评估计划:确认所有批处理的日报、月结、季结任务都稳定运行,没有出现由优化器行为变化引起的性能和结果问题后,再申请窗口把兼容级别升到160,升级后跑完整回归,观察Query Store里的计划变化。
回退方案也要提前想清楚。兼容级别从100升到160不是不可逆的,直接执行ALTER DATABASE SET COMPATIBILITY_LEVEL = 100就能降回,但升级后再降回,某些新生成的重编译计划、统计信息行为可能不会完全回到最初状态。所以真正稳妥的回退方案还是依赖备份——恢复切换当天的全量备份加后续日志备份到一台新实例上,确保随时有一条可以退回的路。
迁移完成后几个月,回看V9R4C019这个项目,我最大的体会是:数据库迁移的技术难点从来不在"搬数据"本身,而在于那些跑了多年的存量T-SQL批处理怎么在新环境里稳稳地继续跑下去。把兼容性当作一等公民来对待,用数据对账而不是"作业状态"来验证正确性,再加上一点"别手痒"的克制,这套思路几乎可以复用到任何一次SQL Server大版本迁移上。