SLA breaches by priority
Every ticket priority has a resolve target in slas.resolve_hours. Support leads want to know how
often each priority misses it.
Only look at closed tickets (closed_at is not NULL). Open tickets haven’t finished yet, so they
can’t have breached the resolve target in this report.
A closed ticket breached if it took longer than the target: closed_at is strictly later than
opened_at + resolve_hours. Closing exactly on the deadline is not a breach.
Return one row per priority that has at least one closed ticket:
| column | type | meaning |
|---|---|---|
priority |
text | the ticket priority |
closed_tickets |
integer | closed tickets with that priority |
breached |
integer | how many of those breached (0 if none) |
breach_pct |
decimal | breached / closed_tickets * 100, rounded to 1 decimal |
A priority with zero breaches still gets a row, with breached = 0.
Row order doesn’t matter.
Tables: tickets, slas.
helpdesk
A support desk. Synthetic data, generated by scripts/gen-datasets.mjs.
Click a table to preview its first 10 rows.
agents 20 rows
20 agents in 3 teams. Agents 1–3 are managers (manager_id NULL); everyone else reports to one of them.
tickets 800 rows
800 tickets from 2025-01-01. About 8% are still open (closed_at NULL).
ticket_events 2,779 rows
opened, first_response, reassigned, customer_reply, closed.
slas 4 rows
response and resolve targets in hours, per priority.
Hints get more specific as you go. Reveal one, try again, then come back.
The target lives in slas, keyed by priority. Join it onto tickets so each ticket row carries its
own resolve_hours.
Turn hours into something you can add to a timestamp: s.resolve_hours * INTERVAL '1 hour'. Then a
breach is t.closed_at > t.opened_at + s.resolve_hours * INTERVAL '1 hour'.
Filter to closed tickets in WHERE, but don’t put the breach test there. If you do, a priority with
no breaches disappears and closed_tickets only counts breaches. Count breaches with
COUNT(*) FILTER (WHERE ...) instead.
Try one more hint first? The solution is always here, but working it out is what makes it stick.
Approach
Join each ticket to its SLA row, keep closed tickets, group by priority, and count breaches with a filtered aggregate.
SELECT t.priority,
COUNT(*) AS closed_tickets,
COUNT(*) FILTER (WHERE t.closed_at > t.opened_at + s.resolve_hours * INTERVAL '1 hour') AS breached,
ROUND(100.0 * COUNT(*) FILTER (WHERE t.closed_at > t.opened_at + s.resolve_hours * INTERVAL '1 hour')
/ COUNT(*), 1) AS breach_pct
FROM tickets t
JOIN slas s ON s.priority = t.priority
WHERE t.closed_at IS NOT NULL
GROUP BY t.priority;
Why it works
There are two filters here, and they belong in different places. “Closed tickets only” decides which
rows exist, so it goes in WHERE. “Breached” is one count among several, so it goes in a FILTER
on that one aggregate. Put it in WHERE and every priority with zero breaches drops out of the
result.
The comparison is >, not >=, because closing exactly on the deadline meets the target.
Multiplying an integer by INTERVAL '1 hour' gives an interval, so you compare timestamps directly
and don’t round anything to whole hours.
Under the hood
slas has four rows. DuckDB builds its hash table on the small side of the join (the four SLA rows)
and streams the 800 tickets past it. Each ticket probes once, picks up its resolve_hours, and goes
straight into the aggregation hash table keyed by priority. The interval arithmetic is a
per-row add on 64-bit microsecond counts, which is why timestamps compare so cheaply.
Per-problem discussion threads are coming. Until then, questions and better solutions are welcome on LinkedIn or by email at hello@builtfrombits.com.
Press ⌘ ↵ or Run to check the visible tests. Submit runs the hidden ones too. Everything runs in your browser.