percentile_cont(): Many Percentiles in One Query

Median, 90th, 95th, and 99th percentile — computed together in a single pass with an ordered-set aggregate.

Run it yourself — all queries on this page use the git commits dataset, pre-loaded in the free TAOP lab. Start a session with psql taop and follow along. No setup beyond Docker.

Need median, 90th percentile, 95th, and 99th? PostgreSQL computes them all in one query with percentile_cont(), an ordered-set aggregate.

The problem

Calculate commit-time percentiles (median, 90th, 95th, 99th) for each project — here, the time between a commit being authored (ats) and committed (cts).

Step 1 — a basic average

The average is easy, but on a skewed distribution it hides the story:

select project, avg(cts-ats) as avg_time
  from commits
 where ats <> cts
 group by project;
 project  │                avg_time
══════════╪═════════════════════════════════════════
 postgres │ @ 1 day 10 hours 36 mins 56.743525 secs

Step 2 — the median

percentile_cont(0.5) is an ordered-set aggregate: it needs the rows sorted, which you give it with WITHIN GROUP (ORDER BY …):

select project,
       percentile_cont(0.5) within group(order by cts-ats) as median
  from commits
 where ats <> cts
 group by project;
 project  │      median
══════════╪══════════════════
 postgres │ @ 3 mins 46 secs

The median is three minutes where the average was a day and a half — that gap is exactly why you want percentiles.

Step 3 — many percentiles at once

Pass an array and get an array back — one pass over the sorted data:

select project,
       percentile_cont(array[0.5, 0.9, 0.95, 0.99])
          within group(order by cts-ats) as percentiles
  from commits
 where ats <> cts
 group by project;
 project  │                                       percentiles
══════════╪══════════════════════════════════════════════════════════════════════════════════
 postgres │ {"@ 3 mins 46 secs","@ 3 hours 21 mins 57 secs",
           │  "@ 2 days 22 hours 57 mins 3 secs","@ 34 days 20 hours 53 mins 12.28 secs"}
(1 row)

Step 4 — name the columns

Index into the array to present each percentile as its own column:

with perc_arrays as (
   select project,
          avg(cts-ats) as average,
          percentile_cont(array[0.5, 0.9, 0.95, 0.99])
             within group(order by cts-ats) as parr
     from commits
    where ats <> cts
 group by project
)
select project, average,
       parr[1] as median,
       parr[2] as "%90th",
       parr[3] as "%95th",
       parr[4] as "%99th"
  from perc_arrays;
project │ postgres
average │ @ 1 day 10 hours 36 mins 56.743525 secs
median  │ @ 3 mins 46 secs
%90th   │ @ 3 hours 21 mins 57 secs
%95th   │ @ 2 days 22 hours 57 mins 3 secs
%99th   │ @ 34 days 20 hours 53 mins 12.28 secs

One scan, four percentiles, plus the average for contrast — the kind of summary that turns a vague “it’s usually fast” into numbers you can act on.

Keep going

The Art of PostgreSQL

Liked this? It's one piece of a complete, practical system for writing real PostgreSQL. The book is your entry point.

Get the Book — $89