sqlpostgresqlaggregationboolean

BOOL_AND and BOOL_OR in PostgreSQL: Boolean Aggregates per Group

Check "every row in the group is true" with BOOL_AND and "at least one" with BOOL_OR, see how NULLs behave, what EVERY adds, and how to emulate it in MySQL and ClickHouse.

2 min branjaReferencesql · postgresql · aggregation · boolean · mysql · clickhouse
Ta članek je trenutno v ruščini — angleški prevod je v pripravi.

BOOL_AND и BOOL_OR — это агрегаты PostgreSQL над булевым выражением: первый сворачивает группу в true, когда выражение истинно во всех строках, второй — когда хотя бы в одной. Их берут, чтобы прямо в SELECT спросить «все ли позиции заказа отгружены?» или «есть ли среди пользователей хотя бы один админ?», не выписывая громоздкие конструкции вида COUNT(*) = COUNT(...).

Что именно возвращают BOOL_AND и BOOL_OR

Обе функции принимают булево выражение и сворачивают группу в один флаг:

  • BOOL_AND(expr) возвращает true, если expr истинно во всех учитываемых строках;
  • BOOL_OR(expr) возвращает true, если expr истинно хотя бы в одной строке.
SELECT
    BOOL_AND(active)   AS all_active,
    BOOL_OR(is_admin)  AS has_admin
FROM users;

С GROUP BY это превращается в компактный отчёт по сегментам.

SELECT
    country,
    BOOL_AND(active)  AS everyone_active,
    BOOL_OR(is_admin) AS any_admin
FROM users
GROUP BY country;

Аргумент не обязан быть готовой колонкой-флагом. Подойдёт любое сравнение, например проверка минимальной суммы заказа.

SELECT
    user_id,
    BOOL_AND(amount >= 100) AS all_big_orders
FROM orders
GROUP BY user_id;

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

NULL игнорируются (и это важно)

Как и большинство агрегатов, BOOL_AND и BOOL_OR пропускают строки, где выражение равно NULL. Они считают только известные true/false значения.

-- a NULL status is ignored, not treated as 'not shipped'
SELECT
    user_id,
    BOOL_AND(status = 'shipped') AS all_shipped
FROM orders
GROUP BY user_id;

Отсюда главная грабля: неизвестное не равно ложному. Один пустой статус не провалит BOOL_AND; он просто не попадёт в расчёт. Если все значения в группе неизвестны или строк после фильтра нет, результат будет NULL, а не true или false.

Запрос WHERE all_shipped = true молча отбросит такие группы, потому что сравнение с NULL не истинно. Если пустые статусы должны считаться нарушением, нормализуйте результат или аргумент явно: COALESCE(BOOL_AND(status = 'shipped'), false).

BOOL_AND против EVERY

EVERY — стандартный SQL-синоним для BOOL_AND в PostgreSQL. Агрегата ANY в такой роли нет: слово ANY занято синтаксисом подзапросов, поэтому для «хотя бы одной строки» используется BOOL_OR.

-- EVERY is exactly BOOL_AND
SELECT
    dept,
    EVERY(salary >= 50000) AS all_well_paid,
    BOOL_OR(salary >= 200000) AS any_top_earner
FROM employees
GROUP BY dept;

Выбор имени — вопрос стиля. EVERY хорошо читается в отчётах, BOOL_AND лучше смотрится рядом с BOOL_OR, когда в коде явно нужна пара «все / хотя бы один». В HAVING булев агрегат особенно выразителен.

SELECT dept
FROM employees
GROUP BY dept
HAVING BOOL_AND(salary >= 50000);

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

Эмуляция в MySQL и ClickHouse

В MySQL нет готовых BOOL_AND и BOOL_OR, но булевы выражения возвращают 0 или 1. Поэтому MIN работает как AND по группе, а MAX — как OR.

-- MySQL: MIN/MAX over a boolean expression cast to 0/1
SELECT
    user_id,
    MIN(amount >= 100) AS all_big_orders,  -- like BOOL_AND
    MAX(status = 'paid') AS any_paid        -- like BOOL_OR
FROM orders
GROUP BY user_id;

Несколько отличий:

  • в MySQL выражение amount >= 100 уже числовое: 1 для true и 0 для false;
  • в PostgreSQL для такого трюка нужен каст ::int, потому что MIN и MAX не применяют к boolean как к числу;
  • NULL в MIN/MAX тоже игнорируется, поэтому поведение по неизвестным значениям остаётся тем же;
  • в ClickHouse обычно пишут min(amount >= 100) и max(status = 'paid'); для nullable-сценариев смотрят на minOrNull/maxOrNull и тип UInt8.

Итог: в PostgreSQL для проверок «вся группа удовлетворяет условию» и «хотя бы одна строка удовлетворяет» берите BOOL_AND/BOOL_OR (или EVERY), помня про пропуск NULL и про NULL на полностью пустой группе. Заранее решите, что для вашей бизнес-логики значит неизвестное значение, и закрепите это решение через COALESCE, а не оставляйте на умолчания агрегата. В MySQL и ClickHouse тот же смысл собирается из MIN/MAX булева флага.

Vadite na resničnih nalogah

Rešujte naloge v vadnici SQL s takojšnjim ocenjevanjem in namigi.

Odpri vadnico