sqlpostgresqlaggregatepercentile

PERCENTILE_CONT in PostgreSQL: Median and Percentiles with WITHIN GROUP

Compute the median and p95 with a single PERCENTILE_CONT aggregate, interpolation included, compare it to PERCENTILE_DISC, and see why it beats AVG.

10 min di letturaReferencesql · postgresql · aggregate · percentile · analytics · clickhouse
Questo articolo è attualmente in russo — la traduzione in inglese è in corso.

Среднее значение не всегда честно описывает данные.

Например, у нас есть суммы заказов:

100
120
130
150
10000

Если посчитать среднее, большой заказ на 10000 сильно сдвинет результат вверх.

AVG = 2100

Но типичный заказ явно не выглядит как 2100. Большинство заказов находятся около 100-150.

В таких ситуациях лучше смотреть не только на AVG, а на перцентили:

  • медиана показывает середину распределения;
  • p95 показывает значение, ниже которого находится 95% наблюдений;
  • p99 показывает хвост распределения ещё строже.

В PostgreSQL для этого есть функция PERCENTILE_CONT.

Она помогает считать медиану, p95, p99 и любые другие перцентили прямо в SQL.

Что такое перцентиль простыми словами

Перцентиль отвечает на вопрос:

Ниже какого значения находится определённая доля данных?

Например, p95 по времени ответа API равен 800 ms.

Это значит:

95% запросов выполнились за 800 ms или быстрее.

А оставшиеся 5% были медленнее.

Медиана — это p50.

Если медиана чека равна 1200, это значит:

половина заказов была дешевле или равна примерно 1200, а половина — дороже или равна примерно 1200.

Перцентили особенно полезны там, где данные имеют длинный хвост:

  • время ответа API;
  • длительность выполнения задач;
  • суммы заказов;
  • зарплаты;
  • размер платежей;
  • количество событий на пользователя;
  • время доставки;
  • время обработки заявки.

В таких данных среднее может сильно врать из-за нескольких экстремальных значений.

Почему AVG иногда вводит в заблуждение

Возьмём простой пример.

Есть пять заказов:

amount
------
100
120
130
150
10000

Среднее:

(100 + 120 + 130 + 150 + 10000) / 5 = 2100

Но если мы скажем:

средний заказ — 2100

это будет странно. Почти все реальные заказы намного меньше.

Медиана для этих данных — 130.

100, 120, 130, 150, 10000
          ^
       медиана

Она намного лучше показывает типичный заказ.

Среднее отвечает на вопрос:

какое значение получится, если равномерно распределить сумму по всем строкам?

Медиана отвечает на другой вопрос:

где находится середина отсортированного набора?

Для данных с выбросами медиана часто полезнее.

Что делает PERCENTILE_CONT

PERCENTILE_CONT считает значение на нужной доле отсортированного набора.

Например:

SELECT
  PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY amount) AS median_amount
FROM orders;

Здесь:

PERCENTILE_CONT(0.5)

означает: взять 50-й перцентиль, то есть медиану.

А:

WITHIN GROUP (ORDER BY amount)

говорит PostgreSQL:

отсортируй значения amount и найди нужную точку внутри этого распределения.

Важно: сортировка для PERCENTILE_CONT пишется не обычным ORDER BY в конце запроса, а внутри WITHIN GROUP.

Базовый синтаксис

Общий вид:

PERCENTILE_CONT(fraction) WITHIN GROUP (ORDER BY expression)

Где:

  • fraction — доля от 0 до 1;
  • expression — значение, по которому строится распределение.

Примеры:

PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY amount)

медиана суммы заказа.

PERCENTILE_CONT(0.95) WITHIN GROUP (ORDER BY response_time_ms)

p95 времени ответа.

PERCENTILE_CONT(0.99) WITHIN GROUP (ORDER BY duration_seconds)

p99 длительности.

Частые значения:

0.5  -- медиана, p50
0.75 -- p75
0.9  -- p90
0.95 -- p95
0.99 -- p99

Пример: медиана суммы заказа

Допустим, есть таблица orders:

id | amount
---+--------
1  | 100
2  | 120
3  | 130
4  | 150
5  | 10000

Посчитаем среднее и медиану:

SELECT
  AVG(amount) AS avg_amount,
  PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY amount) AS median_amount
FROM orders;

Результат:

avg_amount | median_amount
-----------+--------------
2100       | 130

Среднее сильно выросло из-за заказа на 10000.

А медиана показала более типичное значение.

Именно поэтому в аналитике часто смотрят не только AVG, но и медиану.

Что означает WITHIN GROUP

WITHIN GROUP нужен для агрегатов, которым важен порядок значений.

Обычному SUM порядок не нужен.

Например:

SUM(amount)

даст один и тот же результат независимо от того, как отсортированы строки.

А вот перцентиль без сортировки не имеет смысла.

Чтобы найти медиану или p95, PostgreSQL должен знать порядок значений:

100, 120, 130, 150, 10000

Именно поэтому PERCENTILE_CONT пишется так:

PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY amount)

То есть:

найди 0.5-позицию внутри группы, отсортированной по amount.

Обычный ORDER BY в конце запроса сортирует итоговые строки результата.

А ORDER BY внутри WITHIN GROUP определяет распределение, по которому считается перцентиль.

Это разные вещи.

PERCENTILE_CONT — это агрегат

PERCENTILE_CONT работает как агрегатная функция.

То есть он возвращает:

  • одно значение по всей таблице;
  • или одно значение на каждую группу, если есть GROUP BY.

Например, один p95 по всей таблице:

SELECT
  PERCENTILE_CONT(0.95) WITHIN GROUP (ORDER BY amount) AS p95_amount
FROM orders;

А p95 по каждой стране:

SELECT
  u.country,
  PERCENTILE_CONT(0.95) WITHIN GROUP (ORDER BY o.amount) AS p95_amount
FROM orders o
JOIN users u ON u.id = o.user_id
GROUP BY u.country
ORDER BY p95_amount DESC;

В этом запросе PostgreSQL отдельно сортирует суммы заказов внутри каждой страны и считает p95 для каждой группы.

NULL игнорируются

PERCENTILE_CONT игнорирует NULL.

Например, если в данных есть значения:

100
120
NULL
130
150

перцентиль будет считаться только по ненулевым значениям:

100
120
130
150

Это похоже на поведение многих агрегатов в SQL: AVG, SUM, MIN, MAX тоже не учитывают NULL.

Если в группе все значения NULL, результатом будет NULL.

Например:

SELECT
  PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY amount) AS median_amount
FROM orders
WHERE status = 'cancelled';

Если у отменённых заказов нет заполненных amount, медиана будет NULL.

Для отчёта иногда можно обернуть результат в COALESCE, но делать это нужно осознанно. Нулевая медиана и неизвестная медиана — не одно и то же.

CONT означает continuous

В названии PERCENTILE_CONT часть CONT означает continuous, то есть непрерывный.

Это значит, что функция может вернуть значение, которого не было в таблице.

Почему?

Потому что если нужная точка попадает между двумя строками, PostgreSQL интерполирует значение между ними.

Проще всего понять это на медиане.

Допустим, есть четыре суммы:

10
20
30
40

У набора чётное количество значений. У него нет одной центральной строки.

Центр находится между 20 и 30.

PERCENTILE_CONT(0.5) вернёт:

25

Запрос:

SELECT
  PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY amount) AS median_amount
FROM orders;

Результат:

median_amount
-------------
25

Значения 25 не было в таблице. Оно получилось как середина между 20 и 30.

Это и есть интерполяция.

PERCENTILE_CONT против PERCENTILE_DISC

В PostgreSQL есть ещё одна похожая функция:

PERCENTILE_DISC

DISC означает discrete, то есть дискретный.

Главное отличие:

  • PERCENTILE_CONT может вернуть промежуточное значение;
  • PERCENTILE_DISC всегда возвращает одно из реальных значений из набора.

Возьмём данные:

10
20
30
40

Сравним:

SELECT
  PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY amount) AS cont_median,
  PERCENTILE_DISC(0.5) WITHIN GROUP (ORDER BY amount) AS disc_median
FROM orders;

Результат может быть таким:

cont_median | disc_median
------------+------------
25          | 20

PERCENTILE_CONT вернул 25, потому что интерполировал между 20 и 30.

PERCENTILE_DISC вернул реальное значение из набора.

Когда использовать CONT, а когда DISC

PERCENTILE_CONT хорошо подходит для непрерывных величин:

  • деньги;
  • время ответа;
  • длительность;
  • зарплата;
  • вес;
  • расстояние;
  • размер файла;
  • количество миллисекунд;
  • числовые метрики.

Например:

SELECT
  PERCENTILE_CONT(0.95) WITHIN GROUP (ORDER BY response_time_ms) AS p95_latency
FROM api_requests;

PERCENTILE_DISC лучше использовать, когда результат должен быть существующим значением из набора.

Например:

  • категория;
  • уровень тарифа;
  • оценка;
  • дискретная шкала;
  • конкретное значение, которое реально встречалось в строках.

Пример:

SELECT
  PERCENTILE_DISC(0.5) WITHIN GROUP (ORDER BY rating) AS median_rating
FROM reviews;

Если рейтинг может быть только 1, 2, 3, 4, 5, дискретный вариант часто логичнее.

Коротко:

PERCENTILE_CONT -- можно интерполировать
PERCENTILE_DISC -- нужно реальное значение из набора

Пример: p95 времени ответа API

Перцентили особенно часто используют для latency.

Допустим, есть таблица api_requests:

id | endpoint       | response_time_ms
---+----------------+-----------------
1  | /login         | 120
2  | /login         | 140
3  | /login         | 180
4  | /login         | 900
5  | /profile       | 80
6  | /profile       | 110

Посчитаем p95 по каждому endpoint:

SELECT
  endpoint,
  PERCENTILE_CONT(0.95) WITHIN GROUP (ORDER BY response_time_ms) AS p95_latency_ms,
  AVG(response_time_ms) AS avg_latency_ms
FROM api_requests
GROUP BY endpoint
ORDER BY p95_latency_ms DESC;

Такой отчёт показывает не только среднее, но и хвост задержек.

Почему p95 важен?

Потому что пользовательский опыт часто портят именно медленные запросы.

Среднее может выглядеть нормально, но если p95 высокий, значит заметная часть пользователей сталкивается с задержками.

Например:

avg_latency_ms = 150
p95_latency_ms = 900

Это сигнал: большинство запросов быстрые, но хвост очень плохой.

p95, p99 и SLO

В инженерных метриках часто смотрят p95 и p99.

Например:

p95 latency = 800 ms

Это значит:

95% запросов быстрее или равны примерно 800 ms.

p99 latency = 3000 ms

Это значит:

99% запросов быстрее или равны примерно 3000 ms.

Если p99 намного больше p95, значит есть редкие, но очень тяжёлые выбросы.

Для SLO и мониторинга это важнее, чем просто среднее.

Например, среднее время ответа может быть 200 ms, но p99 — 5 s. В таком случае часть пользователей всё равно получает плохой опыт.

Несколько перцентилей одним запросом

Для дашборда часто нужны сразу p50, p95 и p99.

Можно написать три отдельных вызова:

SELECT
  PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY amount) AS p50,
  PERCENTILE_CONT(0.95) WITHIN GROUP (ORDER BY amount) AS p95,
  PERCENTILE_CONT(0.99) WITHIN GROUP (ORDER BY amount) AS p99
FROM orders
WHERE status = 'paid';

Это понятно, но PostgreSQL также позволяет передать массив долей:

SELECT
  PERCENTILE_CONT(ARRAY[0.5, 0.95, 0.99])
    WITHIN GROUP (ORDER BY amount) AS percentiles
FROM orders
WHERE status = 'paid';

Результат будет массивом:

percentiles
-----------------
{130,8000,9800}

Здесь:

первый элемент  -- p50
второй элемент  -- p95
третий элемент  -- p99

Важно не перепутать порядок. Результаты идут в том же порядке, что и доли в массиве.

Как разложить массив перцентилей по колонкам

Массив удобно считать, но в отчёте часто нужны отдельные колонки.

Можно сделать так:

SELECT
  pct[1] AS p50,
  pct[2] AS p95,
  pct[3] AS p99
FROM (
  SELECT
    PERCENTILE_CONT(ARRAY[0.5, 0.95, 0.99])
      WITHIN GROUP (ORDER BY amount) AS pct
  FROM orders
  WHERE status = 'paid'
) s;

Результат:

p50 | p95  | p99
----+------+-----
130 | 8000 | 9800

Здесь pct[1] — первый элемент массива.

В PostgreSQL массивы индексируются с 1, а не с 0.

Можно запомнить:

ARRAY[0.5, 0.95, 0.99]
     |     |     |
    pct[1] pct[2] pct[3]

Медиана по группам

PERCENTILE_CONT хорошо работает вместе с GROUP BY.

Например, посчитаем медианную зарплату по отделам.

SELECT
  dept,
  PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY salary) AS median_salary,
  AVG(salary) AS avg_salary
FROM employees
GROUP BY dept
ORDER BY median_salary DESC;

Результат может быть таким:

dept | median_salary | avg_salary
-----+---------------+-----------
eng  | 180000        | 210000
hr   | 120000        | 125000
sales| 150000        | 300000

Если в отделе sales есть один очень большой бонус или контракт, среднее может сильно вырасти.

Медиана при этом покажет более типичную зарплату.

Для зарплат, чеков и latency медиана часто полезнее среднего.

p95 суммы заказа по странам

Пример для продуктовой аналитики.

Хотим понять, в каких странах высокий хвост по суммам заказов.

SELECT
  u.country,
  PERCENTILE_CONT(0.95) WITHIN GROUP (ORDER BY o.amount) AS p95_amount,
  PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY o.amount) AS median_amount,
  AVG(o.amount) AS avg_amount
FROM orders o
JOIN users u ON u.id = o.user_id
WHERE o.status = 'paid'
GROUP BY u.country
ORDER BY p95_amount DESC;

Такой отчёт показывает:

  • типичный чек через медиану;
  • верхний хвост через p95;
  • среднее для сравнения.

Если в стране медиана 1000, а p95 20000, значит большинство заказов обычные, но есть дорогой хвост.

Это может быть важно для:

  • антифрода;
  • премиум-сегмента;
  • анализа крупных заказов;
  • настройки лимитов;
  • сегментации клиентов.

PERCENTILE_CONT с FILTER

PERCENTILE_CONT можно сочетать с FILTER, как и другие агрегаты.

Например, посчитать медиану только по оплаченным заказам и p95 только по возвратам в одном запросе:

SELECT
  PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY amount)
    FILTER (WHERE status = 'paid') AS median_paid_amount,
  PERCENTILE_CONT(0.95) WITHIN GROUP (ORDER BY amount)
    FILTER (WHERE status = 'refunded') AS p95_refunded_amount
FROM orders;

Это удобно, когда нужно несколько метрик по разным сегментам из одной таблицы.

Но не стоит превращать один SELECT в огромную простыню из десятков перцентилей. Если метрик становится слишком много, лучше подумать о витрине или отдельной структуре отчёта.

Важная техническая деталь про типы

В PostgreSQL PERCENTILE_CONT работает с числовыми значениями, которые можно интерполировать, а также с интервалами.

На практике для денежных значений и других numeric-колонок иногда полезно явно привести выражение к double precision.

Например:

SELECT
  PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY amount::double precision) AS median_amount
FROM orders;

Почему так?

Потому что PERCENTILE_CONT возвращает интерполированное значение. В PostgreSQL результат для числового варианта обычно получается как double precision.

Если вам нужно округление до денег, лучше сделать это явно:

SELECT
  ROUND(
    PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY amount::double precision)::numeric,
    2
  ) AS median_amount
FROM orders;

Так вы явно контролируете формат результата.

Для interval можно считать перцентили длительности:

SELECT
  PERCENTILE_CONT(0.95) WITHIN GROUP (ORDER BY finished_at - started_at) AS p95_duration
FROM jobs
WHERE finished_at IS NOT NULL;

Здесь результатом будет интервал.

Почему PERCENTILE_CONT не пишут как обычный ORDER BY

Новички иногда пытаются написать что-то вроде:

SELECT
  PERCENTILE_CONT(0.5) AS median_amount
FROM orders
ORDER BY amount;

Так нельзя.

PERCENTILE_CONT должен знать, по какому выражению строить распределение. Поэтому сортировка указывается внутри WITHIN GROUP:

SELECT
  PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY amount) AS median_amount
FROM orders;

Обычный ORDER BY в конце запроса нужен для сортировки строк результата.

Например:

SELECT
  u.country,
  PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY o.amount) AS median_amount
FROM orders o
JOIN users u ON u.id = o.user_id
GROUP BY u.country
ORDER BY median_amount DESC;

Здесь:

WITHIN GROUP (ORDER BY o.amount)

нужно для расчёта медианы внутри каждой страны.

А:

ORDER BY median_amount DESC

сортирует итоговые страны по медиане.

PERCENTILE_CONT и оконные функции

PERCENTILE_CONT — это ordered-set агрегат.

В обычной аналитике его чаще используют как агрегат:

SELECT
  country,
  PERCENTILE_CONT(0.95) WITHIN GROUP (ORDER BY amount) AS p95_amount
FROM orders
GROUP BY country;

Если вам нужно получить p95 по группе и показать его рядом с каждой исходной строкой, часто проще посчитать перцентиль в подзапросе, а потом присоединить обратно.

Например:

WITH country_percentiles AS (
  SELECT
    u.country,
    PERCENTILE_CONT(0.95) WITHIN GROUP (ORDER BY o.amount) AS p95_amount
  FROM orders o
  JOIN users u ON u.id = o.user_id
  GROUP BY u.country
)
SELECT
  o.id,
  u.country,
  o.amount,
  cp.p95_amount
FROM orders o
JOIN users u ON u.id = o.user_id
JOIN country_percentiles cp ON cp.country = u.country;

Так каждая строка заказа получит p95 своей страны.

Если же вам нужна позиция конкретной строки внутри распределения, смотрите в сторону оконных функций вроде:

PERCENT_RANK()
CUME_DIST()
NTILE()

Они отвечают на другие вопросы: не «какое значение у p95», а «где эта строка находится внутри отсортированного набора».

PERCENTILE_CONT и производительность

Чтобы посчитать перцентиль, базе нужно отсортировать значения.

Это дороже, чем простой AVG.

Например:

AVG(amount)

можно считать потоком: база просто суммирует значения и считает количество.

А для:

PERCENTILE_CONT(0.95) WITHIN GROUP (ORDER BY amount)

нужно понимать порядок значений.

На маленьких таблицах это обычно не проблема.

На больших таблицах перцентили могут быть дорогими, особенно если:

  • много групп;
  • много строк в каждой группе;
  • считается много разных перцентилей;
  • запрос работает по большой сырой таблице;
  • нет предварительной агрегации или партиционирования.

Практические советы:

  • фильтруйте лишние строки до расчёта;
  • считайте несколько перцентилей массивом, если нужны p50, p95 и p99;
  • проверяйте план через EXPLAIN ANALYZE;
  • для регулярных дашбордов подумайте о витрине;
  • для мониторинга latency на огромных потоках иногда используют приближённые алгоритмы в специализированных системах.

PERCENTILE_CONT точный и удобный, но точность может стоить ресурсов.

MySQL: прямого PERCENTILE_CONT обычно нет

В MySQL нет такого же удобного стандартного PERCENTILE_CONT как в PostgreSQL.

Медиану и перцентили там часто считают через оконные функции:

  • ROW_NUMBER();
  • PERCENT_RANK();
  • CUME_DIST();
  • ручной расчёт позиции.

Для медианы это может выглядеть заметно длиннее, чем в PostgreSQL.

И главное: встроенной удобной интерполяции как у PERCENTILE_CONT обычно нет, поэтому запросы приходится писать вручную и внимательно проверять.

Если вы пишете переносимый SQL, не рассчитывайте, что синтаксис PostgreSQL с WITHIN GROUP просто заработает в MySQL.

ClickHouse: quantile и median

В ClickHouse для похожих задач используют семейство функций quantile.

Например, p95:

SELECT
  quantile(0.95)(amount) AS p95_amount
FROM orders
WHERE status = 'paid';

Точный вариант:

SELECT
  quantileExact(0.95)(amount) AS p95_amount
FROM orders
WHERE status = 'paid';

Медиана:

SELECT
  median(amount) AS median_amount
FROM orders
WHERE status = 'paid';

В ClickHouse синтаксис другой: нет WITHIN GROUP, а уровень точности зависит от выбранной функции.

Для больших аналитических данных это нормально: ClickHouse часто использует быстрые агрегаты и приближённые варианты, если точность до последнего значения не нужна.

Практические шаблоны

Медиана суммы заказа

SELECT
  PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY amount::double precision) AS median_amount
FROM orders;

p95 суммы заказа

SELECT
  PERCENTILE_CONT(0.95) WITHIN GROUP (ORDER BY amount::double precision) AS p95_amount
FROM orders;

p50, p95 и p99 одним массивом

SELECT
  PERCENTILE_CONT(ARRAY[0.5, 0.95, 0.99])
    WITHIN GROUP (ORDER BY amount::double precision) AS percentiles
FROM orders
WHERE status = 'paid';

Разложить массив по колонкам

SELECT
  pct[1] AS p50,
  pct[2] AS p95,
  pct[3] AS p99
FROM (
  SELECT
    PERCENTILE_CONT(ARRAY[0.5, 0.95, 0.99])
      WITHIN GROUP (ORDER BY amount::double precision) AS pct
  FROM orders
  WHERE status = 'paid'
) s;

Медиана и среднее по отделам

SELECT
  dept,
  PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY salary::double precision) AS median_salary,
  AVG(salary) AS avg_salary
FROM employees
GROUP BY dept
ORDER BY median_salary DESC;

p95 latency по endpoint

SELECT
  endpoint,
  PERCENTILE_CONT(0.95) WITHIN GROUP (ORDER BY response_time_ms::double precision) AS p95_latency_ms,
  AVG(response_time_ms) AS avg_latency_ms
FROM api_requests
GROUP BY endpoint
ORDER BY p95_latency_ms DESC;

p95 длительности через interval

SELECT
  PERCENTILE_CONT(0.95) WITHIN GROUP (ORDER BY finished_at - started_at) AS p95_duration
FROM jobs
WHERE finished_at IS NOT NULL;

Перцентиль только по сегменту через FILTER

SELECT
  PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY amount::double precision)
    FILTER (WHERE status = 'paid') AS median_paid_amount,
  PERCENTILE_CONT(0.95) WITHIN GROUP (ORDER BY amount::double precision)
    FILTER (WHERE status = 'paid') AS p95_paid_amount
FROM orders;

Дискретная медиана

SELECT
  PERCENTILE_DISC(0.5) WITHIN GROUP (ORDER BY rating) AS median_rating
FROM reviews;

Что важно запомнить

PERCENTILE_CONT считает перцентиль по отсортированному набору значений.

Пример:

SELECT
  PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY amount) AS median_amount
FROM orders;

Главные правила:

  • 0.5 — медиана;
  • 0.95 — p95;
  • 0.99 — p99;
  • сортировка пишется внутри WITHIN GROUP;
  • PERCENTILE_CONT может вернуть значение, которого не было в таблице;
  • это происходит из-за интерполяции;
  • NULL игнорируются;
  • с GROUP BY перцентиль считается отдельно по каждой группе;
  • массив долей позволяет получить несколько перцентилей сразу;
  • PERCENTILE_DISC возвращает реальное значение из набора;
  • AVG чувствителен к выбросам, а медиана и p95 лучше описывают центр и хвост распределения.

Короткий вывод

PERCENTILE_CONT нужен, когда среднего уже недостаточно.

Если данные имеют выбросы и длинный хвост, AVG может показать красивое, но не очень честное число.

Для типичного значения лучше смотреть медиану:

SELECT
  PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY amount) AS median_amount
FROM orders;

Для хвоста распределения — p95 или p99:

SELECT
  PERCENTILE_CONT(0.95) WITHIN GROUP (ORDER BY response_time_ms) AS p95_latency
FROM api_requests;

Главная мысль:

AVG показывает среднее, медиана показывает типичную середину, p95 и p99 показывают хвост, который часто важнее для пользователей.

В PostgreSQL PERCENTILE_CONT ... WITHIN GROUP даёт прямой и понятный способ считать эти метрики в SQL.

Esercitati su esercizi reali

Risolvi esercizi nel trainer SQL con valutazione e suggerimenti istantanei.

Apri il trainer