Skip to content
/ Chapter 20 · Statistics and Distributions
Lesson 20.1 · website_analytics
Lesson 20.1

Medians and Percentiles in SQL

An average can be dragged by one extreme row. The median is the middle row, and a percentile is any point along the sorted values.

"How long does a typical session last?" sounds like a job for avg(). But one visitor who leaves a tab open for ten minutes moves the average for everyone. The median sorts the values and takes the middle one, so a single extreme row barely moves it.

Pattern
select median(duration_seconds) as median_seconds,
       quantile_cont(duration_seconds, 0.25) as p25_seconds,
       quantile_cont(duration_seconds, 0.75) as p75_seconds,
       quantile_cont(duration_seconds, 0.9) as p90_seconds
from sessions

Median, quartiles, and the 90th percentile

A percentile is a point along the sorted values. The 25th percentile has a quarter of the rows below it, the median is the 50th, and the 90th leaves only the top tenth above it. median(x) is shorthand for quantile_cont(x, 0.5). Reports on response times and session lengths often quote the 90th or 95th percentile, because it describes the slow end without being decided by the single slowest row.

Continuous or discrete

When the point you ask for falls between two rows, quantile_cont interpolates between them, so it can return a value no row has. quantile_disc returns an actual row's value instead:

Paid sessions, both ways
select quantile_cont(duration_seconds, 0.9) as p90_cont,
       quantile_disc(duration_seconds, 0.9) as p90_disc
from sessions
where channel = 'paid'

The six paid sessions run from 81 to 600 seconds. The continuous 90th percentile is 544, part of the way from 488 to 600; the discrete one is 600, a session that happened. Use the discrete version when the answer has to be a real value, such as a price someone paid.

In other databases

median and quantile_cont are DuckDB names. PostgreSQL spells a percentile as an ordered-set aggregate, and DuckDB accepts that form too:

Portable form (PostgreSQL, DuckDB)
select percentile_cont(0.5) within group (order by duration_seconds) as median_seconds
from sessions

percentile_disc is the discrete twin. MySQL has neither, and SQL Server's PERCENTILE_CONT is a window function that needs an OVER clause.

Schema · Website Analytics Table · sessions 7 columns · 30 rows
Table · sessions

One row per visit. Anonymous visits have a null user_id; organic and direct visits have a null campaign_id.

7 columns · 30 rows
session_id int user_id int session_date date channel text campaign_id int device text duration_seconds int
Your task

For each channel, return channel, sessions (the number of sessions), avg_seconds (average duration, rounded to one decimal), median_seconds (median duration), and p90_seconds (the continuous 90th percentile of duration, rounded to one decimal). Sort by median_seconds from highest to lowest.

·

Run your query to inspect the result.

Expected answer
  • Columns: channel, sessions, avg_seconds, median_seconds, p90_seconds.
  • Rows: one per channel, 5 in all, email first.
  • Paid's average is 308.2 seconds but its median is only 256: one 600-second session pulls the average up.

Look at paid and social in your result. Their averages are 50 seconds above their medians, which is the shape of data with a long tail of long sessions. When the average and the median disagree that much, report the median.