sqlpostgresqlaggregateboolean

EVERY в SQL: как проверить, что все строки группы прошли условие

EVERY проверяет, что булево условие истинно для всех не-NULL строк группы; разбираем NULL, BOOL_AND и эмуляцию в MySQL.

10 мин чтенияСправочникsql · postgresql · aggregate · boolean · clickhouse

Иногда в SQL нужно ответить не на вопрос «сколько строк?», а на вопрос «все ли строки хорошие?».

Например:

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

Такую проверку можно делать в приложении: забрать строки, пройтись циклом, поставить флаг. Но база данных умеет сделать это сама — аккуратно, быстро и прямо в запросе.

Для этого есть агрегат EVERY.

Он сворачивает много строк в один булев результат:

Если все проверенные строки проходят условие — вернуть true. Если хотя бы одна строка не проходит — вернуть false.

Базовая идея

EVERY — это агрегатная функция. Она работает по группе строк, как SUM, COUNT или AVG, только вместо чисел собирает булевы значения.

Внутрь EVERY передают условие:

SELECT
    dept,
    EVERY(salary > 0) AS all_paid
FROM employees
GROUP BY dept;

Такой запрос отвечает на вопрос:

Во всех ли строках отдела зарплата больше нуля?

Если в отделе у всех сотрудников salary > 0, результат будет true.

Если хотя бы у одного сотрудника salary <= 0, результат будет false.

То есть EVERY можно читать почти как обычную фразу:

EVERY(salary > 0)

каждая зарплата больше нуля

Именно поэтому функция хорошо подходит для проверок бизнес-правил.

EVERY похож на большое AND

Логику EVERY удобно представить как большое AND по всем строкам группы.

Допустим, в отделе три сотрудника:

salary > 0 -> true
salary > 0 -> true
salary > 0 -> true

Тогда:

true AND true AND true -> true

EVERY вернёт true.

А если хотя бы одна строка не прошла проверку:

salary > 0 -> true
salary > 0 -> false
salary > 0 -> true

Получается:

true AND false AND true -> false

EVERY вернёт false.

Это главное правило:

  • все значения true — результат true;
  • есть хотя бы одно false — результат false;
  • нет ни одного известного true или false — результат NULL.

Последний пункт особенно важен. В SQL есть не только true и false, но ещё и NULL, то есть неизвестное значение.

Первый пример: все ли платежи положительные

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

CREATE TABLE payments (
    id bigint,
    user_id bigint,
    amount numeric
);

Проверим, все ли платежи каждого пользователя положительные:

SELECT
    user_id,
    EVERY(amount > 0) AS all_positive
FROM payments
GROUP BY user_id;

Если у пользователя такие платежи:

100
250
50

Условие amount > 0 будет истинным для каждой строки, и результат будет:

true

Если данные такие:

100
-20
50

Одна строка нарушает правило, и результат будет:

false

Так одним запросом мы получаем аккуратный флаг качества по каждому пользователю.

Пример с заказами: все ли позиции отгружены

Очень понятный сценарий — заказ состоит из нескольких позиций. Нужно понять, можно ли считать заказ полностью отгруженным.

Пусть есть таблица order_items:

CREATE TABLE order_items (
    id bigint,
    order_id bigint,
    product_id bigint,
    status text
);

Статусы могут быть такими:

packed
shipped
cancelled

Проверим, все ли позиции заказа имеют статус shipped:

SELECT
    order_id,
    EVERY(status = 'shipped') AS fully_shipped,
    COUNT(*) AS items_count
FROM order_items
GROUP BY order_id;

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

order_id | fully_shipped | items_count
---------+---------------+------------
1001     | true          | 3
1002     | false         | 4
1003     | true          | 1

Читается просто:

  • заказ 1001 полностью отгружен;
  • заказ 1002 ещё нет;
  • заказ 1003 состоит из одной позиции, и она отгружена.

COUNT(*) рядом полезен для контекста. Флаг говорит «да» или «нет», а счётчик показывает, сколько строк вообще участвовало в проверке.

EVERY в HAVING

EVERY удобно использовать не только в SELECT, но и в HAVING.

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

SELECT
    user_id,
    EVERY(amount > 0) AS all_positive
FROM payments
GROUP BY user_id
HAVING EVERY(amount > 0);

HAVING фильтрует уже сгруппированные данные. Поэтому здесь логика такая:

  1. Сгруппировать платежи по пользователю.
  2. Проверить условие для каждой группы.
  3. Оставить только группы, где все строки прошли проверку.

Можно использовать это для отчётов, дашбордов и контроля данных.

Например, найти страны, где все заказы имеют положительную сумму:

SELECT
    u.country,
    EVERY(o.amount > 0) AS all_orders_positive
FROM users AS u
JOIN orders AS o
    ON o.user_id = u.id
GROUP BY u.country
HAVING EVERY(o.amount > 0);

Запрос читается почти как правило:

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

EVERY против COUNT

До знакомства с EVERY такую проверку часто пишут через счётчики.

Например:

SELECT
    order_id,
    COUNT(*) = COUNT(*) FILTER (WHERE status = 'shipped') AS fully_shipped
FROM order_items
GROUP BY order_id;

Это рабочий вариант. Он считает все строки и сравнивает их с количеством отгруженных строк.

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

  • сколько всего позиций;
  • сколько позиций со статусом shipped;
  • равны ли эти числа.

С EVERY правило видно сразу:

SELECT
    order_id,
    EVERY(status = 'shipped') AS fully_shipped
FROM order_items
GROUP BY order_id;

Если задача именно «все строки должны пройти условие», EVERY обычно выразительнее.

EVERY против BOOL_AND

В PostgreSQL есть две похожие функции:

EVERY(...)
BOOL_AND(...)

Для обычного использования они дают одинаковый результат.

SELECT EVERY(amount > 0) AS result
FROM payments;

И:

SELECT BOOL_AND(amount > 0) AS result
FROM payments;

Смысл один и тот же:

все ли значения истинны?

Разница больше в стиле.

EVERY ближе к стандартному SQL и хорошо читается как английское слово:

EVERY(status = 'shipped')

BOOL_AND явно показывает, что это булево AND по группе:

BOOL_AND(status = 'shipped')

Если рядом используется проверка «хотя бы одна строка прошла условие», часто удобно писать пару:

BOOL_AND(...)
BOOL_OR(...)

Например:

SELECT
    order_id,
    BOOL_AND(status = 'shipped') AS all_shipped,
    BOOL_OR(status = 'cancelled') AS has_cancelled
FROM order_items
GROUP BY order_id;

Так видно две разные проверки:

  • BOOL_AND — все строки подходят;
  • BOOL_OR — хотя бы одна строка подходит.

Для учебных и отчётных запросов EVERY выглядит очень понятно. Для системного SQL многие команды предпочитают BOOL_AND, потому что он сразу показывает операцию.

Как работает NULL

Главная ловушка EVERY — значения NULL.

SQL не считает NULL ни true, ни false. Это неизвестность.

Например:

SELECT NULL = 'shipped' AS result;

Результат будет не false, а NULL.

Почему? Потому что если статус неизвестен, база не может честно сказать, равен он shipped или нет.

Теперь посмотрим, что происходит внутри EVERY.

Пусть у заказа три позиции:

shipped
shipped
NULL

Условие status = 'shipped' даст:

true
true
NULL

EVERY игнорирует NULL-значения, как и многие другие агрегаты.

Значит, он увидит только:

true
true

И вернёт:

true

Вот здесь можно легко ошибиться.

Если NULL означает «статус пока неизвестен», такой результат может быть слишком оптимистичным. Отчёт скажет «всё отгружено», хотя одна позиция вообще без статуса.

Когда NULL нужно считать ошибкой

Если неизвестный статус должен проваливать проверку, нужно явно превратить NULL в плохое значение.

Например:

SELECT
    order_id,
    EVERY(COALESCE(status, 'pending') = 'shipped') AS fully_shipped
FROM order_items
GROUP BY order_id;

Здесь COALESCE(status, 'pending') говорит:

Если статус неизвестен, считай его pending.

Тогда строка с NULL уже не будет проигнорирована. Она даст false.

Можно написать ещё строже:

SELECT
    order_id,
    EVERY(status IS NOT NULL AND status = 'shipped') AS fully_shipped
FROM order_items
GROUP BY order_id;

Так правило читается очень явно:

статус должен быть известен и равен shipped.

Для проверок качества данных это часто лучший вариант.

Когда NULL можно игнорировать

Иногда NULL действительно означает «не применимо».

Например, есть таблица проверок документов. У некоторых документов поле verified_at заполнено, а у некоторых проверка не требуется.

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

SELECT
    client_id,
    EVERY(verified_at IS NOT NULL) FILTER (WHERE verification_required) AS all_required_verified
FROM documents
GROUP BY client_id;

Здесь проверяются только документы, для которых verification_required истинно.

Это важная мысль: судьбу NULL нужно решать не случайно, а по смыслу данных.

Если NULL — это ошибка или незавершённость, превращайте его в false.

Если NULL — это честное «не применимо», его можно исключить из проверки.

Пустые группы и NULL в результате

Ещё одна тонкость: если после фильтрации в группе не осталось ни одного значения, EVERY вернёт NULL, а не true.

Например:

SELECT
    client_id,
    EVERY(verified_at IS NOT NULL) FILTER (WHERE verification_required) AS all_required_verified
FROM documents
GROUP BY client_id;

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

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

Но для дашборда часто удобнее получить явный false или true.

Если пустую проверку нужно считать провалом, используйте COALESCE вокруг результата:

SELECT
    client_id,
    COALESCE(
        EVERY(verified_at IS NOT NULL) FILTER (WHERE verification_required),
        false
    ) AS all_required_verified
FROM documents
GROUP BY client_id;

Теперь вместо NULL будет false.

Если по вашей бизнес-логике отсутствие обязательных документов означает «нарушений нет», можно заменить на true:

SELECT
    client_id,
    COALESCE(
        EVERY(verified_at IS NOT NULL) FILTER (WHERE verification_required),
        true
    ) AS all_required_verified
FROM documents
GROUP BY client_id;

Главное — принять решение явно. Не оставляйте смысл NULL на догадки.

Хороший отчёт: флаг плюс диагностика

EVERY даёт красивый итоговый флаг, но он не показывает, какая именно строка всё сломала.

Для дашборда флага достаточно. Для инженера, аналитика или поддержки — нет.

Поэтому в серьёзных проверках полезно выводить рядом несколько метрик:

SELECT
    order_id,
    EVERY(status = 'shipped') AS fully_shipped,
    COUNT(*) AS items_count,
    COUNT(*) FILTER (WHERE status <> 'shipped') AS not_shipped_count,
    COUNT(*) FILTER (WHERE status IS NULL) AS unknown_status_count
FROM order_items
GROUP BY order_id;

Такой отчёт уже намного честнее.

Он показывает:

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

Если видим fully_shipped = true, но unknown_status_count > 0, это сигнал: проверка слишком мягкая, потому что NULL был проигнорирован.

Для строгой проверки лучше так:

SELECT
    order_id,
    EVERY(status IS NOT NULL AND status = 'shipped') AS fully_shipped,
    COUNT(*) AS items_count,
    COUNT(*) FILTER (WHERE status IS NULL) AS unknown_status_count
FROM order_items
GROUP BY order_id;

Теперь неизвестный статус уже проваливает правило.

Drill-down: запрос для поиска виноватых строк

Агрегат отвечает на вопрос:

Есть ли проблема в группе?

Но когда проблема найдена, нужен второй запрос:

Какие строки её создали?

Например, дашборд показал, что заказ 1002 не полностью отгружен. Найдём проблемные позиции:

SELECT
    id,
    order_id,
    product_id,
    status
FROM order_items
WHERE order_id = 1002
  AND (status IS NULL OR status <> 'shipped')
ORDER BY id;

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

Хорошая практика такая:

  • EVERY — для верхнего уровня, чтобы увидеть статус группы;
  • отдельный SELECT — для расследования конкретных нарушений.

Флаг нужен дашборду. Детали нужны человеку, который будет чинить данные.

Пример: все события батча относятся к одному клиенту

EVERY полезен не только для статусов и денег.

Представим, что в систему загружается пачка событий. У каждого события есть batch_id и tenant_id. Нужно проверить, что внутри одного батча все события относятся к ожидаемому клиенту.

SELECT
    batch_id,
    EVERY(tenant_id = expected_tenant_id) AS tenant_is_consistent,
    COUNT(*) AS events_count
FROM events
GROUP BY batch_id;

Если хотя бы одно событие попало не к тому клиенту, флаг станет false.

Для строгой проверки с NULL лучше написать так:

SELECT
    batch_id,
    EVERY(
        tenant_id IS NOT NULL
        AND expected_tenant_id IS NOT NULL
        AND tenant_id = expected_tenant_id
    ) AS tenant_is_consistent,
    COUNT(*) AS events_count
FROM events
GROUP BY batch_id;

Теперь неизвестные значения не прячутся, а честно проваливают проверку.

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

Допустим, есть корзина пользователя. Нужно понять, можно ли оформлять заказ: все товары должны быть доступны.

SELECT
    cart_id,
    EVERY(in_stock) AS ready_to_checkout
FROM cart_items
GROUP BY cart_id;

Если in_stock уже булев столбец, условие можно передавать прямо.

Но если in_stock может быть NULL, лучше решить это явно:

SELECT
    cart_id,
    EVERY(COALESCE(in_stock, false)) AS ready_to_checkout
FROM cart_items
GROUP BY cart_id;

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

EVERY и LEFT JOIN

С LEFT JOIN нужно быть особенно внимательным.

Допустим, хотим вывести всех пользователей и проверить, все ли их заказы оплачены.

SELECT
    u.id,
    EVERY(o.status = 'paid') AS all_orders_paid
FROM users AS u
LEFT JOIN orders AS o
    ON o.user_id = u.id
GROUP BY u.id;

Для пользователя без заказов после LEFT JOIN появится строка, где поля заказа равны NULL.

Выражение:

o.status = 'paid'

даст NULL.

А EVERY по одному NULL вернёт NULL.

Поэтому результат будет не true и не false, а неизвестность.

Что с этим делать, зависит от смысла.

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

SELECT
    u.id,
    COALESCE(EVERY(o.status = 'paid'), false) AS all_orders_paid
FROM users AS u
LEFT JOIN orders AS o
    ON o.user_id = u.id
GROUP BY u.id;

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

SELECT
    u.id,
    COUNT(o.id) AS orders_count,
    EVERY(o.status = 'paid') AS all_orders_paid
FROM users AS u
LEFT JOIN orders AS o
    ON o.user_id = u.id
GROUP BY u.id;

А потом уже на уровне отчёта различать:

  • заказов нет;
  • все заказы оплачены;
  • есть неоплаченные;
  • статус неизвестен.

EVERY как проверка инварианта

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

Например:

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

EVERY хорошо подходит для отчётной проверки таких правил.

SELECT
    batch_id,
    EVERY(currency IS NOT NULL) AS all_rows_have_currency,
    EVERY(amount > 0) AS all_amounts_positive
FROM payment_import_rows
GROUP BY batch_id;

Такой запрос можно использовать после загрузки файла, чтобы быстро понять, можно ли двигаться дальше.

Но есть важная граница: EVERY показывает состояние данных, но не защищает данные сам по себе.

Если правило критично, его лучше закреплять ближе к базе:

  • CHECK;
  • NOT NULL;
  • внешний ключ;
  • уникальный индекс;
  • отдельная таблица ошибок;
  • проверка в загрузчике.

EVERY — прекрасный инструмент диагностики и отчётности, но не единственная линия обороны.

EVERY в отчётных витринах

В отчётных витринах часто хочется показывать не десятки технических строк, а один понятный статус.

Например, по клиенту:

SELECT
    client_id,
    EVERY(document_status = 'verified') AS all_documents_verified,
    EVERY(payment_status = 'paid') AS all_payments_paid,
    EVERY(risk_score < 80) AS all_risks_acceptable
FROM client_checks
GROUP BY client_id;

Так витрина становится похожа на набор бизнес-правил.

Ревьюер читает запрос и сразу понимает:

  • все документы проверены;
  • все платежи оплачены;
  • все риск-оценки ниже порога.

Это намного выразительнее, чем набор сложных счётчиков без понятных имён.

Как назвать результат

Хорошее имя колонки помогает читать запрос.

Для EVERY обычно подходят имена с all:

all_paid
all_positive
all_shipped
all_verified
all_documents_ready
all_rows_valid

Например:

SELECT
    order_id,
    EVERY(status = 'shipped') AS all_items_shipped
FROM order_items
GROUP BY order_id;

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

все позиции отгружены

Плохие имена вроде flag, check или result быстро превращают отчёт в загадку.

MySQL: замена через MIN

В MySQL стандартного EVERY обычно не используют. Там булево выражение можно свести через MIN.

Идея такая:

  • условие истинно — это 1;
  • условие ложно — это 0;
  • минимум будет 1 только если все строки дали 1.

Пример:

SELECT
    user_id,
    MIN(amount > 0) = 1 AS all_positive
FROM orders
GROUP BY user_id;

Если у пользователя все суммы положительные, выражение amount > 0 даст только 1, и минимум будет 1.

Если хотя бы одна сумма неположительная, появится 0, и минимум станет 0.

Для строгой обработки неизвестных значений лучше подставить 0:

SELECT
    user_id,
    MIN(COALESCE(amount > 0, 0)) = 1 AS all_positive
FROM orders
GROUP BY user_id;

Так NULL не будет случайно проигнорирован.

ClickHouse: min по условию

В ClickHouse похожую проверку часто делают через min по условию.

SELECT
    user_id,
    min(amount > 0) = 1 AS all_positive
FROM orders
GROUP BY user_id;

Смысл тот же: если все строки дали 1, минимум равен 1. Если есть хотя бы один 0, результат провален.

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

Главная идея не меняется:

Проверка «все строки прошли правило» сводится к минимуму по булевому условию.

Практическая шпаргалка

Для PostgreSQL:

SELECT
    group_id,
    EVERY(condition) AS all_ok
FROM table_name
GROUP BY group_id;

Если NULL должен считаться ошибкой:

SELECT
    group_id,
    EVERY(COALESCE(condition, false)) AS all_ok
FROM table_name
GROUP BY group_id;

Если нужно заменить пустой результат на false:

SELECT
    group_id,
    COALESCE(EVERY(condition), false) AS all_ok
FROM table_name
GROUP BY group_id;

Если нужно показать диагностику:

SELECT
    group_id,
    EVERY(condition) AS all_ok,
    COUNT(*) AS rows_count,
    COUNT(*) FILTER (WHERE condition IS FALSE) AS failed_count,
    COUNT(*) FILTER (WHERE condition IS NULL) AS unknown_count
FROM table_name
GROUP BY group_id;

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

Главное

EVERY — агрегатная функция для правил вида «все строки группы прошли проверку».

Пример:

SELECT
    dept,
    EVERY(salary > 0) AS all_paid
FROM employees
GROUP BY dept;

Она работает как большое AND по группе:

  • если все известные значения true, результат true;
  • если есть хотя бы одно false, результат false;
  • если известных значений нет, результат NULL.

В PostgreSQL EVERY и BOOL_AND в обычных задачах дают одинаковый результат. EVERY читается ближе к человеческой фразе, а BOOL_AND явно показывает булеву операцию.

Главная ловушка — NULL. Условие с неизвестными данными может дать NULL, а агрегат проигнорирует такие строки. Поэтому для строгих проверок пишите условие явно:

EVERY(status IS NOT NULL AND status = 'shipped')

или используйте COALESCE:

EVERY(COALESCE(in_stock, false))

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

Но итоговый флаг — это только верхний слой. Для реальной эксплуатации рядом полезно держать диагностику: сколько строк проверили, сколько провалилось и сколько осталось неизвестными. Тогда зелёный статус не будет прятать дырки в данных, а запрос станет понятным и для разработчика, и для аналитика, и для человека, который потом будет чинить проблему.

Закрепи на практике

Решай задачи в SQL-тренажёре с мгновенной проверкой и подсказками.

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