SQL Server中财务对账90%场景应改用CTE或窗口函数;必须用子查询时,须避开WHERE中SUM()、避免相关子查询全表扫描、所有空值用COALESCE()兜底。

直接说结论:SQL Server 里用子查询做财务对账,90% 的场景该换 CTE 或 窗口函数;真要用子查询,必须避开 WHERE 中写 SUM()、避免相关子查询扫全表、所有空值必须用 COALESCE() 显式兜底。
WHERE 中不能直接用 SUM() 做结转过滤
常见错误是写:SELECT * FROM ledger WHERE balance = (SELECT SUM(debit) - SUM(credit) FROM ledger_sub WHERE period <= '202604'),结果报错 aggregate functions are not allowed in WHERE。因为 WHERE 执行在分组和聚合之前,SUM() 还没算出来。
- 正确做法是把累计逻辑提到外层:先用窗口函数算滚动余额,再加
WHERE——SELECT * FROM (SELECT period, SUM(debit - credit) OVER (ORDER BY period ROWS UNBOUNDED PRECEDING) AS cum_balance FROM ledger) t WHERE period = '202604' - 若必须用子查询,得确保内层只返回单值且不含聚合:比如查上期期末,写成
(SELECT COALESCE(SUM(debit - credit), 0) FROM ledger WHERE period = '202603'),而不是带GROUP BY的子查询 - SQL Server 2016+ 支持
LAG(),比多层子查询更稳:LAG(cum_balance, 1, 0) OVER (ORDER BY period)直接取上期值,不依赖子查询嵌套
相关子查询在对账中极易引发 N+1 扫描
比如写 SELECT account_id, (SELECT SUM(debit - credit) FROM ledger l2 WHERE l2.account_id = l1.account_id AND l2.period <= l1.period) AS bal FROM accounts l1,表面看是按账户滚动求和,实际每行都触发一次全表扫描,1000 个账户 × 每次扫 5 万行 = 5000 万行 I/O,对账跑十几分钟。
- 优先改用窗口函数:
SUM(debit - credit) OVER (PARTITION BY account_id ORDER BY period ROWS UNBOUNDED PRECEDING),一次排序全部搞定 - 若业务强依赖子查询(如跨库或权限隔离),必须给
ledger(account_id, period)加联合索引,否则索引失效 - SQL Server 不支持
LATERAL,但可用APPLY替代:用CROSS APPLY (SELECT SUM(...) FROM ledger WHERE ...),执行计划更可控,且能利用外层参数走索引
子查询返回多行或 NULL 导致对账结果错乱
典型现象:对账单里某账户“期末余额”显示为 NULL,但原始数据明明有值;或者扣款校验时整条记录被过滤掉,实际应填默认值 0。
-
IN子句遇到子查询返回NULL会整体失效——比如WHERE id IN (SELECT ref_id FROM logs),只要logs.ref_id有一条为NULL,整个条件变UNKNOWN,该行直接丢弃 - 一律改用
EXISTS:WHERE EXISTS (SELECT 1 FROM logs WHERE logs.ref_id = accounts.id),不受NULL影响 - 所有标量子查询必须包
COALESCE(..., 0),尤其期初/本期发生额:哪怕子查询只查一行,只要该行字段为NULL,NULL + 100还是NULL,不是 100 - SQL Server 2017+ 可用
TRY_CAST防止类型转换失败中断对账流程,老版本只能靠CASE WHEN ISNUMERIC() = 1 THEN CAST(...) ELSE 0 END
FROM 子句里的子查询必须起别名且注意物化时机
写 SELECT * FROM (SELECT account_id, SUM(debit - credit) bal FROM ledger GROUP BY account_id) WHERE bal > 0,SQL Server 会报错 Every derived table must have its own alias;更隐蔽的问题是,这个子查询在 SQL Server 中未必物化,可能被优化器重写成关联子查询,导致性能崩盘。
- 强制起别名:
SELECT * FROM (SELECT account_id, SUM(debit - credit) bal FROM ledger GROUP BY account_id) t WHERE t.bal > 0 - 用
OPTION (FORCE ORDER)或OPTION (HASH GROUP)提示优化器按预期执行顺序处理,避免意外重排 - 对超大数据集(千万级 ledger 行),先用
SELECT INTO #temp落临时表,再查临时表——SQL Server 对临时表统计信息更准,执行计划更稳定 - 别名别用
t1、a这类无意义缩写,比如写成acc_bal,后续维护时一眼知道这层干啥
最常被忽略的一点:SQL Server 的子查询在事务中不自动继承锁行为。比如你用子查询查出余额后 UPDATE,中间可能被并发修改;真正安全的做法是用 SELECT ... FROM ledger WITH (UPDLOCK, HOLDLOCK) 显式加锁,而不是依赖子查询本身。

















