3. 坏账率核心指标SQL:分子与分母的精确计算

坏账率是信贷与应收账款管理中最核心的风险指标之一。其计算逻辑看似简单——坏账金额 ÷ 总应收金额,但在实际数据仓库环境中,分子与分母的定义口径、数据来源、时间窗口以及异常值处理,直接决定了指标的准确性与可信度。本节将给出标准化的基础SQL实现,并逐层拆解每个字段的业务含义与技术约束。

核心公式
坏账率(%) = (逾期 / 坏账金额) ÷ (总应收金额) × 100%

3.1 指标定义与口径说明

在编写SQL之前,必须明确分子与分母的业务边界。下表列出了常见口径及其适用场景:

指标组件 口径名称 业务定义 数据来源表
分子(坏账金额) 逾期30天+金额 截至报告日,逾期天数 ≥ 30 的未还本金 + 利息 + 罚息 loan_detail、repayment_schedule
核销坏账金额 已确认无法收回、完成财务核销的贷款余额 write_off_record
分母(总应收金额) 期末在贷余额 报告日所有未结清贷款的本金余额之和 loan_detail
当期应还总额 报告周期内所有到期应还的本息合计 repayment_schedule
推荐口径:在数据质量评估场景中,建议分子使用“逾期30天+金额”,分母使用“期末在贷余额”。该口径对风险反应最敏感,且易于与行业基准对标。

3.2 分子SQL:逾期/坏账金额计算

分子需要从还款计划表中筛选出逾期记录,并汇总其未还金额。以下SQL实现了按产品、按日期的逾期金额聚合:

WITH overdue_detail AS (
    SELECT
        rs.loan_id,
        rs.due_date,
        rs.principal_due + rs.interest_due + rs.penalty_due AS total_due,
        COALESCE(rp.total_paid, 0) AS total_paid,
        DATEDIFF('day', rs.due_date, CURRENT_DATE) AS overdue_days,
        CASE
            WHEN DATEDIFF('day', rs.due_date, CURRENT_DATE) >= 30
                THEN (rs.principal_due + rs.interest_due + rs.penalty_due)
                     - COALESCE(rp.total_paid, 0)
            ELSE 0
        END AS overdue_amount_30plus
    FROM repayment_schedule rs
    LEFT JOIN (
        SELECT
            loan_id,
            due_date,
            SUM(paid_amount) AS total_paid
        FROM repayment_record
        GROUP BY loan_id, due_date
    ) rp
        ON rs.loan_id = rp.loan_id
        AND rs.due_date = rp.due_date
    WHERE rs.due_date <= CURRENT_DATE
)
SELECT
    CURRENT_DATE AS report_date,
    SUM(overdue_amount_30plus) AS total_overdue_amount
FROM overdue_detail
WHERE overdue_amount_30plus > 0;
数据质量检查点:
• 确认 repayment_schedule 中每条记录都有唯一的 loan_id + due_date 组合,避免重复聚合。
• 注意 DATEDIFF 函数在不同数据库中的语法差异(SQL Server 用 DATEDIFF(day, …),MySQL 用 DATEDIFF(…),PostgreSQL 用 CURRENT_DATE - due_date)。
• 逾期天数计算应使用业务日历,而非自然日,避免周末/节假日干扰。

3.3 分母SQL:总应收金额计算

分母通常取报告日所有在贷资产的本金余额。以下SQL从贷款主表中获取当前有效贷款的本金合计:

WITH active_loans AS (
    SELECT
        loan_id,
        product_type,
        principal_amount,
        repaid_principal,
        (principal_amount - repaid_principal) AS outstanding_principal,
        loan_status
    FROM loan_detail
    WHERE loan_status IN ('ACTIVE', 'OVERDUE', 'RESTRUCTURED')
      AND disbursement_date <= CURRENT_DATE
)
SELECT
    CURRENT_DATE AS report_date,
    COUNT(loan_id) AS total_loan_count,
    SUM(outstanding_principal) AS total_outstanding_principal
FROM active_loans;
分母扩展建议:如果业务需要计算“滚动坏账率”,分母应替换为“过去12个月平均在贷余额”。此时需对 loan_detail 表按月快照并取均值。

3.4 坏账率核心指标SQL(完整版)

将分子与分母合并,得到最终的坏账率指标。同时加入分组维度,支持按产品、按渠道、按地区下钻分析:

WITH
-- 分子:逾期30天+金额(按产品分组)
overdue_by_product AS (
    SELECT
        ld.product_type,
        SUM(
            CASE
                WHEN DATEDIFF('day', rs.due_date, CURRENT_DATE) >= 30
                    THEN (rs.principal_due + rs.interest_due + rs.penalty_due)
                         - COALESCE(rp.total_paid, 0)
                ELSE 0
            END
        ) AS overdue_amount
    FROM repayment_schedule rs
    JOIN loan_detail ld ON rs.loan_id = ld.loan_id
    LEFT JOIN (
        SELECT loan_id, due_date, SUM(paid_amount) AS total_paid
        FROM repayment_record
        GROUP BY loan_id, due_date
    ) rp ON rs.loan_id = rp.loan_id AND rs.due_date = rp.due_date
    WHERE rs.due_date <= CURRENT_DATE
    GROUP BY ld.product_type
),
-- 分母:期末在贷余额(按产品分组)
outstanding_by_product AS (
    SELECT
        product_type,
        SUM(principal_amount - repaid_principal) AS outstanding_principal
    FROM loan_detail
    WHERE loan_status IN ('ACTIVE', 'OVERDUE', 'RESTRUCTURED')
      AND disbursement_date <= CURRENT_DATE
    GROUP BY product_type
)
SELECT
    COALESCE(o.product_type, d.product_type) AS product_type,
    CURRENT_DATE AS report_date,
    ROUND(COALESCE(d.overdue_amount, 0), 2) AS overdue_amount,
    ROUND(COALESCE(o.outstanding_principal, 0), 2) AS outstanding_principal,
    ROUND(
        CASE
            WHEN COALESCE(o.outstanding_principal, 0) = 0 THEN 0
            ELSE COALESCE(d.overdue_amount, 0) / o.outstanding_principal * 100
        END,
        4
    ) AS bad_debt_rate_pct
FROM outstanding_by_product o
FULL OUTER JOIN overdue_by_product d ON o.product_type = d.product_type
ORDER BY bad_debt_rate_pct DESC;

3.5 坏账率计算数据流架构

下图展示了从源表到最终指标的数据流转过程,以及每个环节的数据质量校验点:

坏账率计算数据流与质量校验点 repayment_schedule repayment_record loan_detail 数据清洗层:去重、空值填充、逾期天数计算、状态过滤 分子聚合:逾期30天+金额 分母聚合:期末在贷余额 坏账率 = 分子 ÷ 分母 × 100% 质量校验点: ① 还款计划与还款记录关联完整性 ② 逾期天数计算一致性 ③ 贷款状态枚举值合法性 ④ 金额字段非负校验 ⑤ 分组维度完整性检查

3.6 常见数据质量陷阱与规避

  • 重复还款记录:同一笔还款被录入多次,导致分子被低估。应在 repayment_record 层按 loan_id + due_date + payment_seq 去重。
  • 逾期天数计算基准不一致:部分系统使用自然日,部分使用工作日。建议在ETL层统一为业务日历,并在指标元数据中标注。
  • 分母包含已核销贷款:已核销贷款不应计入分母,否则会低估坏账率。需在 loan_detail 中过滤 loan_status != 'WRITTEN_OFF'。
  • 跨期还款冲抵:客户提前还款或部分还款时,需按合同约定的冲抵顺序(先息后本)分摊到各期,避免分子计算偏差。
最佳实践:将分子与分母的计算逻辑封装为数据库视图或存储过程,并加入数据质量监控探针(如:分子 > 分母时触发告警)。每次跑批前先执行质量探针,通过后再生成最终指标。