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.
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
CASE WHENtests each loan from top to bottom and stops at the first match, so the order of the conditions matters.GROUP BY 1groups by the first selected column, the bucket.SUM(SUM(outstanding)) OVER ()sums the bucket totals across all rows, giving the total book without a second query.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 wholeCASEexpression in theGROUP BYclause. - Rounding and integer division differ between engines. Multiplying by
100.0rather than100forces 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
Related articles
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