BOOL_AND и BOOL_OR — это булевые агрегатные функции PostgreSQL.
Они помогают свернуть много строк в один понятный ответ:
- все ли строки в группе прошли проверку;
- есть ли хотя бы одна строка, которая прошла проверку.
Например, можно прямо в SQL спросить:
- все ли позиции заказа уже отгружены;
- есть ли в команде хотя бы один администратор;
- все ли платежи пользователя больше 100;
- есть ли среди заказов хотя бы один оплаченный.
Без таких функций запрос часто превращается в громоздкую конструкцию через COUNT, CASE, SUM и сравнения. А с BOOL_AND и BOOL_OR бизнес-правило читается почти как обычная фраза.
Что делает BOOL_AND
BOOL_AND(expr) возвращает true, если выражение истинно во всех учитываемых строках группы.
Проще: это проверка «все ли».
Допустим, есть таблица пользователей:
Проверим, все ли пользователи активны:
SELECT
BOOL_AND(active) AS all_active
FROM users;
Результат:
Почему false? Потому что хотя бы у одного пользователя active = false.
Если бы все значения были true, результат был бы true.
Что делает BOOL_OR
BOOL_OR(expr) возвращает true, если выражение истинно хотя бы в одной строке группы.
Проще: это проверка «есть ли хотя бы один».
Например, есть таблица пользователей:
Проверим, есть ли среди пользователей администратор:
SELECT
BOOL_OR(is_admin) AS has_admin
FROM users;
Результат:
Почему 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.
Например:
Итог:
BOOL_AND(expr)
Результат:
А теперь так:
Итог уже будет false, потому что одна строка нарушила правило.
Как BOOL_OR работает внутри
BOOL_OR мягче. Он ищет хотя бы одно совпадение.
Он спрашивает:
Есть ли хотя бы одна строка, где условие истинно?
Если хотя бы одна строка дала true, итог true.
Например:
Итог:
BOOL_OR(expr)
Результат:
А если все строки дали 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 короче, чище и намного понятнее.
BOOL_ANDиBOOL_OR— это булевые агрегатные функции PostgreSQL.Они помогают свернуть много строк в один понятный ответ:
Например, можно прямо в SQL спросить:
Без таких функций запрос часто превращается в громоздкую конструкцию через
COUNT,CASE,SUMи сравнения. А сBOOL_ANDиBOOL_ORбизнес-правило читается почти как обычная фраза.Что делает BOOL_AND
BOOL_AND(expr)возвращаетtrue, если выражение истинно во всех учитываемых строках группы.Проще: это проверка «все ли».
Допустим, есть таблица пользователей:
Проверим, все ли пользователи активны:
SELECT BOOL_AND(active) AS all_active FROM users;Результат:
Почему
false? Потому что хотя бы у одного пользователяactive = false.Если бы все значения были
true, результат был быtrue.Что делает BOOL_OR
BOOL_OR(expr)возвращаетtrue, если выражение истинно хотя бы в одной строке группы.Проще: это проверка «есть ли хотя бы один».
Например, есть таблица пользователей:
Проверим, есть ли среди пользователей администратор:
SELECT BOOL_OR(is_admin) AS has_admin FROM users;Результат:
Почему
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;Результат может быть таким:
Как это читать:
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;Это читается как бизнес-правило:
Пример результата:
У пользователя
2есть хотя бы один заказ меньше 100, поэтому результатfalse.Пример: все ли позиции заказа отгружены
Допустим, есть таблица позиций заказа:
Нужно понять, какие заказы полностью отгружены.
SELECT order_id, BOOL_AND(status = 'shipped') AS all_shipped FROM order_items GROUP BY order_id ORDER BY order_id;Результат:
Заказ
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;Результат:
Если у пользователя есть хотя бы один заказ со статусом
paid, результат будетtrue.Если таких заказов нет, результат будет
false.Как BOOL_AND работает внутри
Можно думать о
BOOL_ANDкак о строгом проверяющем.Он смотрит на группу строк и спрашивает:
Если все строки дали
true, итогtrue.Если хотя бы одна строка дала
false, итогfalse.Например:
Итог:
Результат:
А теперь так:
Итог уже будет
false, потому что одна строка нарушила правило.Как BOOL_OR работает внутри
BOOL_ORмягче. Он ищет хотя бы одно совпадение.Он спрашивает:
Если хотя бы одна строка дала
true, итогtrue.Например:
Итог:
Результат:
А если все строки дали
false, тогда результат будетfalse.NULL игнорируется
Очень важный момент:
BOOL_ANDиBOOL_ORигнорируютNULL.Они учитывают только известные значения:
trueиfalse.Допустим, есть заказы:
Проверим, все ли заказы отгружены:
SELECT user_id, BOOL_AND(status = 'shipped') AS all_shipped FROM orders GROUP BY user_id;Пустой статус не будет считаться как
false. Он дастNULL, а агрегат его пропустит.То есть результат может оказаться таким:
На первый взгляд это может удивить. Ведь один статус неизвестен. Но для SQL неизвестное значение — это не ложь. Это именно неизвестность.
Когда NULL может стать проблемой
Представьте бизнес-правило:
В таком случае
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.Например:
Запрос:
SELECT user_id, BOOL_AND(approved) AS all_approved, BOOL_OR(approved) AS any_approved FROM checks GROUP BY user_id;Результат:
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 короче, чище и намного понятнее.