Postgres/SQL:按 payment source 计算 charges 与 refunds
支付小周
结论
先按 charge_id 汇总退款,再把每笔 charge 的净使用金额按 credit_id 汇总。这样,每个 charges.amount 只会参与一次计算,不会因关联多条 refunds 而被重复累计。
WITH refund_totals AS (
SELECT
charge_id,
SUM(amount) AS refunded_amount
FROM refunds
GROUP BY charge_id
),
credit_usage AS (
SELECT
ch.credit_id,
SUM(ch.amount - COALESCE(rt.refunded_amount, 0)) AS used_amount
FROM charges AS ch
LEFT JOIN refund_totals AS rt
ON rt.charge_id = ch.id
GROUP BY ch.credit_id
)
SELECT
c.*,
c.amount - COALESCE(cu.used_amount, 0) AS unspent_amount
FROM credits AS c
LEFT JOIN credit_usage AS cu
ON cu.credit_id = c.id
WHERE c.amount > COALESCE(cu.used_amount, 0)
LIMIT 1;
原查询为什么会重复计算
原查询直接连接了三张表:
credits → charges → refunds
如果一笔 charge 对应两条 refund,连接后的中间结果会出现两行:
charge.amount | refund.amount
--------------+--------------
25.0 | 20.0
25.0 | 5.0
这时执行:
SUM(charges.amount)
结果是 50.0,而不是 25.0。问题不在 SUM() 本身,而是一对多 JOIN 在聚合前复制了 charge 行。
查询如何计算
第一步是按 charge_id 汇总退款:
SELECT
charge_id,
SUM(amount) AS refunded_amount
FROM refunds
GROUP BY charge_id;
示例数据的结果是:
charge_id | refunded_amount
----------+----------------
1 | 25.0
第二步计算每笔 charge 实际占用的 credit:
net charge = charge amount - refunded amount
再按 credit_id 汇总:
SUM(ch.amount - COALESCE(rt.refunded_amount, 0))
最后计算剩余额度:
unspent amount = credit amount - used amount
对于示例数据:
used_amount = 25.0 - (20.0 + 5.0) = 0.0
unspent_amount = 25.0 - 0.0 = 25.0
最终结果为:
id | amount | unspent_amount
---+--------+---------------
1 | 25.0 | 25.0
不要使用 SUM(DISTINCT charges.amount)
一种看似可行的写法是:
SUM(DISTINCT charges.amount)
但这种写法并不正确。DISTINCT 去重的是 amount 值,而不是 charge ID。假设同一个 credit 有两笔不同的 charge,金额刚好都是 10.0,SUM(DISTINCT ...) 只会计算一次,结果会从正确的 20.0 变成 10.0。
正确的做法是先把一对多关系聚合到每个 charge_id 一行,再进行上层汇总。
注意事项
LEFT JOIN会保留完全没有 charges 的 credit,此时COALESCE(..., 0)会把used_amount处理为零。- 没有 refunds 的 charge 也能正常计算,其退款金额按零处理。
- 如果退款总额可以超过 charge 金额,
used_amount可能为负数,导致unspent_amount大于credits.amount。是否要限制这种情况,应由业务规则决定。 LIMIT 1没有搭配ORDER BY时,PostgreSQL 不保证返回哪一条符合条件的 credit。如果需要固定选择顺序,应明确指定,例如:
ORDER BY c.id
LIMIT 1;
备注:内容仅供参考。
