Как посчитать процент в 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 раз.

Связанные темы

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+ вопросами для собесов.