SQL Server 数据迁移进入验收阶段后,业务侧问得最多的并不是表迁完了没有,而是报表还能不能像原来一样用。简单查询跑通只能证明连接、对象和基础语法没有问题,真正压在项目组心里的,是那些已经运行多年的 BI 查询:表多、统计口径复杂,SQL 中还夹着标量子查询,一到月初或者经营分析会就会同时涌入大量请求。
这次迁移的目标环境是 KingbaseES V9R4C019。对象迁移和数据核对完成后,性能验收没有选一条刻意简化的 SQL,而是保留了报表系统里较典型的一类查询:订单、明细、客户、区域、商品分类多表关联,再按订单明细计算退款金额。单次执行要处理明细聚合,标量子查询还会引用外层订单明细号,正好能检验迁移后复杂查询的执行效率和并发稳定性。
压测记录给出的结果很直接:100 并发下,复杂查询 TPS 比迁移前提升 60%,平均响应时间约为原来的 1/10。原来需要等待较长时间的 BI 报表,在迁移后的环境中能够更快返回。这个结果不是用一条 SELECT 1 得出来的,而是从业务 SQL、结果一致性、索引和统计信息一路核对后的迁移验收结果。
迁完对象,只完成了前半程
SQL Server 到 KingbaseES 的迁移,通常先处理表、视图、函数、存储过程、数据类型和数据本身。KingbaseES SQL Server 迁移指南列出了迁移环境、对象和验证路径,这些工作解决的是"能不能迁、能不能运行"。
BI 系统还需要再过一道性能验收。它读取的往往不是单表,而是一组事实表和维度表;一个报表页面可能同时发出多条统计 SQL,正常办公时又会出现明显的并发峰值。迁移后的数据库即使每条 SQL 都能返回正确结果,只要高并发下排队时间拉长,业务侧仍会觉得迁移没有完成。
因此,验收路径被拆成三个部分:先确认 SQL Server 原有查询的业务口径,再核对迁移后的对象、类型和结果集,最后观察 KingbaseES 上的执行计划、索引命中和并发表现。

这套安排有一个很实际的好处:语法兼容、数据一致性和性能问题不会混在一起。查询报错时先处理兼容;结果不同先查数据和口径;只有结果一致后,TPS 和响应时间对比才有意义。
先把迁移口径固定下来
迁移前后的查询不能只比较一条返回记录。报表使用的是一个完整时间窗口,订单状态、退款状态、金额精度和区域分类都要保持相同,时间边界尤其容易被忽略。SQL Server 中 datetime2 可以保留更细的时间精度,迁移到目标端后,参数类型和比较方式要统一,否则同一秒边界上的订单可能落在不同结果集中。
金额字段也不能在比对时直接转成浮点数。验收表保留 numeric(18,2) 的金额精度,订单数用整数,区域和分类使用迁移后的标准化编码。每个维度先按键排序,再做差集,金额则使用固定的小数位比较。这样查出的差异能对应到具体订单或明细,不会被格式差异掩盖。
sql
SELECT COUNT(*) AS row_count,
SUM(gross_amount) AS gross_amount,
SUM(refund_amount) AS refund_amount,
SUM(net_amount) AS net_amount
FROM verify_report_kingbase
WHERE report_date = DATE '2026-07-01';
如果总行数相同但金额不同,还要继续按 region_name、category_name 拆分。迁移工具完成的是数据搬运和对象转换,业务口径仍然要由报表 SQL 自己证明。
退款为空和退款为零也要分开确认:没有退款记录时,结果应由 COALESCE 转成零;有退款记录但金额为零时,仍然应该保留明细。区域编码、分类编码的大小写和尾部空格也会影响分组结果,迁移后的字符类型、排序规则和清洗逻辑需要与报表原有口径保持一致。只有这些细节都对齐,后面看到的性能变化才是在比较同一件事情。
把原来的复杂查询完整保留下来
报表按区域和商品分类统计已支付订单,同时扣除成功退款。核心数据分布在六张表中:bi_sales_order 保存订单主数据,bi_sales_order_item 保存商品明细,dim_product、dim_customer 和 dim_region 提供商品、客户及区域维度,bi_refund 按订单明细记录退款。
SQL Server 中的原始查询使用 DATEADD、ISNULL 和相关标量子查询。退款金额需要按当前订单明细号再次访问退款表,因此它比普通的多表关联更容易放大执行计划选择带来的差异。
sql
DECLARE @begin_time datetime2 = '2026-07-01 00:00:00';
DECLARE @end_time datetime2 = DATEADD(day, 1, @begin_time);
SELECT q.region_name,
q.category_name,
COUNT(DISTINCT q.order_id) AS order_count,
SUM(q.gross_amount) AS gross_amount,
SUM(q.refund_amount) AS refund_amount,
SUM(q.gross_amount)
- SUM(q.refund_amount) AS net_amount
FROM (
SELECT o.order_id,
oi.order_item_id,
r.region_name,
p.category_name,
oi.quantity * oi.sale_price AS gross_amount,
ISNULL((
SELECT SUM(rf.refund_amount)
FROM bi_refund AS rf
WHERE rf.order_item_id = oi.order_item_id
AND rf.refund_status = 'SUCCESS'
), 0) AS refund_amount
FROM bi_sales_order AS o
JOIN bi_sales_order_item AS oi
ON oi.order_id = o.order_id
JOIN dim_product AS p
ON p.product_id = oi.product_id
JOIN dim_customer AS c
ON c.customer_id = o.customer_id
JOIN dim_region AS r
ON r.region_id = c.region_id
WHERE o.order_status = 'PAID'
AND o.pay_time >= @begin_time
AND o.pay_time < @end_time
) AS q
GROUP BY q.region_name,
q.category_name
ORDER BY q.region_name,
q.category_name;
这类 SQL 的麻烦不在行数多,而在执行路径容易变长。订单先与明细、商品、客户和区域关联,退款标量子查询再按外层 order_item_id 取值,最外层完成区域和分类汇总。数据量和并发上来以后,退款表的访问次数、连接顺序以及中间结果规模都会影响响应时间。
先保持业务语义,再做集合化改写
迁入 KingbaseES 后,第一版 SQL 只做必要的语法适配。ISNULL 改为 COALESCE,时间边界使用 TIMESTAMP 字面量,其他关联和聚合口径保持不变。这样得到的结果可以与 SQL Server 基线逐项对照。
sql
WITH order_summary AS (
SELECT o.order_id,
oi.order_item_id,
r.region_name,
p.category_name,
oi.quantity * oi.sale_price AS gross_amount,
COALESCE((
SELECT SUM(rf.refund_amount)
FROM bi_refund AS rf
WHERE rf.order_item_id = oi.order_item_id
AND rf.refund_status = 'SUCCESS'
), 0) AS refund_amount
FROM bi_sales_order AS o
JOIN bi_sales_order_item AS oi
ON oi.order_id = o.order_id
JOIN dim_product AS p
ON p.product_id = oi.product_id
JOIN dim_customer AS c
ON c.customer_id = o.customer_id
JOIN dim_region AS r
ON r.region_id = c.region_id
WHERE o.order_status = 'PAID'
AND o.pay_time >= TIMESTAMP '2026-07-01 00:00:00'
AND o.pay_time < TIMESTAMP '2026-07-02 00:00:00'
)
SELECT region_name,
category_name,
COUNT(DISTINCT order_id) AS order_count,
SUM(gross_amount) AS gross_amount,
SUM(refund_amount) AS refund_amount,
SUM(gross_amount - refund_amount) AS net_amount
FROM order_summary
GROUP BY region_name,
category_name
ORDER BY region_name,
category_name;
结果核对完成后,退款计算改成按订单明细预聚合。原来嵌在明细结果中的标量子查询被展开为独立集合,优化器可以在退款汇总结果和订单明细之间选择更合适的连接路径,也避免同一明细行重复触发退款访问。
sql
WITH refund_agg AS (
SELECT order_item_id,
SUM(refund_amount) AS refund_amount
FROM bi_refund
WHERE refund_status = 'SUCCESS'
GROUP BY order_item_id
),
order_detail AS (
SELECT o.order_id,
oi.order_item_id,
r.region_name,
p.category_name,
oi.quantity * oi.sale_price AS gross_amount,
COALESCE(ra.refund_amount, 0) AS refund_amount
FROM bi_sales_order AS o
JOIN bi_sales_order_item AS oi
ON oi.order_id = o.order_id
JOIN dim_product AS p
ON p.product_id = oi.product_id
JOIN dim_customer AS c
ON c.customer_id = o.customer_id
JOIN dim_region AS r
ON r.region_id = c.region_id
LEFT JOIN refund_agg AS ra
ON ra.order_item_id = oi.order_item_id
WHERE o.order_status = 'PAID'
AND o.pay_time >= TIMESTAMP '2026-07-01 00:00:00'
AND o.pay_time < TIMESTAMP '2026-07-02 00:00:00'
)
SELECT region_name,
category_name,
COUNT(DISTINCT order_id) AS order_count,
SUM(gross_amount) AS gross_amount,
SUM(refund_amount) AS refund_amount,
SUM(gross_amount - refund_amount) AS net_amount
FROM order_detail
GROUP BY region_name,
category_name
ORDER BY region_name,
category_name;
改写前后都必须返回相同的区域、分类、订单数、销售金额、退款金额和净额。可以把两端结果导入两张验收表,通过双向差集和金额差异检查确认业务口径没有变化。
sql
SELECT region_name, category_name, order_count,
gross_amount, refund_amount, net_amount
FROM verify_report_sqlserver
EXCEPT
SELECT region_name, category_name, order_count,
gross_amount, refund_amount, net_amount
FROM verify_report_kingbase;
SELECT region_name, category_name, order_count,
gross_amount, refund_amount, net_amount
FROM verify_report_kingbase
EXCEPT
SELECT region_name, category_name, order_count,
gross_amount, refund_amount, net_amount
FROM verify_report_sqlserver;
两个方向都没有返回记录,才进入性能对比。这样可以避免为了追求耗时下降,误把少算关联、漏算退款或者时间边界变化当成优化成果。
并发测试也按同一条查询口径执行。请求参数固定为同一个报表日期,连接数固定为 100,返回列不随数据库切换改变;压测客户端只记录成功请求,不把连接失败、语法错误或结果集为空的请求算成 TPS。平均响应时间之外,还需要关注 P95 和错误率,避免平均值很好看,少量慢请求却一直拖住报表页面。
执行计划、索引和统计信息一起核对
同一条复杂 SQL 在两种数据库上的计划节点名称可能不同,验收时没有必要逐字对应,更应该关注扫描范围、连接顺序、标量子查询执行次数、中间结果行数和排序聚合开销。
KingbaseES 可以使用执行计划分析最终查询。ANALYZE 会实际执行 SQL,BUFFERS 可以补充缓存访问情况,适合在测试环境核对,生产环境使用前仍需评估查询开销。
sql
EXPLAIN (ANALYZE, BUFFERS)
WITH refund_agg AS (
SELECT order_item_id,
SUM(refund_amount) AS refund_amount
FROM bi_refund
WHERE refund_status = 'SUCCESS'
GROUP BY order_item_id
)
SELECT o.order_id,
oi.order_item_id,
COALESCE(ra.refund_amount, 0) AS refund_amount
FROM bi_sales_order AS o
JOIN bi_sales_order_item AS oi
ON oi.order_id = o.order_id
LEFT JOIN refund_agg AS ra
ON ra.order_item_id = oi.order_item_id
WHERE o.order_status = 'PAID'
AND o.pay_time >= TIMESTAMP '2026-07-01 00:00:00'
AND o.pay_time < TIMESTAMP '2026-07-02 00:00:00';
索引没有追求"越多越好",而是围绕报表的过滤和关联路径设置。订单表先按状态和支付时间缩小范围,订单与明细通过 order_id 关联,退款再通过 order_item_id 对应到具体商品明细。
sql
CREATE INDEX idx_bi_order_status_pay_time
ON bi_sales_order (order_status, pay_time, customer_id, order_id);
CREATE INDEX idx_bi_order_item_order_product
ON bi_sales_order_item (order_id, product_id, order_item_id);
CREATE INDEX idx_bi_refund_status_item
ON bi_refund (refund_status, order_item_id);
ANALYZE bi_sales_order;
ANALYZE bi_sales_order_item;
ANALYZE bi_refund;
ANALYZE dim_customer;
ANALYZE dim_region;
ANALYZE dim_product;
具体列顺序仍应以数据分布和执行计划为准。这里的组合对应当前报表:order_status 是等值条件,pay_time 是范围条件,order_id 连接订单与明细,order_item_id 连接明细与退款。统计信息刷新后再观察估算行数与实际行数的差距,能够减少迁移初期因统计信息不足造成的计划波动。

左侧查询路径保留了相关标量子查询和多层汇总的特征,右侧把退款数据先按订单明细聚合,再与订单明细结果连接。图中表达的是 SQL 结构和验收关注点,并非某次控制台执行计划的复刻;实际判断仍以 KingbaseES 返回的计划、行数和耗时为准。
100 并发下,性能差异才真正显现
单会话测试只能发现明显的全表扫描或语法问题。BI 报表上线后,同一个分析页面可能被多个部门同时打开,数据库还要承接其他查询。此次验收把复杂查询放到 100 并发下对比,并统一业务数据、统计时间窗、返回字段和结果集口径。
由于现有材料没有给出服务器型号和绝对耗时,性能数据采用迁移前归一化值,避免补写不存在的毫秒数:
| 指标 | SQL Server 迁移前 | KingbaseES V9R4C019 | 对比结果 |
|---|---|---|---|
| 复杂查询 TPS | 1.00 | 1.60 | 提升 60% |
| 平均响应时间 | 1.00 | 0.10 | 约为原来的 1/10 |
| 并发数 | 100 | 100 | 口径一致 |
TPS 上升意味着同一时间内可以完成更多报表查询,响应时间下降则直接改变了前端等待体验。原先容易出现长时间等待的经营分析报表,迁移后能够更快返回,业务人员不必为了同一张报表反复刷新页面。
这组数字属于该业务查询、该数据规模和该并发模型下的迁移验收结果,不能直接替代其他系统的容量评估。它能够确认的是:SQL Server 数据迁移到 KingbaseES 后,复杂 BI 查询并没有停留在"语法可执行"的层面;在结果一致、索引匹配和统计信息完整的条件下,V9R4C019 承接了包含标量子查询与多表关联的实际分析负载,并取得了更好的并发表现。
性能变化还需要回到 SQL 本身解释。第一版兼容 SQL 保留了相关子查询,便于核对迁移前后的语义;经过结果集确认后,热点报表采用按订单明细预聚合的写法,减少重复访问退款表。这个调整没有改变报表的统计含义,却让连接关系和聚合边界更清楚。索引与统计信息的处理同样如此:索引服务于时间过滤和明细关联,统计信息帮助优化器估算实际行数,二者都需要通过计划和并发结果验证,不能仅凭 DDL 是否执行成功来判断效果。
性能验收要留住可复查的证据
数据库迁移上线前,兼容率、对象数量和数据行数都比较容易形成清单,性能却常被一句"测试过了"带过。复杂查询最好至少保留原始 SQL、迁移后 SQL、结果差异、执行计划和并发指标五类材料。后续数据量增长或报表逻辑调整时,可以沿着同一口径重新验证,而不是重新猜测问题出在哪里。
这次 BI 查询的改善来自几项具体工作共同作用:迁移后的查询保持了原有业务语义,相关标量子查询被转换成可统一优化的集合运算,索引覆盖了时间过滤和订单关联路径,统计信息也在压测前完成刷新。KingbaseES V9R4C019 最终交付的不只是迁移工具中的"成功"状态,而是一套能够继续承接高并发报表的查询环境。
对 SQL Server 数据迁移项目来说,表能查、程序能连只是切换条件;复杂 SQL 的结果不变,高并发下响应稳定,业务人员打开报表时不再长时间等待,迁移才真正进入可交付状态。