Как посчитать процент в SQL
Карьерник — квиз-тренажёр в Telegram с 1500+ вопросами для собесов аналитика. SQL, Python, A/B, метрики. Бесплатно.
Базовая формула
percent = часть × 100 / целоеВ SQL нужно быть аккуратным с типами — integer / integer возвращает integer (обрубает до нуля).
1. Процент от общего
Сколько % заказов оплачены:
SELECT
100.0 * COUNT(CASE WHEN status = 'paid' THEN 1 END) / COUNT(*) AS paid_pct
FROM orders;Или через AVG:
SELECT
100 * AVG(CASE WHEN status = 'paid' THEN 1.0 ELSE 0 END) AS paid_pct
FROM orders;Важно: 1.0 (float) а не 1 (int), иначе деление будет integer.
2. Доля каждой категории
Процент заказов по категориям:
SELECT
category,
COUNT(*) AS orders,
100.0 * COUNT(*) / SUM(COUNT(*)) OVER () AS share_pct
FROM orders
GROUP BY category
ORDER BY share_pct DESC;Оконная функция SUM(COUNT(*)) OVER () даёт общее количество.
3. Конверсия воронки
WITH funnel AS (
SELECT
COUNT(DISTINCT CASE WHEN event_name = 'signup' THEN user_id END) AS signups,
COUNT(DISTINCT CASE WHEN event_name = 'activation' THEN user_id END) AS activated,
COUNT(DISTINCT CASE WHEN event_name = 'purchase' THEN user_id END) AS paid
FROM events
)
SELECT
signups,
activated,
paid,
100.0 * activated / NULLIF(signups, 0) AS cr_activation,
100.0 * paid / NULLIF(signups, 0) AS cr_purchase,
100.0 * paid / NULLIF(activated, 0) AS cr_activation_to_paid
FROM funnel;NULLIF(x, 0) защищает от деления на ноль.
4. Change week-over-week (WoW)
WITH weekly AS (
SELECT
DATE_TRUNC('week', created_at) AS week,
COUNT(*) AS orders
FROM orders
GROUP BY 1
)
SELECT
week,
orders,
LAG(orders) OVER (ORDER BY week) AS prev_week,
100.0 * (orders - LAG(orders) OVER (ORDER BY week))
/ NULLIF(LAG(orders) OVER (ORDER BY week), 0) AS wow_change_pct
FROM weekly
ORDER BY week;5. Month-over-month change
WITH monthly AS (
SELECT DATE_TRUNC('month', created_at) AS month, SUM(total) AS revenue
FROM orders GROUP BY 1
)
SELECT
month,
revenue,
LAG(revenue) OVER (ORDER BY month) AS prev_month,
100.0 * (revenue - LAG(revenue) OVER (ORDER BY month))
/ NULLIF(LAG(revenue) OVER (ORDER BY month), 0) AS mom_growth_pct
FROM monthly
ORDER BY month;6. Year-over-year (YoY)
Сравнить с тем же месяцем год назад:
WITH monthly AS (
SELECT DATE_TRUNC('month', created_at) AS month, SUM(total) AS revenue
FROM orders GROUP BY 1
)
SELECT
month,
revenue,
LAG(revenue, 12) OVER (ORDER BY month) AS prev_year_same_month,
100.0 * (revenue - LAG(revenue, 12) OVER (ORDER BY month))
/ NULLIF(LAG(revenue, 12) OVER (ORDER BY month), 0) AS yoy_pct
FROM monthly;7. Процент по когортам
SELECT
DATE_TRUNC('month', signup_at) AS cohort,
COUNT(*) AS signups,
COUNT(CASE WHEN has_paid THEN 1 END) AS buyers,
100.0 * COUNT(CASE WHEN has_paid THEN 1 END) / COUNT(*) AS conversion_pct
FROM users
GROUP BY 1
ORDER BY 1;8. Running percent (накопительный)
WITH daily AS (
SELECT DATE(created_at) AS day, COUNT(*) AS orders
FROM orders GROUP BY 1
),
cumulative AS (
SELECT
day,
orders,
SUM(orders) OVER (ORDER BY day) AS running_total
FROM daily
)
SELECT
day,
orders,
running_total,
100.0 * running_total / (SELECT SUM(orders) FROM daily) AS pct_of_total
FROM cumulative
ORDER BY day;9. Процент от группы
Доля каждого продукта в категории:
SELECT
category,
product_id,
revenue,
100.0 * revenue / SUM(revenue) OVER (PARTITION BY category) AS share_in_category
FROM products;10. Pareto 80/20
Сколько продуктов генерируют 80% выручки:
WITH ranked AS (
SELECT
product_id,
revenue,
SUM(revenue) OVER () AS total_revenue,
SUM(revenue) OVER (ORDER BY revenue DESC
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS running_revenue
FROM products
)
SELECT
COUNT(*) AS products_generating_80pct
FROM ranked
WHERE running_revenue / total_revenue <= 0.8;11. Форматирование процента в выводе
-- Postgres
SELECT ROUND(100.0 * x / y, 2) AS pct;
-- с символом %
SELECT ROUND(100.0 * x / y, 1) || '%' AS pct;
-- MySQL
SELECT CONCAT(ROUND(100 * x / y, 2), '%') AS pct;12. Условный процент (when > 0)
Коэффициент повторных покупок:
SELECT
100.0 * COUNT(CASE WHEN orders_count > 1 THEN 1 END) / COUNT(*) AS repeat_buyer_pct
FROM (
SELECT user_id, COUNT(*) AS orders_count
FROM orders
GROUP BY user_id
) t;Частые ошибки
Ошибка 1. Integer division
-- возвращает 0 (integer division)
SELECT 100 * 5 / 20;
-- возвращает 25.0 (float division)
SELECT 100.0 * 5 / 20;
-- или CAST
SELECT 100 * 5 / CAST(20 AS FLOAT);Всегда умножайте на 100.0 (с точкой) при подсчёте процентов.
Ошибка 2. Деление на ноль
-- если denominator = 0 — ошибка / NaN
x / y
-- безопаснее
x / NULLIF(y, 0) -- вернёт NULL, если y = 0Ошибка 3. COUNT vs COUNT(DISTINCT)
-- доля уникальных пользователей, совершивших покупку
COUNT(DISTINCT user_id)
-- доля строк — часто не то, что нужно
COUNT(*)Ошибка 4. Неверная группировка
Считаете % по когорте, а группируете по дню → общий процент.
Ошибка 5. Смешение с double counting
Один пользователь может совершить 5 покупок → в знаменателе он учитывается 5 раз.
Связанные темы
- Как посчитать конверсию в SQL
- Как посчитать воронку в SQL
- NULL в SQL — шпаргалка
- COALESCE — шпаргалка
FAQ
Почему 100 / 3 возвращает 33, а не 33.33?
Integer division. Используйте 100.0 / 3 или CAST к FLOAT.
Как показать 2 знака после запятой?
ROUND(value, 2). В MySQL — то же самое.
NULLIF или CASE для защиты от деления на 0?
NULLIF короче: x / NULLIF(y, 0). Работает везде.
Как получить отрицательный процент при падении?
(new - old) / old × 100 — положительный, если рост, отрицательный при падении. Формула одна для обоих направлений.
Тренируйте SQL — откройте тренажёр с 1500+ вопросами для собесов.