sqlpostgresqlaggregationboolean

BOOL_AND y BOOL_OR en PostgreSQL: agregados booleanos por grupo

Comprueba "todas las filas del grupo son verdaderas" con BOOL_AND y "al menos una" con BOOL_OR, mira como se tratan los NULL y como emularlo en MySQL y ClickHouse.

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

BOOL_AND y BOOL_OR son funciones de agregacion sobre una columna booleana. La primera responde a "?todas las filas del grupo son verdaderas?", la segunda a "?hay al menos una verdadera?". Son la forma directa de preguntar cosas como "?todos los pedidos de este grupo cumplen?" o "?algun usuario es admin?", sin la gimnasia de COUNT(*) = COUNT(...).

Que devuelven BOOL_AND y BOOL_OR

Ambas reciben una expresion booleana y pliegan el grupo en un unico valor:

  • BOOL_AND(expr) es verdadero solo cuando expr es verdadero en todas las filas del grupo.
  • BOOL_OR(expr) es verdadero cuando expr es verdadero en al menos una fila.
SELECT
    BOOL_AND(active)   AS all_active,
    BOOL_OR(is_admin)  AS has_admin
FROM users;

Lo normal es usarlas junto a un GROUP BY. Por pais, de un vistazo ves donde todas las cuentas estan activas y donde hay al menos un administrador:

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

La expresion no tiene que ser una columna booleana ya hecha; sirve cualquier comparacion. Asi se comprueba que todos los pedidos de un usuario son grandes:

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

Los NULL se ignoran (y un grupo vacio da NULL)

Como el resto de agregados, BOOL_AND y BOOL_OR saltan las filas donde la expresion es NULL. El resultado se calcula solo sobre los valores booleanos "conocidos":

  • BOOL_AND mira solo las filas no NULL; un unico NULL no fuerza el resultado a falso.
  • BOOL_OR se apoya igualmente en las filas no NULL.
  • Si todos los valores del grupo son NULL (o no hay filas), el resultado es NULL, no 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;

Trampa: la regla "todo NULL produce NULL" se escapa con facilidad. Un filtro WHERE all_shipped = true descarta en silencio los grupos cuyo estado nunca se rellena, porque para ellos el predicado es NULL, no false. Si esos grupos deben contar como "no cumplidos", envuelve el resultado: COALESCE(BOOL_AND(status = 'shipped'), false).

BOOL_AND frente a EVERY

En PostgreSQL, EVERY es el sinonimo del SQL estandar para BOOL_AND, identico en comportamiento. No existe un agregado ANY simetrico en el estandar (ANY esta reservado para subconsultas), asi que BOOL_OR cubre el caso de "al menos una".

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

Elegir entre EVERY y BOOL_AND es cuestion de estilo y portabilidad: EVERY esta mas cerca del estandar, mientras que BOOL_AND se lee mejor junto a BOOL_OR. Un patron util es una compuerta de grupo entero en HAVING, por ejemplo departamentos donde cada empleado gana al menos un umbral:

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

Emularlo en MySQL y ClickHouse

MySQL no tiene BOOL_AND/BOOL_OR nativos, pero alli un booleano es 0/1, asi que MIN y MAX dan el mismo resultado: el minimo de las banderas es 1 solo cuando todas son 1 (eso es AND), y el maximo es 1 cuando alguna es 1 (eso es 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;

El mismo truco funciona en PostgreSQL si primero conviertes el booleano a entero: MIN((amount >= 100)::int). Algunas diferencias a tener presentes:

  • En MySQL la comparacion amount >= 100 ya devuelve 1/0, asi que no hace falta cast explicito. En PostgreSQL anade ::int, porque MIN/MAX no aceptan un boolean directamente.
  • El MIN/MAX de MySQL tambien ignora los NULL, asi que el comportamiento ante grupos vacios (salida NULL) coincide.
  • ClickHouse tiene min/max nativos sobre UInt8, ademas de minOrNull/maxOrNull; no hay un bool_and directo, asi que se escribe min(amount >= 100) y max(status = 'paid').

En resumen: para las comprobaciones de "todo el grupo cumple una condicion" y "al menos uno cumple", usa BOOL_AND/BOOL_OR (o EVERY) en PostgreSQL, recordando que ignoran NULL y devuelven NULL en un grupo totalmente vacio; en otros motores, el mismo sentido se construye con MIN/MAX de una bandera booleana.

Practica con ejercicios reales

Resuelve ejercicios en el entrenador de SQL con corrección instantánea y pistas.

Abrir el entrenador