sqlpostgresqlaggregationboolean

BOOL_AND и BOOL_OR в SQL: как проверить «все» и «хотя бы один»

BOOL_AND отвечает «все строки группы истинны», BOOL_OR — «хотя бы одна»; разбираем, как они пропускают NULL, чем полезен EVERY и как повторить это через MIN/MAX в MySQL и ClickHouse.

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

BOOL_AND и BOOL_OR — это булевые агрегатные функции PostgreSQL.

Они помогают свернуть много строк в один понятный ответ:

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

Например, можно прямо в SQL спросить:

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

Без таких функций запрос часто превращается в громоздкую конструкцию через COUNT, CASE, SUM и сравнения. А с BOOL_AND и BOOL_OR бизнес-правило читается почти как обычная фраза.

Что делает BOOL_AND

BOOL_AND(expr) возвращает true, если выражение истинно во всех учитываемых строках группы.

Проще: это проверка «все ли».

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

id email active
1 ann@example.com true
2 bob@example.com true
3 max@example.com false

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

SELECT
  BOOL_AND(active) AS all_active
FROM users;

Результат:

all_active
false

Почему false? Потому что хотя бы у одного пользователя active = false.

Если бы все значения были true, результат был бы true.

Что делает BOOL_OR

BOOL_OR(expr) возвращает true, если выражение истинно хотя бы в одной строке группы.

Проще: это проверка «есть ли хотя бы один».

Например, есть таблица пользователей:

id email is_admin
1 ann@example.com false
2 bob@example.com true
3 max@example.com false

Проверим, есть ли среди пользователей администратор:

SELECT
  BOOL_OR(is_admin) AS has_admin
FROM users;

Результат:

has_admin
true

Почему true? Потому что хотя бы одна строка прошла проверку.

BOOL_AND и BOOL_OR в одном запросе

Часто эти функции используют вместе: одна отвечает за «все», другая — за «хотя бы один».

SELECT
  BOOL_AND(active) AS all_active,
  BOOL_OR(is_admin) AS has_admin
FROM users;

Такой запрос можно прочитать почти без перевода:

  • all_active — все ли пользователи активны;
  • has_admin — есть ли хотя бы один администратор.

Это гораздо приятнее, чем писать длинные условия через подсчёты.

Проверка по группам через GROUP BY

С GROUP BY булевые агрегаты становятся особенно полезными.

Например, проверим пользователей по странам:

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

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

country everyone_active any_admin
DE true false
ES false true
PL true true

Как это читать:

  • в DE все пользователи активны, но админов нет;
  • в ES не все пользователи активны, зато есть хотя бы один админ;
  • в PL все активны и есть админ.

Получился компактный отчёт по сегментам без лишней ручной логики.

Аргументом может быть условие

В BOOL_AND и BOOL_OR не обязательно передавать готовый столбец типа boolean.

Можно передать любое выражение, которое возвращает true или false.

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

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

Это читается как бизнес-правило:

для каждого пользователя проверь, что все его заказы не меньше 100.

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

user_id all_big_orders
1 true
2 false
3 true

У пользователя 2 есть хотя бы один заказ меньше 100, поэтому результат false.

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

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

order_id item_id status
101 1 shipped
101 2 shipped
102 3 shipped
102 4 pending

Нужно понять, какие заказы полностью отгружены.

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

Результат:

order_id all_shipped
101 true
102 false

Заказ 101 полностью отгружен.

Заказ 102 ещё нет, потому что одна позиция в статусе pending.

Без BOOL_AND пришлось бы писать более тяжёлую проверку: считать все строки, считать отгруженные строки и сравнивать количества. А здесь правило видно сразу.

Пример: есть ли оплаченный заказ

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

Для этого подходит BOOL_OR.

SELECT
  user_id,
  BOOL_OR(status = 'paid') AS has_paid_order
FROM orders
GROUP BY user_id
ORDER BY user_id;

Результат:

user_id has_paid_order
1 true
2 false
3 true

Если у пользователя есть хотя бы один заказ со статусом paid, результат будет true.

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

Как BOOL_AND работает внутри

Можно думать о BOOL_AND как о строгом проверяющем.

Он смотрит на группу строк и спрашивает:

Все строки прошли проверку?

Если все строки дали true, итог true.

Если хотя бы одна строка дала false, итог false.

Например:

expr
true
true
true

Итог:

BOOL_AND(expr)

Результат:

bool_and
true

А теперь так:

expr
true
false
true

Итог уже будет false, потому что одна строка нарушила правило.

Как BOOL_OR работает внутри

BOOL_OR мягче. Он ищет хотя бы одно совпадение.

Он спрашивает:

Есть ли хотя бы одна строка, где условие истинно?

Если хотя бы одна строка дала true, итог true.

Например:

expr
false
false
true

Итог:

BOOL_OR(expr)

Результат:

bool_or
true

А если все строки дали false, тогда результат будет false.

NULL игнорируется

Очень важный момент: BOOL_AND и BOOL_OR игнорируют NULL.

Они учитывают только известные значения: true и false.

Допустим, есть заказы:

user_id status
1 shipped
1 shipped
1 NULL

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

SELECT
  user_id,
  BOOL_AND(status = 'shipped') AS all_shipped
FROM orders
GROUP BY user_id;

Пустой статус не будет считаться как false. Он даст NULL, а агрегат его пропустит.

То есть результат может оказаться таким:

user_id all_shipped
1 true

На первый взгляд это может удивить. Ведь один статус неизвестен. Но для SQL неизвестное значение — это не ложь. Это именно неизвестность.

Когда NULL может стать проблемой

Представьте бизнес-правило:

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

В таком случае NULL должен ломать проверку. Неизвестный статус нельзя считать успешным.

Тогда условие нужно написать явно:

SELECT
  order_id,
  BOOL_AND(COALESCE(status = 'shipped', false)) AS all_shipped
FROM order_items
GROUP BY order_id
ORDER BY order_id;

Теперь если status равен NULL, выражение превратится в false.

То есть пустой статус уже не будет тихо проигнорирован.

Можно сделать и так:

SELECT
  order_id,
  BOOL_AND(status IS NOT NULL AND status = 'shipped') AS all_shipped
FROM order_items
GROUP BY order_id
ORDER BY order_id;

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

Если в группе только NULL

Если в группе нет ни одного известного значения true или false, результат будет NULL.

Например:

user_id approved
1 NULL
1 NULL

Запрос:

SELECT
  user_id,
  BOOL_AND(approved) AS all_approved,
  BOOL_OR(approved) AS any_approved
FROM checks
GROUP BY user_id;

Результат:

user_id all_approved any_approved
1 NULL NULL

SQL не говорит ни «да», ни «нет». Он говорит: «я не знаю».

Если для отчёта вам нужен конкретный false, добавьте COALESCE:

SELECT
  user_id,
  COALESCE(BOOL_AND(approved), false) AS all_approved,
  COALESCE(BOOL_OR(approved), false) AS any_approved
FROM checks
GROUP BY user_id;

Теперь NULL в результате агрегата превратится в false.

Почему WHERE может молча отбросить NULL

Допустим, вы посчитали флаг all_shipped, а потом хотите оставить только полностью отгруженные заказы.

Такой запрос выглядит логично:

SELECT
  order_id,
  BOOL_AND(status = 'shipped') AS all_shipped
FROM order_items
GROUP BY order_id
HAVING BOOL_AND(status = 'shipped') = true;

Но группы, где результат равен NULL, не попадут в выдачу.

Почему? Потому что сравнение NULL = true не даёт true. Оно даёт неизвестный результат, а HAVING, как и WHERE, пропускает только те строки, где условие истинно.

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

SELECT
  order_id,
  BOOL_AND(COALESCE(status = 'shipped', false)) AS all_shipped
FROM order_items
GROUP BY order_id
HAVING BOOL_AND(COALESCE(status = 'shipped', false));

Так запрос становится честнее: пустой статус не прячется, а считается нарушением правила.

EVERY: стандартный синоним BOOL_AND

В PostgreSQL есть ещё EVERY.

Это стандартный SQL-синоним для BOOL_AND.

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

SELECT
  BOOL_AND(active) AS all_active
FROM users;

И такой:

SELECT
  EVERY(active) AS all_active
FROM users;

EVERY читается красиво: «каждый ли пользователь активен».

Например:

SELECT
  dept,
  EVERY(salary >= 50000) AS all_well_paid
FROM employees
GROUP BY dept
ORDER BY dept;

Такой запрос проверяет, во всех ли строках отдела зарплата не меньше 50000.

Почему нет агрегата ANY

Может показаться, что если есть EVERY, то должен быть и ANY.

Но в PostgreSQL слово ANY уже используется в другом синтаксисе — например, при сравнении с массивом или результатом подзапроса.

Поэтому для проверки «хотя бы одна строка» используют BOOL_OR.

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

Здесь:

  • EVERY(salary >= 50000) проверяет, все ли в отделе получают не меньше 50000;
  • BOOL_OR(salary >= 200000) проверяет, есть ли в отделе хотя бы один сотрудник с зарплатой от 200000.

BOOL_AND и BOOL_OR в HAVING

Булевые агрегаты отлично смотрятся в HAVING.

Напомним: WHERE фильтрует строки до группировки, а HAVING фильтрует уже готовые группы.

Например, найдём отделы, где у всех сотрудников зарплата не меньше 50000:

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

Обратите внимание: не обязательно писать = true.

Так как BOOL_AND(...) уже возвращает булево значение, его можно использовать как готовое условие.

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

SELECT
  dept
FROM employees
GROUP BY dept
HAVING BOOL_OR(salary >= 200000)
ORDER BY dept;

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

Как заменить BOOL_AND через COUNT

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

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

Можно было бы написать так:

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

Смысл такой:

  • COUNT(*) считает все позиции заказа;
  • COUNT(*) FILTER (WHERE status = 'shipped') считает только отгруженные позиции;
  • если количества равны, значит все позиции отгружены.

Но через BOOL_AND это короче:

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

Оба подхода имеют право на жизнь. Но BOOL_AND чаще читается как готовое бизнес-правило, а не как математическая проверка количества строк.

Как заменить BOOL_OR через COUNT

Для проверки «есть ли хотя бы одна строка» тоже можно использовать COUNT.

Например:

SELECT
  user_id,
  COUNT(*) FILTER (WHERE status = 'paid') > 0 AS has_paid_order
FROM orders
GROUP BY user_id
ORDER BY user_id;

Через BOOL_OR короче:

SELECT
  user_id,
  BOOL_OR(status = 'paid') AS has_paid_order
FROM orders
GROUP BY user_id
ORDER BY user_id;

Первый вариант буквально считает строки. Второй сразу говорит, что нам нужен булев ответ: есть или нет.

Эмуляция в MySQL

В MySQL нет агрегатов BOOL_AND и BOOL_OR с такими именами.

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

  • true — это 1;
  • false — это 0.

Поэтому можно использовать MIN и MAX.

MIN работает как проверка «все ли»:

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

Почему это похоже на BOOL_AND?

Если все строки дают 1, минимальное значение будет 1.

Если хотя бы одна строка даёт 0, минимальное значение станет 0.

Для проверки «есть ли хотя бы один» используют MAX:

SELECT
  user_id,
  MAX(status = 'paid') AS any_paid
FROM orders
GROUP BY user_id
ORDER BY user_id;

Если хотя бы одна строка дала 1, максимум будет 1.

Если все строки дали 0, максимум будет 0.

Эмуляция в PostgreSQL через MIN и MAX

В PostgreSQL нельзя просто взять MIN или MAX от boolean как от числа в таком же стиле, как в MySQL.

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

SELECT
  user_id,
  MIN((amount >= 100)::int) AS all_big_orders,
  MAX((status = 'paid')::int) AS any_paid
FROM orders
GROUP BY user_id
ORDER BY user_id;

Здесь:

  • true::int превращается в 1;
  • false::int превращается в 0.

Но в обычном PostgreSQL-коде так делать чаще не нужно. Если есть BOOL_AND и BOOL_OR, лучше использовать их: они понятнее и честнее выражают намерение.

Эмуляция в ClickHouse

В ClickHouse похожий смысл обычно собирают через min и max по булевому выражению.

SELECT
  user_id,
  min(amount >= 100) AS all_big_orders,
  max(status = 'paid') AS any_paid
FROM orders
GROUP BY user_id
ORDER BY user_id;

Как и в MySQL, идея простая:

  • минимум по флагам работает как «все ли»;
  • максимум по флагам работает как «есть ли хотя бы один».

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

Когда использовать BOOL_AND

BOOL_AND нужен, когда правило должно выполниться для всех строк группы.

Например:

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

Хорошие вопросы для BOOL_AND:

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

Ключевая мысль: одно нарушение делает весь результат false.

Когда использовать BOOL_OR

BOOL_OR нужен, когда достаточно одного совпадения.

Например:

SELECT
  user_id,
  BOOL_OR(status = 'paid') AS has_paid_order
FROM orders
GROUP BY user_id;

Хорошие вопросы для BOOL_OR:

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

Ключевая мысль: одно совпадение делает весь результат true.

На что обратить внимание

Главная тонкость — NULL.

BOOL_AND и BOOL_OR не считают NULL ни успехом, ни провалом. Они его пропускают.

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

SELECT
  order_id,
  BOOL_AND(COALESCE(status = 'shipped', false)) AS all_shipped
FROM order_items
GROUP BY order_id;

Или так:

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

Не оставляйте смысл NULL на волю агрегата. Сначала решите, что неизвестность означает в вашей задаче, и только потом пишите запрос.

Главное из статьи

BOOL_AND и BOOL_OR — агрегатные функции PostgreSQL для булевых выражений.

BOOL_AND(expr) отвечает на вопрос:

выражение истинно во всех строках группы?

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

BOOL_OR(expr) отвечает на вопрос:

выражение истинно хотя бы в одной строке группы?

SELECT
  user_id,
  BOOL_OR(status = 'paid') AS has_paid_order
FROM orders
GROUP BY user_id;

EVERY — стандартный SQL-синоним для BOOL_AND в PostgreSQL.

NULL внутри этих агрегатов игнорируется. Если все значения неизвестны, результат будет NULL.

Если NULL должен считаться провалом, используйте COALESCE или явное условие:

SELECT
  order_id,
  BOOL_AND(COALESCE(status = 'shipped', false)) AS all_shipped
FROM order_items
GROUP BY order_id;

В MySQL и ClickHouse похожую логику обычно собирают через MIN и MAX по булевому выражению.

Главная идея простая: BOOL_AND — это «все», BOOL_OR — это «хотя бы один». Когда в запросе нужно выразить именно такое правило, эти функции делают SQL короче, чище и намного понятнее.

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

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

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