SQLLab
Все статьи

А/Б тестирование на SQL: считаем статистическую значимость

А/Б тестирование на SQL: сравнение конверсий, z-тест для пропорций, p-value в PostgreSQL. Как проверить результаты эксперимента без Python.

28 марта 2026 г.·4 мин чтения·

Результаты А/Б теста принято анализировать в Python или R. Но если данные уже в PostgreSQL, SQL справится сам — включая z-тест, доверительные интервалы и расчёт нужного размера выборки.

Базовое сравнение конверсий

Предположим, есть таблица experiments:

-- experiments: user_id, variant ('control' | 'treatment'), converted (boolean), created_at

Первым делом — простое сравнение:

SELECT
    variant,
    COUNT(*)                                       AS users,
    SUM(converted::int)                            AS conversions,
    ROUND(100.0 * SUM(converted::int) / COUNT(*), 2) AS conversion_pct
FROM experiments
WHERE experiment_name = 'new_checkout_button'
  AND created_at BETWEEN '2026-03-01' AND '2026-03-31'
GROUP BY variant
ORDER BY variant;

Пример результата:

variantusersconversionsconversion_pct
control48213868.00%
treatment49344438.98%

Разница есть — +0.98%. Но случайна ли она?

Z-тест для пропорций

Формула z-статистики для двух пропорций:

z = (p1 - p2) / sqrt(p_pool * (1 - p_pool) * (1/n1 + 1/n2))

где p_pool = (conv1 + conv2) / (n1 + n2) — объединённая конверсия.

Реализуем в SQL:

WITH stats AS (
    SELECT
        SUM(CASE WHEN variant = 'control'   THEN 1 ELSE 0 END) AS n_control,
        SUM(CASE WHEN variant = 'treatment' THEN 1 ELSE 0 END) AS n_treatment,
        SUM(CASE WHEN variant = 'control'   AND converted THEN 1 ELSE 0 END) AS c_control,
        SUM(CASE WHEN variant = 'treatment' AND converted THEN 1 ELSE 0 END) AS c_treatment
    FROM experiments
    WHERE experiment_name = 'new_checkout_button'
),
rates AS (
    SELECT
        n_control,
        n_treatment,
        c_control,
        c_treatment,
        c_control::float  / n_control   AS p1,   -- конверсия контроля
        c_treatment::float / n_treatment AS p2,   -- конверсия лечения
        (c_control + c_treatment)::float / (n_control + n_treatment) AS p_pool
    FROM stats
)
SELECT
    ROUND(p1 * 100, 2)          AS control_pct,
    ROUND(p2 * 100, 2)          AS treatment_pct,
    ROUND((p2 - p1) * 100, 2)   AS lift_pct,
    ROUND(
        (p2 - p1) /
        SQRT(p_pool * (1 - p_pool) * (1.0/n_control + 1.0/n_treatment))
    , 4)                         AS z_stat
FROM rates;

Интерпретация z-статистики

| |z| | Примерный p-value | Вывод | |---|---|---| | < 1.65 | > 0.10 | Не значимо | | 1.65 — 1.96 | 0.05 — 0.10 | Слабый сигнал | | 1.96 — 2.58 | 0.01 — 0.05 | Значимо (p < 0.05) | | > 2.58 | < 0.01 | Высокозначимо (p < 0.01) |

Если |z| > 1.96 — можно отвергнуть нулевую гипотезу при уровне доверия 95%.

Доверительный интервал для конверсии

Для каждого варианта считаем 95%-ный доверительный интервал:

WITH stats AS (
    SELECT
        variant,
        COUNT(*)                          AS n,
        SUM(converted::int)               AS c,
        SUM(converted::int)::float / COUNT(*) AS p
    FROM experiments
    WHERE experiment_name = 'new_checkout_button'
    GROUP BY variant
)
SELECT
    variant,
    ROUND(p * 100, 2)                              AS conversion_pct,
    ROUND((p - 1.96 * SQRT(p * (1-p) / n)) * 100, 2) AS ci_lower,
    ROUND((p + 1.96 * SQRT(p * (1-p) / n)) * 100, 2) AS ci_upper
FROM stats;

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

Расчёт нужного размера выборки

Перед запуском теста важно понять: сколько пользователей нужно на каждый вариант? Формула для 95% доверия и 80% мощности:

WITH params AS (
    SELECT
        0.08  AS p_baseline,   -- текущая конверсия (8%)
        0.009 AS mde,          -- минимальный обнаруживаемый эффект (+0.9%)
        1.96  AS z_alpha,      -- z для α=0.05 (двусторонний)
        0.84  AS z_beta        -- z для β=0.20 (мощность 80%)
)
SELECT
    CEIL(
        2 * POWER(z_alpha + z_beta, 2)
          * p_baseline * (1 - p_baseline)
          / POWER(mde, 2)
    ) AS required_users_per_variant
FROM params;

Запустите этот запрос до теста — и вы будете знать, когда можно останавливаться.

Типичные ошибки в А/Б тестах

1. Peeking problem (подглядывание). Нельзя останавливать тест, как только результат стал «значимым» — это ложная значимость. Определите размер выборки заранее и не смотрите до его достижения.

2. Множественное тестирование. Тестируете 10 метрик одновременно? При α=0.05 в среднем одна из 20 будет «значима» случайно. Применяйте поправку Бонферрони: делите α на число тестов.

-- Вместо z > 1.96 (α = 0.05), при 5 метриках используйте z > 2.58 (α = 0.01)
WHERE ABS(z_stat) > 2.58

3. Сегментированная разбивка после факта. Нашли значимость только в сегменте «iOS + Москва»? Это data dredging — случайная находка, не результат.

4. Неравные условия между вариантами. Убедитесь, что трафик разделён равномерно, а оба варианта работают одновременно, а не последовательно.


Хотите оттачивать аналитические запросы на реальных задачах? Заходите в тренажёр SQLlab.

Похожие статьи

Попробуй на практике

Тренажёр с реальными задачами — бесплатно и без регистрации

Открыть тренажёр →