sqlpostgresqlmysqlfunctions

GREATEST y LEAST en SQL: maximo y minimo por fila entre columnas

Como GREATEST y LEAST devuelven el maximo y el minimo dentro de una fila, acotan un valor a un rango y tratan los NULL de forma distinta en PostgreSQL, MySQL y ClickHouse.

4 min de lecturaReferencesql · postgresql · mysql · functions · null

GREATEST y LEAST toman una lista de argumentos y devuelven el mayor o el menor de ellos dentro de una misma fila. Se recurre a ellos cuando no hay que comparar filas entre si, sino varias columnas o expresiones dentro de la fila actual: elegir la mas reciente de dos fechas, poner un suelo o un techo a un numero, o acotar un valor a un rango. No son funciones de agregacion: en lugar de agrupar filas, miran las columnas una junto a otra, en horizontal, y emiten un valor por cada fila.

Ambas funciones comparten la misma firma: la entrada es una lista de dos o mas expresiones de tipo comparable y la salida es un unico valor de ese mismo tipo. GREATEST devuelve el maximo de la lista y LEAST el minimo. Puedes mezclar los argumentos con libertad (columnas, literales, resultados de otras llamadas), por eso ambas funciones se anidan con tanta facilidad una dentro de otra.

Conviene revisar antes dos cosas: los tipos y el NULL. Los tipos deben ser comparables entre si, o PostgreSQL los convertira a un tipo comun o se negara a ejecutar la consulta; y un NULL en la lista de argumentos se trata de forma distinta en cada motor, que es justo donde suele romperse una consulta migrada.

En que se diferencian de MAX y MIN

La confusion nace de las palabras: un "maximo" es un "maximo". Pero las dos familias trabajan en direcciones distintas. MAX y MIN son agregados: recorren verticalmente las filas de un grupo y devuelven un solo numero para todo el resultado. GREATEST y LEAST recorren horizontalmente los argumentos de una sola fila y devuelven un valor por cada fila.

-- Aggregate: one number per group, scans many rows
SELECT MAX(amount) AS biggest_order
FROM orders;

-- Row-wise: one value per row, compares columns side by side
SELECT id, GREATEST(amount, 10) AS amount_floor_10
FROM orders;

Un caso clasico es elegir la fecha mas reciente entre varias dentro de la propia fila, sin subconsultas ni funciones de ventana:

SELECT
    u.id,
    GREATEST(u.created_at, o.created_at) AS last_touch
FROM users u
JOIN orders o ON o.user_id = u.id;

Acotar un valor a un rango (clamp)

El truco mas practico con estas funciones es meter un numero en un pasillo entre un limite inferior y uno superior. Se logra anidando una llamada dentro de otra: GREATEST(lo, LEAST(hi, x)) garantiza que el resultado nunca se salga de [lo, hi]. La formula se lee de dentro hacia fuera: primero recortas por arriba y luego elevas por abajo.

-- Clamp the order amount into the range [1, 1000]
SELECT
    id,
    amount,
    GREATEST(1, LEAST(1000, amount)) AS amount_clamped
FROM orders;

Es imprescindible alli donde un valor debe mantenerse dentro de unos limites: descuentos, puntos de fidelidad, normalizacion de salarios para informes.

-- Cap every salary at 200000 but never below 30000
SELECT
    id,
    name,
    GREATEST(30000, LEAST(200000, salary)) AS salary_banded
FROM employees;

Veamos quien hace que:

  • el GREATEST(lo, ...) exterior eleva hasta lo los valores demasiado pequenos;
  • el LEAST(hi, x) interior baja hasta hi los valores demasiado grandes;
  • el orden de los limites es critico: si por accidente pones lo > hi, el pasillo se colapsa en una contradiccion y obtienes un resultado sin sentido, en silencio y sin error.

Manejo de NULL: la mina principal

Aqui empieza lo realmente traicionero. El comportamiento con NULL diverge entre motores, y es justo lo que rompe el codigo al pasar de un motor a otro.

  • PostgreSQL: los argumentos NULL simplemente se descartan de la comparacion. GREATEST(5, NULL, 9) devuelve 9. Solo cuando todos y cada uno de los argumentos son NULL el resultado tambien es NULL.
  • MySQL: basta un solo NULL entre los argumentos para anular todo el resultado. GREATEST(5, NULL, 9) devuelve NULL en MySQL: un NULL envenena la expresion entera.
  • ClickHouse: semanticamente esta mas cerca de MySQL. Si aunque sea un argumento de tipo Nullable vale NULL, el resultado tambien es NULL. Aqui no puedes confiar en el salto de NULL al estilo de PostgreSQL, asi que no archives ClickHouse como una copia de PostgreSQL: protege los argumentos de forma explicita.
-- PostgreSQL: returns 9, NULL is ignored
-- MySQL and ClickHouse: return NULL, one NULL poisons the result
SELECT GREATEST(5, NULL, 9) AS demo;

Trampa: no cuentes con que "el NULL se descarta solo" al migrar codigo de PostgreSQL a MySQL o ClickHouse. Una logica que funciono en silencio durante anos puede empezar de repente a devolver NULL tras la migracion, y detectarlo a simple vista en una consulta grande es casi imposible. Si una columna puede ser NULL, envuelvela en COALESCE para que el comportamiento sea explicito e identico en todas partes.

-- Portable: define a neutral fallback before comparing
SELECT
    id,
    GREATEST(COALESCE(amount, 0), 0) AS amount_nonneg
FROM orders;

Combinaciones utiles

GREATEST y LEAST encajan mas alla del SELECT. Dentro de un UPDATE y en expresiones calculadas te ahorran bloques CASE voluminosos.

-- Bump salary by 10% but never below the floor of 40000
UPDATE employees
SET salary = GREATEST(40000, salary * 1.10)
WHERE dept = 'sales';
-- Days since the more recent of signup or first order
SELECT
    u.id,
    CURRENT_DATE - GREATEST(u.created_at, o.created_at)::date AS days_idle
FROM users u
JOIN orders o ON o.user_id = u.id;

En resumen: GREATEST y LEAST comparan valores en horizontal, dentro de una fila; acotan un numero a un rango con elegancia mediante una llamada anidada; y exigen una proteccion explicita con COALESCE si el mismo codigo debe comportarse igual en PostgreSQL, MySQL y ClickHouse. Aprende la diferencia de NULL una vez y te ahorraras una tarde de depuracion mas adelante.

Practica con ejercicios reales

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

Abrir el entrenador