PAYATHON 2026

如何在 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 或更高版本。

备注:内容仅供参考。