sqlpostgresqlaggregationboolean

BOOL_AND e BOOL_OR no PostgreSQL: agregados booleanos por grupo

Verifique "todas as linhas do grupo sao verdadeiras" com BOOL_AND e "ao menos uma" com BOOL_OR, veja como os NULL se comportam e como emular no MySQL e no ClickHouse.

3 min de leituraReferencesql · postgresql · aggregation · boolean · mysql · clickhouse

BOOL_AND e BOOL_OR sao funcoes de agregacao sobre uma coluna booleana. A primeira responde "todas as linhas do grupo sao verdadeiras?", a segunda "ha ao menos uma verdadeira?". Sao a forma direta de perguntar coisas como "todos os pedidos deste grupo se qualificam?" ou "algum usuario e admin?", sem a ginastica de COUNT(*) = COUNT(...).

O que BOOL_AND e BOOL_OR retornam

Ambas recebem uma expressao booleana e dobram o grupo em um unico valor:

  • BOOL_AND(expr) e verdadeiro apenas quando expr e verdadeiro em todas as linhas do grupo.
  • BOOL_OR(expr) e verdadeiro quando expr e verdadeiro em ao menos uma linha.
SELECT
    BOOL_AND(active)   AS all_active,
    BOOL_OR(is_admin)  AS has_admin
FROM users;

Na maioria das vezes elas ficam ao lado de um GROUP BY. Por pais, de relance voce ve onde todas as contas estao ativas e onde ha ao menos um administrador:

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

A expressao nao precisa ser uma coluna booleana pronta; qualquer comparacao serve. Assim se verifica que todos os pedidos de um usuario sao grandes:

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

Os NULL sao ignorados (e um grupo vazio retorna NULL)

Como os demais agregados, BOOL_AND e BOOL_OR pulam as linhas em que a expressao e NULL. O resultado e calculado apenas sobre os valores booleanos "conhecidos":

  • BOOL_AND olha somente para as linhas nao NULL; um unico NULL nao forca o resultado para falso.
  • BOOL_OR tambem se apoia nas linhas nao NULL.
  • Se todos os valores do grupo forem NULL (ou nao houver linhas), o resultado e NULL, e nao 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;

Pegadinha: a regra "tudo NULL produz NULL" e facil de deixar passar. Um filtro WHERE all_shipped = true descarta em silencio os grupos cujo status nunca e preenchido, porque para eles o predicado e NULL, nao false. Se esses grupos devem contar como "nao cumpridos", envolva o resultado: COALESCE(BOOL_AND(status = 'shipped'), false).

BOOL_AND versus EVERY

No PostgreSQL, EVERY e o sinonimo do SQL padrao para BOOL_AND, identico no comportamento. Nao existe um agregado ANY simetrico no padrao (ANY esta reservado para subconsultas), entao BOOL_OR cobre o caso de "ao menos uma".

-- 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;

Escolher entre EVERY e BOOL_AND e questao de estilo e portabilidade: EVERY esta mais perto do padrao, enquanto BOOL_AND fica mais legivel ao lado de BOOL_OR. Um padrao util e um portao de grupo inteiro no HAVING, por exemplo departamentos em que cada funcionario ganha ao menos um limite:

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

Emulando no MySQL e no ClickHouse

O MySQL nao tem BOOL_AND/BOOL_OR nativos, mas la um booleano e 0/1, entao MIN e MAX dao o mesmo resultado: o minimo das flags e 1 somente quando todas sao 1 (isso e AND), e o maximo e 1 quando alguma e 1 (isso e 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;

O mesmo truque funciona no PostgreSQL se voce converter o booleano para inteiro antes: MIN((amount >= 100)::int). Algumas diferencas para ter em mente:

  • No MySQL a comparacao amount >= 100 ja retorna 1/0, entao nao e preciso cast explicito. No PostgreSQL adicione ::int, porque MIN/MAX nao aceitam um boolean diretamente.
  • O MIN/MAX do MySQL tambem ignora os NULL, entao o comportamento diante de grupos vazios (saida NULL) coincide.
  • O ClickHouse tem min/max nativos sobre UInt8, alem de minOrNull/maxOrNull; nao ha um bool_and direto, entao escreve-se min(amount >= 100) e max(status = 'paid').

Resumindo: para as verificacoes de "o grupo inteiro satisfaz uma condicao" e "ao menos um satisfaz", use BOOL_AND/BOOL_OR (ou EVERY) no PostgreSQL, lembrando que ignoram NULL e retornam NULL em um grupo totalmente vazio; em outros motores, o mesmo sentido e construido com MIN/MAX de uma flag booleana.

Pratique com exercícios reais

Resolva exercícios no treinador de SQL com correção instantânea e dicas.

Abrir o treinador