如何在 SQL 中递归计算两列相减后的 REST?
支付阿杰
结论
不需要递归 CTE。这里要计算的是累计付款金额,用窗口函数 SUM() OVER() 就能完成:
SELECT
Invoice_ID,
Invoice_Total,
Payment,
Invoice_Total
- SUM(Payment) OVER (
PARTITION BY Invoice_ID
ORDER BY Payment_Date, Payment_ID
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
) AS REST
FROM Invoice_Payments
ORDER BY Invoice_ID, Payment_Date, Payment_ID;
计算公式是:
REST = Invoice Total - 截至当前行的累计 Payment
为什么当前结果每行都是 69
如果查询汇总了整张发票的所有付款,那么每一行计算的都是:
93 - (10 + 9 + 5) = 69
所以三行的 REST 完全相同。
需要的结果应当按付款顺序逐行累计:
第一行:93 - 10 = 83
第二行:93 - (10 + 9) = 74
第三行:93 - (10 + 9 + 5) = 69
窗口函数既能保留每条付款记录,又能算出从第一笔付款到当前记录为止的付款总额。
完整示例
假设付款表中有用于确定付款顺序的 Payment_ID:
SELECT
Invoice_ID,
Invoice_Total,
Payment,
Invoice_Total
- SUM(Payment) OVER (
PARTITION BY Invoice_ID
ORDER BY Payment_ID
ROWS UNBOUNDED PRECEDING
) AS REST
FROM Invoice_Payments
ORDER BY Invoice_ID, Payment_ID;
查询结果如下:
Invoice_ID | Invoice_Total | Payment | REST
-----------|---------------|---------|-----
01 | 93 | 10 | 83
01 | 93 | 9 | 74
01 | 93 | 5 | 69
其中:
PARTITION BY Invoice_ID让每张发票单独累计,不会混入其他发票的付款。ORDER BY Payment_ID决定付款的计算顺序。ROWS UNBOUNDED PRECEDING表示从当前发票的第一条付款记录累计到当前行。
发票表和付款表分开时
如果发票总额保存在 Invoices 表,付款记录保存在 Payments 表,可以先关联两张表,再计算累计付款:
SELECT
i.Invoice_ID,
i.Invoice_Total,
p.Payment,
i.Invoice_Total
- SUM(p.Payment) OVER (
PARTITION BY i.Invoice_ID
ORDER BY p.Payment_Date, p.Payment_ID
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
) AS REST
FROM Invoices AS i
JOIN Payments AS p
ON p.Invoice_ID = i.Invoice_ID
ORDER BY
i.Invoice_ID,
p.Payment_Date,
p.Payment_ID;
注意事项
排序字段必须能稳定地确定付款顺序,例如 Payment_Date 和唯一的 Payment_ID。SQL 表没有默认行顺序,不能把查询结果当前显示的先后位置当作付款顺序。
如果多笔付款可能发生在同一天,需要加入唯一字段作为第二个排序条件:
ORDER BY Payment_Date, Payment_ID
窗口范围也应明确写成 ROWS。如果只使用 ORDER BY Payment_Date,部分数据库会默认采用 RANGE。日期相同的付款可能因此被放在同一组中累计,使同一天的多行得到相同余额。
如果 Payment 可以为 NULL,可以用 COALESCE() 将它按零处理:
Invoice_Total
- SUM(COALESCE(Payment, 0)) OVER (
PARTITION BY Invoice_ID
ORDER BY Payment_Date, Payment_ID
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
) AS REST
这种写法要求数据库支持窗口函数,例如 PostgreSQL、SQL Server、Oracle,以及 MySQL 8.0 或更高版本。
备注:内容仅供参考。
