Conditional Running Totals — Resets, Caps, and Flags

Standard running totals accumulate forever. Real business problems often need running totals that reset at certain conditions, cap at a maximum value, or start counting only when a threshold is crossed. These patterns push window functions to their limits — and sometimes require combining them with recursive CTEs.


Running total that resets on a condition

Scenario: compute revenue per customer, but reset the running total at the start of each calendar year.

WITH order_totals AS (
    SELECT
        o.customer_id,
        o.created_at,
        DATE_PART('year', o.created_at)          AS year,
        SUM(oi.quantity * oi.unit_price)          AS order_total
    FROM orders o
    JOIN order_items oi ON oi.order_id = o.order_id
    WHERE o.status = 'completed'
    GROUP BY o.customer_id, o.created_at, DATE_PART('year', o.created_at)
)
SELECT
    customer_id,
    created_at::DATE,
    year,
    order_total,
    SUM(order_total) OVER (
        PARTITION BY customer_id, year     -- partition by BOTH customer AND year
        ORDER BY created_at
        ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
    ) AS ytd_spend
FROM order_totals
ORDER BY customer_id, created_at;

Adding year to PARTITION BY resets the running total at year boundaries. This is the simplest form of a conditional reset — the reset condition aligns with the partition boundary.


Running count of consecutive positive months

Scenario: count how many consecutive months in a row a customer has had at least one order — reset to 0 when they have a gap month.

This is a gap-and-island problem combined with a running aggregation:

WITH customer_months AS (
    SELECT
        customer_id,
        DATE_TRUNC('month', created_at) AS month,
        COUNT(DISTINCT order_id)         AS orders
    FROM orders
    WHERE status = 'completed'
    GROUP BY customer_id, DATE_TRUNC('month', created_at)
),
numbered AS (
    SELECT
        customer_id,
        month,
        orders,
        ROW_NUMBER() OVER (PARTITION BY customer_id ORDER BY month) AS rn
    FROM customer_months
),
with_group AS (
    SELECT
        customer_id,
        month,
        orders,
        month - (rn * INTERVAL '1 month') AS streak_group
    FROM numbered
),
streak_lengths AS (
    SELECT
        customer_id,
        streak_group,
        MIN(month) AS streak_start,
        MAX(month) AS streak_end,
        COUNT(*)   AS streak_months
    FROM with_group
    GROUP BY customer_id, streak_group
)
SELECT
    c.name,
    sl.streak_start,
    sl.streak_end,
    sl.streak_months
FROM streak_lengths sl
JOIN customers c ON c.customer_id = sl.customer_id
ORDER BY sl.streak_months DESC, c.name;

Purchase this course to unlock the full lesson.

Sign up