Recipe Tableau and SQL
How do I get the median and P90 for each group?
Use MEDIAN([x]) and PERCENTILE([x], 0.9) in Tableau, or MEDIAN(x) and QUANTILE_CONT(x, 0.9) in SQL, grouped by your category. On skewed data the mean can rank the groups backwards: here Enterprise support tickets have the slowest average, 55.6 hours, and the fastest median, 4.1 hours.
The data
Pennant support tickets, January to June 2025
Support for a project-management app: 1,600 tickets across four plans. Enterprise gets a priority queue, except for the bug escalations that wait on engineering.
ticket_id | opened_date | plan | channel | topic | hours_to_resolve |
|---|---|---|---|---|---|
| PN-31000 | 2025-01-01 | Free | Chat | Access | 64.9 |
| PN-31001 | 2025-01-01 | Free | Chat | Bug | 21.6 |
| PN-31002 | 2025-01-01 | Starter | Chat | How-to | 6.5 |
| PN-31003 | 2025-01-01 | Starter | Chat | How-to | 13.1 |
Step 1
Average hours to resolve, by plan
By the average, Enterprise customers wait longest: more than two days per ticket. That is the number that ends up in a quarterly review. It is also misleading.
Tableau
AVG([hours_to_resolve])The xmetric engine ran this formula and got the SQL’s Hours to resolve column, row for row.
SQL
SELECT plan, AVG(hours_to_resolve) AS mean_hours
FROM input
GROUP BY plan
ORDER BY mean_hours DESCRows
| Plan | Hours to resolve |
|---|---|
| Enterprise | 55.58 |
| Free | 35.16 |
| Starter | 24.16 |
| Team | 16.07 |
Step 2
Now the median
The ranking flips. The typical Enterprise ticket closes in about four hours, faster than any other plan. A small group of bug escalations, each waiting weeks on engineering, drags the Enterprise mean up.
Tableau
MEDIAN([hours_to_resolve])The xmetric engine ran this formula and got the SQL’s Hours to resolve column, row for row.
SQL
SELECT plan, MEDIAN(hours_to_resolve) AS median_hours
FROM input
GROUP BY plan
ORDER BY median_hours DESCRows
| Plan | Hours to resolve |
|---|---|
| Free | 30.05 |
| Starter | 20.05 |
| Team | 11.55 |
| Enterprise | 4.1 |
Why does the mean say the opposite of the median?
Because 19 of the 160 Enterprise tickets are bug escalations that wait days or weeks on engineering. They are 12% of the tickets and 93% of the hours. The mean spreads those hours over every ticket, so Enterprise looks slowest (55.6 hours). The median ignores how long the long ones are, so it shows what a typical Enterprise customer gets: an answer in 4.1 hours.
Step 3
How many tickets beat their own plan's average?
On symmetric data, about half the rows sit below the mean (the reference line). Here 88% of Enterprise tickets close faster than the Enterprise average. When this share is far from half, the mean is describing almost nobody.
Tableau
AVG(IIF([hours_to_resolve] < { FIXED [plan] : AVG([hours_to_resolve]) }, 1, 0))The xmetric engine ran this formula and got the SQL’s Share below mean column, row for row.
SQL
WITH m AS (
SELECT plan, AVG(hours_to_resolve) AS mean_hours
FROM input
GROUP BY plan
)
SELECT
i.plan,
AVG(CASE WHEN i.hours_to_resolve < m.mean_hours THEN 1 ELSE 0 END) AS share_below_mean
FROM input i JOIN m USING (plan)
GROUP BY i.plan
ORDER BY share_below_mean DESCRows
| Plan | Share below mean |
|---|---|
| Enterprise | 88.1% |
| Team | 71.8% |
| Starter | 62.9% |
| Free | 62.4% |
Common mistake
Ranking groups by the mean on skewed data
The mean puts Enterprise first for slow support; the median puts it last. Both are right about different things: the mean counts the weeks lost to escalations, the median tells you what a typical customer gets. Report the median and a high percentile, not the mean alone.
Tableau
AVG([hours_to_resolve])
// sorted descendingSQL
SELECT plan, AVG(hours_to_resolve) AS hours
FROM input
GROUP BY plan
ORDER BY hours DESC
LIMIT 1Tableau
MEDIAN([hours_to_resolve])
// with PERCENTILE([hours_to_resolve], 0.9) beside itSQL
SELECT plan, MEDIAN(hours_to_resolve) AS hours
FROM input
GROUP BY plan
ORDER BY hours DESC
LIMIT 1Which percentile should I report?
The median for the typical case, and one high percentile for the tail your customers remember. Turn the slider.
Step 4
Pick a percentile
P90 is the time 90% of tickets beat. Slide the percentile and watch Enterprise: fastest at the median, still fastest at P85, then the slowest plan by far at P90, once the escalations enter the window.
Tableau
PERCENTILE([hours_to_resolve], 0.9)The xmetric engine ran this formula and got the SQL’s Percentile hours column, row for row.
SQL
SELECT
plan,
QUANTILE_CONT(hours_to_resolve, 0.9) AS percentile_hours
FROM input
GROUP BY plan
ORDER BY percentile_hours DESCRows
| Plan | Percentile hours |
|---|---|
| Enterprise | 29 |
| Free | 62 |
| Starter | 42.54 |
| Team | 28.99 |
At 90%, the slowest plan is Enterprise, at 29 hours. Between P85 and P90 Enterprise goes from fastest to slowest: a single percentile can hide a whole group of tickets, so show two.
Step 5
Put the whole spread in one table
Median for the typical ticket, P90 or P95 for the tail, and mean over median as a skew alarm: near 1 the mean is fine, above about 1.3 the tail is driving it. Enterprise sits above 13.
Tableau
MEDIAN([hours_to_resolve])
// and PERCENTILE([hours_to_resolve], 0.25), 0.75, 0.9, 0.95; AVG([hours_to_resolve]) / MEDIAN([hours_to_resolve])The xmetric engine ran this formula and got the SQL’s Hours to resolve column, row for row.
SQL
SELECT
plan,
COUNT(*) AS tickets,
AVG(hours_to_resolve) AS mean_hours,
QUANTILE_CONT(hours_to_resolve, 0.25) AS p25,
MEDIAN(hours_to_resolve) AS median_hours,
QUANTILE_CONT(hours_to_resolve, 0.75) AS p75,
QUANTILE_CONT(hours_to_resolve, 0.9) AS p90,
QUANTILE_CONT(hours_to_resolve, 0.95) AS p95,
AVG(hours_to_resolve)
/ MEDIAN(hours_to_resolve) AS mean_to_median
FROM input
GROUP BY plan
ORDER BY median_hoursRows
| Plan | Tickets | Hours to resolve | P25 | Hours to resolve | P75 | P90 | P95 | Hours to resolve |
|---|---|---|---|---|---|---|---|---|
| Enterprise | 160 | 55.58 | 2.6 | 4.1 | 6.32 | 29 | 45 | 13.56 |
| Team | 340 | 16.07 | 7 | 11.55 | 16.83 | 28.99 | 38.15 | 1.39 |
| Starter | 480 | 24.16 | 13.2 | 20.05 | 29.42 | 42.54 | 52.21 | 1.2 |
| Free | 620 | 35.16 | 21.28 | 30.05 | 43.6 | 62 | 75.98 | 1.17 |