SQL

Group loans into delinquency buckets with SQL CASE WHEN

One query that buckets loans by days past due and shows each bucket's share of the book, with a note on what changes between database engines.

By Adekunle Okunade2 min read

Sample article. Created to demonstrate the layout. The data in it is invented.

A delinquency report is usually one table: how many loans, how much money and what share of the book sits in each bucket. SQL can produce it in a single query.

The table

The examples assume a loans table with three columns:

Column Meaning
loan_id One row per loan
outstanding Balance still owed
days_past_due Days overdue, 0 if current

The query

CASE WHEN creates the buckets. A window function, SUM(...) OVER (), gives the total of all buckets so each row can show its share.

sql
SELECT
  CASE
    WHEN days_past_due = 0 THEN 'Current'
    WHEN days_past_due BETWEEN 1 AND 30 THEN '1-30'
    WHEN days_past_due BETWEEN 31 AND 60 THEN '31-60'
    WHEN days_past_due BETWEEN 61 AND 90 THEN '61-90'
    ELSE '90+'
  END AS bucket,
  COUNT(*) AS loans,
  SUM(outstanding) AS outstanding,
  ROUND(100.0 * SUM(outstanding) / SUM(SUM(outstanding)) OVER (), 1) AS pct_of_book
FROM loans
GROUP BY 1
ORDER BY MIN(days_past_due);

Run on a six-loan sample book, it returns:

bucket loans outstanding pct_of_book
Current 2 400000 44.0
1-30 1 250000 27.5
31-60 1 80000 8.8
61-90 1 120000 13.2
90+ 1 60000 6.6

How it works

  1. CASE WHEN tests each loan from top to bottom and stops at the first match, so the order of the conditions matters.
  2. GROUP BY 1 groups by the first selected column, the bucket.
  3. SUM(SUM(outstanding)) OVER () sums the bucket totals across all rows, giving the total book without a second query.
  4. ORDER BY MIN(days_past_due) keeps the buckets in a sensible order instead of alphabetical.

What changes between databases

  • SQL Server does not allow GROUP BY 1. Repeat the whole CASE expression in the GROUP BY clause.
  • Rounding and integer division differ between engines. Multiplying by 100.0 rather than 100 forces decimal arithmetic.
  • Window functions need a reasonably recent version of MySQL (8.0 or later). PostgreSQL, SQL Server and SQLite support them.

This is a sample article created to demonstrate the layout. The data is invented.

  • SQL
  • credit risk
  • tutorial
  • PAR

Useful analytics insights, tools and resources.

No noise. An email now and then, and you can unsubscribe in one click.

SQL5 min read

Build a roll-rate table in SQL

A delinquency report tells you where loans are today. A roll rate tells you where they are heading. Here is how to build one with a SQL window function, with output you can check by hand.

  • SQL
  • credit risk
  • roll rates
  • window functions
  • tutorial

Python2 min readSample

How to calculate PAR30, PAR60 and PAR90 in pandas

A step-by-step walkthrough of bucketing loans by days past due and measuring portfolio at risk in Python, using a small made-up loan book.

  • pandas
  • credit risk
  • PAR
  • tutorial