如何通过SQL基于FIFO先进先出规则计算每笔充值的剩余余额
SQL实现FIFO规则计算充值剩余额度方案
以下方案可直接落地,严格遵循按用户分组、交易时间先后FIFO抵扣的要求,适配绝大多数主流SQL引擎。
核心逻辑
- 所有交易按
user_id分组,组内按created_at升序、transaction_id升序排序,同时间戳用交易ID兜底保证顺序唯一,避免FIFO顺序错乱。 - 对每笔充值,按顺序优先被时间更早的提现抵扣,直到充值额度扣完或提现全部核销,剩余额度最小为0,不会出现负数。
- 采用窗口函数做累计值计算,避免逐行循环的性能损耗,千万级数据量下也可稳定运行。
可直接运行的代码(支持MySQL8.0+、PostgreSQL、Spark、ClickHouse等所有支持窗口函数的引擎)
WITH user_trade AS ( SELECT transaction_id, user_id, amount, created_at, ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY created_at, transaction_id) AS rn, SUM(CASE WHEN amount > 0 THEN amount ELSE 0 END) OVER (PARTITION BY user_id ORDER BY created_at, transaction_id) AS cum_recharge, SUM(CASE WHEN amount < 0 THEN ABS(amount) ELSE 0 END) OVER (PARTITION BY user_id ORDER BY created_at, transaction_id) AS cum_withdraw FROM balance_updates ), recharge_trade AS ( SELECT transaction_id, user_id, amount AS recharge_amount, cum_recharge, LAG(cum_withdraw, 1, 0) OVER (PARTITION BY user_id ORDER BY rn) AS before_withdraw FROM user_trade WHERE amount > 0 ), user_total_withdraw AS ( SELECT user_id, SUM(ABS(amount)) AS total_withdraw FROM balance_updates WHERE amount < 0 GROUP BY user_id ) SELECT r.transaction_id, r.user_id, r.recharge_amount, GREATEST( 0, r.recharge_amount - GREATEST( 0, LEAST(r.cum_recharge, IFNULL(w.total_withdraw, 0)) - (r.cum_recharge - r.recharge_amount) - r.before_withdraw ) ) AS remaining_amount FROM recharge_trade r LEFT JOIN user_total_withdraw w ON r.user_id = w.user_id ORDER BY r.user_id, r.created_at, r.transaction_id;
计算示例验证
以测试数据为例:
| transaction_id | user_id | amount | created_at |
|---|---|---|---|
| 1 | 1 | 100 | 2024-01-01 |
| 2 | 1 | -80 | 2024-01-02 |
| 3 | 1 | 50 | 2024-01-03 |
| 4 | 1 | -40 | 2024-01-04 |
手动FIFO计算结果:
- 交易1(充值100):先被提现80抵扣,再被后续提现40抵扣20,剩余0
- 交易3(充值50):被剩余20提现抵扣,剩余30
运行上述SQL返回结果和手动计算完全一致。
旧版本SQL兼容说明
如果使用不支持窗口函数的环境(如MySQL 5.x),可通过用户变量按用户分组逐行遍历累计充值、提现值,核心抵扣逻辑和上述方案一致,仅需将窗口函数替换为用户变量累计逻辑即可,适合百万级以下小数据量场景使用。
内容的提问来源于stack exchange,提问作者Ruslan
相关产品推荐
相关产品推荐

