复利分期不能用SUM()或AVG(),因需迭代计算每期本息和,必须用递归CTE或带LIMIT 1的子查询逐期推导,且须用DECIMAL避免浮点误差。

复利分期计算为什么不能直接用 SUM() 或 AVG()
因为复利是“利滚利”,每期本金 = 上期本息和,而标准聚合函数只对原始行做静态汇总,无法表达这种依赖前序结果的迭代关系。SQL 本身不支持原生循环变量,所以必须靠子查询逐层展开——不是为了炫技,而是唯一能逼近金融级精度的方式。
常见错误现象:ERROR: more than one row returned by a subquery used as an expression,说明子查询没加 LIMIT 1 或没绑定到当前期数;或结果偏差 >0.01 元,大概率是浮点数累加误差没控制,或利率没转成小数(比如把 4.8% 写成 4.8 而非 0.048)。
- 务必用
DECIMAL(18,6)存储中间本息值,避免FLOAT累积误差 - 子查询必须关联外层的期数(如
t1.period_no = t2.period_no - 1),否则变成笛卡尔积 - 首期本金要显式初始化,不能依赖 NULL 值参与计算
三层嵌套子查询实现等额本息分期(MySQL / PostgreSQL)
以贷款 100 万元、年化利率 4.8%、分 12 期为例,核心逻辑是:第 n 期本息和 = 第 n-1 期本息和 × (1 + 月利率)。用三层子查询分别处理「上期本息」「当期利息」「当期还款」:
SELECT
period_no,
ROUND(principal_start, 2) AS principal_start,
ROUND(interest, 2) AS interest,
ROUND(repayment, 2) AS repayment,
ROUND(principal_end, 2) AS principal_end
FROM (
SELECT
t1.period_no,
-- 上期期末本金(首期为贷款总额)
COALESCE((
SELECT principal_end
FROM loan_schedule t2
WHERE t2.period_no = t1.period_no - 1
LIMIT 1
), 1000000.00) AS principal_start,
-- 当期利息 = 上期期末本金 × 月利率
COALESCE((
SELECT principal_end
FROM loan_schedule t2
WHERE t2.period_no = t1.period_no - 1
LIMIT 1
), 1000000.00) * 0.048/12 AS interest,
-- 等额本息固定还款额(预计算)
85567.29 AS repayment,
-- 当期期末本金 = 上期期末本金 + 利息 - 还款
COALESCE((
SELECT principal_end
FROM loan_schedule t2
WHERE t2.period_no = t1.period_no - 1
LIMIT 1
), 1000000.00) * (1 + 0.048/12) - 85567.29 AS principal_end
FROM generate_series(1,12) AS t1(period_no)
) AS calc;注意:generate_series() 在 PostgreSQL 中可用;MySQL 需用递归 CTE 或临时数字表。关键点在于所有子查询都带 LIMIT 1 且严格按 period_no - 1 关联,否则会报错或结果错乱。
WITH RECURSIVE 是更稳的替代方案(推荐用于生产)
嵌套子查询在期数多时性能陡降(O(n²)),且难以调试。递归 CTE 把迭代逻辑显式写出,可读性和可控性高得多:
WITH RECURSIVE loan_schedule AS (
-- 锚点:首期
SELECT
1 AS period_no,
1000000.00::DECIMAL(18,6) AS principal_start,
1000000.00 * 0.048/12 AS interest,
85567.29 AS repayment,
(1000000.00 * (1 + 0.048/12) - 85567.29)::DECIMAL(18,6) AS principal_end
UNION ALL
-- 递归:下一期基于本期结果
SELECT
ls.period_no + 1,
ls.principal_end,
ls.principal_end * 0.048/12,
85567.29,
(ls.principal_end * (1 + 0.048/12) - 85567.29)::DECIMAL(18,6)
FROM loan_schedule ls
WHERE ls.period_no < 12
)
SELECT * FROM loan_schedule;这里 ::DECIMAL(18,6) 强制类型转换比隐式转换更安全;WHERE ls.period_no 是终止条件,漏写会导致无限递归。相比嵌套子查询,CTE 的执行计划清晰,且能用 <code>EXPLAIN 直接看迭代次数。
实际业务中必须校验的三个数值断点
金融系统上线前,至少要跑三组边界值验证,仅靠“看起来像”不行:
- 最后一期
principal_end必须 ≈ 0(允许 ±0.01 元误差),否则存在尾差未摊销 - 累计还款总额 =
SUM(repayment),必须等于公式计算值loan_amount * (rate/12) * POWER(1+rate/12, n) / (POWER(1+rate/12, n) - 1) * n - 任意一期
interest + (repayment - interest)必须严格等于repayment,排除四舍五入导致的截断失真
真正难的不是写出第一版 SQL,而是让第 36 期的本金余额和 Excel 模型差值 ≤ 0.001 元——这要求每一步都用 DECIMAL、每处乘除都检查精度丢失点、每次取整都明确是向上/向下/银行家舍入。

















