ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

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

SQL Server数据迁移这件事,远比你想的复杂——但也远比你想的简单(下) 文章目录那个让我震撼的BI报表场景标量子查询到底为什么慢又是怎么快的100并发下的压力测试细节兼容性不只是能跑运维侧的体验变化智能调优不改应用也能优化写在最后的一些思考兼容是对前人努力的尊重是确保业务平稳过渡的基石然而这仅仅是故事的起点接着上篇聊。上篇说到我实测了KES V9R4C019100并发场景下复杂查询TPS提升60%、响应时间压缩到原来的1/10这个事。这篇我想把这事拆开了揉碎了好好说说因为光给个数字没说服力你得知道这个数字背后到底发生了什么才能判断它到底是真功夫还是花架子。那个让我震撼的BI报表场景先说说BI报表这个场景吧因为这个是最直观的、业务方最能感知到的性能变化。做过的BI系统的同学都知道BI报表有个特别让人头疼的特点SQL特别复杂但用户对响应时间的期望又特别高。你做交易系统接口响应3秒可能用户还能接受但你做BI报表用户点一下查询等30秒还没出结果他就开始焦虑了等1分钟就开始刷新页面等3分钟就直接提工单了。更搞人的是老板看的那些大屏报表老板可没什么耐心等你跑查询他要的就是即时呈现你让他等一分钟他能记你半年。我之前负责的那个BI平台迁移项目问题最严重的就是月度经营分析报表。这个报表的数据来源涉及七八张表有的表几千万行数据查询SQL大概长这样-- 月度经营分析报表核心查询简化版实际比这复杂得多SELECTr.region_name,p.product_line,c.channel_name,(SELECTSUM(s.gross_amount)FROMsales_fact sWHEREs.region_idr.region_idANDs.product_idIN(SELECTproduct_idFROMdim_productWHEREproduct_linep.product_line)ANDs.sale_dateBETWEEN2025-06-01AND2025-06-30)ASgross_sales,(SELECTSUM(s.discount_amount)FROMsales_fact sWHEREs.region_idr.region_idANDs.channel_idc.channel_idANDs.sale_dateBETWEEN2025-06-01AND2025-06-30)AStotal_discount,(SELECTCOUNT(DISTINCTs.customer_id)FROMsales_fact sWHEREs.region_idr.region_idANDs.sale_dateBETWEEN2025-06-01AND2025-06-30)ASunique_customers,SUM(f.net_amount)ASnet_revenue,AVG(f.unit_price)ASavg_priceFROMdim_region rCROSSJOINdim_product pCROSSJOINdim_channel cINNERJOINsales_fact fONr.region_idf.region_idANDp.product_idf.product_idANDc.channel_idf.channel_idWHEREf.sale_dateBETWEEN2025-06-01AND2025-06-30GROUPBYr.region_name,r.region_id,p.product_line,c.channel_name,c.channel_idORDERBYgross_salesDESC;你看看这SQL三个标量子查询嵌在SELECT列表里每个子查询还都关联了外层查询的字段其中一个子查询里还嵌套了个IN子查询。CROSS JOIN做维度组合再跟事实表做内连接。这种SQL在SQL Server上能跑因为它查询优化器比较猛能把标量子查询自动转换成 LEFT JOIN 或者其他更高效的执行方式。在SQL Server上这段查询的响应时间大概是4到5秒。迁到旧版本的国产库上之后变成了40多秒。你想想这个体验差距原来5秒出结果用户觉得还行能接受现在45秒出结果用户觉得这系统是不是挂了。这就是能用和好用之间的鸿沟最真实的写照。SQL能跑、结果正确这是能用但45秒的响应时间让这个查询在实际业务中几乎不可用这就是不好用。升级到V9R4C019之后呢同一段SQL响应时间降到了4秒左右。从45秒到4秒这个变化是实实在在的不是什么实验室数据。业务方的反馈是终于不用等着喝完一杯咖啡才出报表了。我后来又跑了几个类似的BI报表查询结果都差不多-- 另一个典型的BI分析查询带标量子查询的环比同比计算SELECTt1.year_month,t1.region,t1.monthly_sales,(SELECTSUM(monthly_sales)FROMmonthly_summary t2WHEREt2.regiont1.regionANDt2.year_monthFORMAT(DATEADD(MONTH,-1,CAST(t1.year_month01ASDATE)),yyyyMM))ASlast_month_sales,(SELECTSUM(monthly_sales)FROMmonthly_summary t3WHEREt3.regiont1.regionANDt3.year_monthFORMAT(DATEADD(MONTH,-12,CAST(t1.year_month01ASDATE)),yyyyMM))ASlast_year_sales,CASEWHENt1.monthly_sales0THENROUND((t1.monthly_sales-(SELECTSUM(monthly_sales)FROMmonthly_summary t2WHEREt2.regiont1.regionANDt2.year_monthFORMAT(DATEADD(MONTH,-1,CAST(t1.year_month01ASDATE)),yyyyMM)))/(SELECTSUM(monthly_sales)FROMmonthly_summary t2WHEREt2.regiont1.regionANDt2.year_monthFORMAT(DATEADD(MONTH,-1,CAST(t1.year_month01ASDATE)),yyyyMM))*100,2)ELSENULLENDASmom_growth_rateFROMmonthly_summary t1WHEREt1.year_monthIN(202505,202506)ORDERBYt1.region,t1.year_month;这种SQL就更搞人了CASE WHEN里面嵌着标量子查询做环比增长率计算同一个子查询还出现了两次。在旧版本上这个查询跑了将近1分钟升级后大概5秒。虽然比SQL Server的3秒还慢了一点点但已经是可接受的范畴了。标量子查询到底为什么慢又是怎么快的这一段我想稍微深入一点聊聊技术原理因为不搞清楚这个你心里总是不踏实。标量子查询为什么容易成为性能杀手说到底是因为它打破了数据库集合操作的美感。数据库最擅长的是批量处理——给我一个集合我对这个集合做操作返回结果。但标量子查询本质上是一个逐行操作的模式对外层结果的每一行去执行一次子查询。这跟在应用代码里写for循环然后每次循环都查一次数据库是一个道理只不过它发生在数据库内核里。用伪代码来理解就是这样的区别# 旧版本的执行方式概念性类比# 相当于N1查询问题outer_resultsquery(SELECT * FROM dim_region)# 假设返回10000行forrowinouter_results:# 对每一行执行一次子查询row.total_salesquery(SELECT SUM(amount) FROM sales WHERE region_id ?,row.region_id)# 再执行一次另一个子查询row.avg_discountquery(SELECT AVG(discount_rate) FROM sales WHERE region_id ? AND product_category ?,row.region_id,row.product_category)# 总共执行了 1 10000*2 20001 次查询# 新版本的执行方式概念性类比# 相当于批量查询用JOIN替代N1outer_resultsquery( SELECT d.*, s1.total_sales, s2.avg_discount FROM dim_region d LEFT JOIN ( SELECT region_id, SUM(amount) as total_sales FROM sales WHERE sale_date 2025-01-01 GROUP BY region_id ) s1 ON d.region_id s1.region_id LEFT JOIN ( SELECT region_id, product_category, AVG(discount_rate) as avg_discount FROM sales GROUP BY region_id, product_category ) s2 ON d.region_id s2.region_id AND d.product_category s2.product_category )# 总共执行了 1 次查询内部被优化成Hash Join当然数据库内核里实际做的事情比这个伪代码复杂得多。它不是真的把子查询改写成JOIN而是在查询优化阶段识别出标量子查询可以被展开flatten然后选择更高效的执行策略。具体来说可能涉及子查询解关联、物化、连接顺序优化、并行执行等一系列优化器的内部机制。从V9R2C14的资料来看当时的优化主要针对的是目标列中包含相关标量子查询和包含等价性谓词及传递谓词条件的场景。说白了就是如果你的标量子查询里用到了外层查询的某个字段这就是相关的意思并且这个字段上有等值条件等价性谓词或者可以通过传递关系推导出等值条件那优化器就可以对这个子查询做特殊处理把它从逐行执行转成批量执行。V9R4C019大概率是在这个基础上进一步扩展了优化的覆盖范围。因为从我实测的结果来看一些V9R2C14优化不了的复杂标量子查询场景在V9R4C019上也变快了。具体扩展了哪些场景官方资料里没有详细说但从效果来看至少含标量子查询的多表关联分析场景是明显改善了的。我特别想指出的一点是这种优化不是靠加索引、调参数能做到的。索引和参数调优解决的是已有执行计划不够高效的问题但执行计划本身的选择——是用Nested Loop还是Hash Join是把子查询保持为子查询还是展开成连接——这是查询优化器的决策。如果优化器做了一个错误的决策你加再多的索引也没用。这也就是为什么我说从SQL Server迁移过来的性能问题本质上是数据库内核的问题。你不可能指望通过外部调优来解决查询优化器的内在缺陷。V9R4C019的这次升级真正有价值的地方在于它在优化器层面做了改进而不是在兼容层做补丁。100并发下的压力测试细节光说单个查询的响应时间可能还不够直观我再聊聊并发场景下的测试情况。做迁移性能评估的时候单查询响应时间只是一方面并发能力同样关键。因为真实业务环境下不可能只有一个用户在查询通常都是几十甚至上百个请求同时打过来。并发场景下的性能表现取决于很多因素锁竞争、CPU调度、内存分配、I/O吞吐、连接管理等等。我们的测试方案大概是这样的用JMeter模拟100个并发用户每个用户循环执行10个预定义的复杂查询SQL持续30分钟统计TPS每秒事务数和P95响应时间。先在旧版本上跑# 旧版本压力测试结果概要 Duration: 30 minutes Concurrency: 100 Total Requests: 12,847 Success Rate: 99.2% TPS: ~7.1 Average Response Time: 13.8s P95 Response Time: 28.4s P99 Response Time: 41.2sTPS只有7.1也就是说每秒只能完成7个复杂查询。平均响应时间13.8秒P95到了28秒。这个数据说实话很不好看业务方看到这个肯定要炸。然后升级到V9R4C019同样的测试条件# V9R4C019压力测试结果概要 Duration: 30 minutes Concurrency: 100 Total Requests: 20,523 Success Rate: 99.8% TPS: ~11.4 Average Response Time: 8.6s P95 Response Time: 12.1s P99 Response Time: 18.7sTPS从7.1提升到11.4提升幅度大概60%。平均响应时间从13.8秒降到8.6秒P95从28.4秒降到12.1秒。等等这里有个有意思的地方。TPS提升60%是没错的但响应时间的改善幅度比TPS的提升幅度更大——平均响应时间降了37%P95降了57%。这说明什么说明在高并发场景下性能改善不仅仅是单个查询变快了还可能是并发处理能力本身也提升了比如减少了锁等待、改善了内存管理之类的。不过我也要说句公道话这个测试结果跟SQL Server的基准对比还有差距。SQL Server在同样的测试条件下TPS大概在13到14左右平均响应时间7秒左右。也就是说V9R4C019已经非常接近SQL Server的水平了但还没有完全追平。但是——注意这个但是——对于从SQL Server迁移过来的场景来说这个性能水平已经完全可用了。从40秒等结果到4到8秒出结果从TPS只有7到TPS超过11这个跨越让系统从业务不可用变成了业务可用。这才是最重要的。我后来还做了一个更极端的测试200并发-- 200并发下执行的典型查询之一-- 这个查询有3个标量子查询涉及4张表的关联SELECTo.order_id,o.customer_id,o.order_date,o.total_amount,(SELECTc.customer_nameFROMcustomers cWHEREc.customer_ido.customer_id)AScustomer_name,(SELECTSUM(d.quantity)FROMorder_details dWHEREd.order_ido.order_id)AStotal_quantity,(SELECTp.payment_statusFROMpayments pWHEREp.order_ido.order_idANDp.payment_typePRIMARY)ASpayment_status,(SELECTAVG(rating)FROMreviews rWHEREr.customer_ido.customer_idANDr.create_dateo.order_date)ASavg_rating_after_orderFROMorders oWHEREo.order_dateBETWEEN2025-06-01AND2025-06-30ANDo.statusCOMPLETEDORDERBYo.total_amountDESCOFFSET0ROWSFETCHNEXT50ROWSONLY;200并发下旧版本直接就扛不住了TPS掉到3以下响应时间P99超过了60秒超时率飙升。V9R4C019虽然也有性能下降这很正常并发翻倍嘛但TPS还能维持在7到8左右P95响应时间在20秒以内。作为对比SQL Server在200并发下TPS大概是9到10P95在15秒左右。你看这个数据差距在缩小。在100并发时V9R4C019跟SQL Server的TPS差距大约是18%11.4 vs 13.5在200并发时差距缩小到了约20%到30%7.5 vs 9.5。考虑误差和不同并发下的负载特性基本可以说在同一个量级上了。兼容性不只是能跑聊完了性能我想再说说兼容性。因为迁移这件事性能和兼容是两条腿缺一不可。性能再好如果SQL跑不了那也白搭。V9R4C019在兼容性方面补齐了几个很重要的能力。之前我提到过SQL Server特有的MERGE、并行DML、OUTPUT子句这次都原生支持了。这意味着很多原来需要手工改写的T-SQL代码现在直接连上去就能跑。拿OUTPUT子句来说在SQL Server里这玩意儿用得特别多特别是在审计日志场景-- SQL Server里的审计日志写入-- 用OUTPUT子句把删除的数据自动写入审计表DELETEFROMuser_sessions OUTPUT deleted.session_id,deleted.user_id,deleted.login_time,SYSDATETIME()INTOsession_audit_log(session_id,user_id,login_time,delete_time)WHERElast_activity_timeDATEADD(MINUTE,-30,SYSDATETIME());-- 更新操作也可以用OUTPUTUPDATEinventorySETstock_countstock_count-o.order_quantity,update_timeSYSDATETIME()OUTPUT inserted.product_id,inserted.stock_count,deleted.stock_count,inserted.update_timeINTOinventory_change_log(product_id,new_stock,old_stock,change_time)FROMorders oWHEREinventory.product_ido.product_idANDo.statusCONFIRMED;这种SQL在以前的国产数据库上基本跑不了你得拆成两步先查出要删除/更新的数据执行操作再把数据写入审计表。代码量翻倍而且不是原子操作了中间出问题可能导致审计数据丢失。现在直接支持OUTPUT子句了迁移的时候就不用改这部分代码。这就叫0改造——不是说所有SQL都不用改而是大部分常见的SQL Server特有写法都能直接跑了。窗口函数方面也有改进。SQL Server的窗口函数支持得比较全面特别是RANGE类型的窗口框架-- SQL Server窗口函数RANGE类型的窗口框架SELECTemployee_id,department_id,salary,hire_date,-- 累计薪资SUM(salary)OVER(PARTITIONBYdepartment_idORDERBYhire_date RANGEBETWEENUNBOUNDEDPRECEDINGANDCURRENTROW)AScumulative_salary,-- 部门平均薪资同一入职日期的AVG(salary)OVER(PARTITIONBYdepartment_idORDERBYhire_date RANGEBETWEENUNBOUNDEDPRECEDINGANDUNBOUNDEDFOLLOWING)ASdept_avg_salary,-- 前后记录对比LAG(salary,1)OVER(PARTITIONBYdepartment_idORDERBYhire_date)ASprev_salary,LEAD(salary,1)OVER(PARTITIONBYdepartment_idORDERBYhire_date)ASnext_salaryFROMemployeesWHEREdepartment_idIN(1,2,3,4,5);PIVOT/UNPIVOT也是SQL Server里的高频操作做行列转换特别方便-- PIVOT行转列SELECTproduct_id,[Q1],[Q2],[Q3],[Q4]FROM(SELECTproduct_id,quarter,sales_amountFROMquarterly_sales)ASsrcPIVOT(SUM(sales_amount)FORquarterIN([Q1],[Q2],[Q3],[Q4]))ASpiv;-- UNPIVOT列转行SELECTproduct_id,quarter,sales_amountFROMquarterly_sales_wideUNPIVOT(sales_amountFORquarterIN([Q1],[Q2],[Q3],[Q4]))ASunpiv;这些在V9R4C019里都支持了。另外LIKE通配符的全面支持也很实用SQL Server里 LIKE ‘[a-c]%’ 这种字符范围匹配终于不用改了。还有一个我觉得比较有意思的特性是跨库访问系统视图。以前在不同数据库之间做数据交互基本要靠ETL或者dblink现在Oracle、MySQL、KingbaseES之间可以直接通过系统视图互访。这个在迁移过渡期特别有用因为迁移通常不是一刀切的可能有段时间需要新旧库并存。这个案例还有一个细节我觉得值得注意客户说查询效率、并发能力、运维成本均优于原有架构。这说明迁移之后不只是能用而且是更好用了。当然客户的评价可能有滤镜但从技术角度来说如果原来用的是老旧版本的SQL Server或者其他商业数据库迁到新一代的国产数据库上性能提升也不是不可能的——毕竟数据库技术也在发展新的优化器、新的存储引擎、新的缓存机制都可能带来性能红利。不过我也要提一句这个案例里迁的主要是从Oracle和MySQL过来的系统不是SQL Server。SQL Server迁移的技术难度跟Oracle/MySQL不是一个量级的因为SQL Server的T-SQL语法体系跟其他数据库差异更大。所以这个案例的极简体验不能直接推导到SQL Server迁移场景上。SQL Server迁移要达到同样的极简程度还需要更多的语法兼容和优化器改进。运维侧的体验变化做迁移不能只看迁移过程和性能运维侧的体验也很重要。因为迁移完之后系统就交给运维团队来管了如果运维工具跟不上出了问题排查不了那也是个坑。V9R4C019在运维方面加了不少东西。首先是内置的故障收集分析工具能自动抓取日志、生成诊断报告。这个功能对于运维来说太重要了——以前出了问题DBA要手动去翻日志文件、拼凑各种信息来判断根因这个过程既耗时又容易遗漏。有了自动化的故障收集分析工具从人工拼图变成了工具出结论效率完全不一样了。官方说出了问题5分钟找到根因这个时间我觉得还算合理。当然前提是故障类型在工具的识别范围内太复杂的故障可能还是需要人工介入。但至少常见的故障——比如锁等待、死锁、连接超时、空间不足这些——应该能自动识别。HA组件也做了改进支持独立运维企业可以自主管理切换逻辑。以前很多国产数据库的HA组件是个黑盒子出问题只能找厂商支持。现在能自己管了运维团队的主控权更强了。备份恢复方面也有升级。块级增量备份变成了默认的归档模式全量增量归档日志三位一体。PITRPoint-in-Time Recovery支持最优恢复路径计算能自动选最快的恢复路径。备份窗口从小时级压缩到分钟级。这些变化对运维的实际影响是什么呢我举个具体场景# 以前的备份方式概念性示意# 全量备份每周日凌晨2点耗时约3小时pg_basebackup-D/backup/full-Fp-Xs-P# 归档日志持续WAL归档archive_commandcp %p /archive/%f# 恢复时需要手动定位时间点手动选择恢复路径# 恢复耗时全量恢复(3h) 日志回放(看日志量)# V9R4C019的备份方式# 块级增量备份默认开启# 全量周日耗时约40分钟块级备份比文件级快得多# 增量每天耗时约5-10分钟# 归档日志持续# PITR恢复自动选最优路径# 如果时间点在最近一次增量之后只需要回放少量日志# 恢复耗时可能只有几分钟从3小时备份到40分钟备份从手动选恢复路径到自动选最优路径这些变化在运维日常中是真金白银的时间节约。智能调优不改应用也能优化还有一个功能我觉得值得一提QueryMapping模糊匹配和智能SQL调优建议。迁移之后经常会遇到一个尴尬的问题某些SQL在原来的数据库上跑得很快但在新库上很慢。原因可能是优化器选择了不同的执行计划或者索引策略不同。这时候如果改SQL就要动应用代码走开发流程、测试流程、发布流程周期很长。不改SQL吧性能又不行。QueryMapping的模糊匹配能自动发现相近SQL的执行差异。什么意思呢就是它会把你的SQL跟数据库里执行过的其他SQL做比对找到那些结构相似但执行计划差异很大的SQL。写在最后的一些思考写到这里我这个测试毕竟只覆盖了一个特定场景。不同业务系统的SQL写法千差万别有些场景可能表现好有些可能还有差距。我不能因为一个场景测出来不错就拍胸脯说所有场景都没问题——这不严谨。真正要做迁移决策的时候还是得拿你自己的业务SQL去测拿你自己的数据量去压别人测出来的数据只能做参考。最后我想说数据库迁移从来不是终点而是一个新的起点。迁过去之后还有很长的路要走——持续调优、版本升级、架构演进。但至少V9R4C019让这条路的起点不那么坎坷了。至于后面能走多远取决于厂商的持续投入和用户的深度使用。我们拭目以待吧。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进