sqlpostgresqlmysqlfunctions

GREATEST e LEAST no SQL: maximo e minimo por linha entre colunas

Como GREATEST e LEAST retornam o maximo e o minimo dentro de uma linha, limitam um valor a um intervalo e tratam NULL de forma diferente em PostgreSQL, MySQL e ClickHouse.

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

GREATEST e LEAST recebem uma lista de argumentos e retornam o maior ou o menor entre eles dentro de uma mesma linha. Voce recorre a eles quando precisa comparar nao linhas entre si, mas varias colunas ou expressoes dentro da linha atual: escolher a mais recente entre duas datas, colocar um piso ou um teto em um numero, ou prender um valor dentro de um intervalo. Nao sao funcoes de agregacao: em vez de agrupar linhas, olham as colunas lado a lado, na horizontal, e emitem um valor por linha.

As duas funcoes compartilham a mesma assinatura: a entrada e uma lista de duas ou mais expressoes de tipo comparavel e a saida e um unico valor desse mesmo tipo. GREATEST retorna o maximo da lista e LEAST, o minimo. Voce pode misturar os argumentos a vontade (colunas, literais, resultados de outras chamadas), por isso as duas funcoes se aninham com tanta facilidade uma dentro da outra.

Convem verificar antes duas coisas: os tipos e o NULL. Os tipos precisam ser comparaveis entre si, ou o PostgreSQL os converte para um tipo comum ou se recusa a executar a consulta; e um NULL na lista de argumentos e tratado de forma diferente em cada banco, que e justamente onde uma consulta migrada costuma quebrar.

Como isso difere de MAX e MIN

A confusao nasce das palavras: um "maximo" e um "maximo". Mas as duas familias trabalham em direcoes diferentes. MAX e MIN sao agregados: percorrem verticalmente as linhas de um grupo e retornam um unico numero para todo o resultado. GREATEST e LEAST percorrem horizontalmente os argumentos de uma unica linha e retornam um valor por linha.

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

Um caso classico e escolher a data mais recente entre varias diretamente na linha, sem subconsultas nem funcoes de janela:

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;

Limitar um valor a um intervalo (clamp)

O truque mais pratico com essas funcoes e empurrar um numero para um corredor entre um limite inferior e um superior. Isso se consegue aninhando uma chamada dentro da outra: GREATEST(lo, LEAST(hi, x)) garante que o resultado nunca escape de [lo, hi]. A formula se le de dentro para fora: primeiro corta por cima, depois eleva por baixo.

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

E indispensavel onde um valor precisa se manter dentro de limites: descontos, pontos de fidelidade, normalizacao de salarios para relatorios.

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

Vejamos quem faz o que:

  • o GREATEST(lo, ...) externo eleva ate lo os valores pequenos demais;
  • o LEAST(hi, x) interno reduz ate hi os valores grandes demais;
  • a ordem dos limites e critica: se voce definir lo > hi por acidente, o corredor colapsa em uma contradicao e voce obtem um resultado sem sentido, em silencio e sem erro.

Tratamento de NULL: a principal armadilha

Aqui comeca a parte realmente traicoeira. O comportamento com NULL diverge entre os bancos, e e justamente o que quebra o codigo ao mudar de um banco para outro.

  • PostgreSQL: argumentos NULL sao simplesmente descartados da comparacao. GREATEST(5, NULL, 9) retorna 9. So quando todos os argumentos, sem excecao, sao NULL o resultado tambem e NULL.
  • MySQL: basta um unico NULL entre os argumentos para anular todo o resultado. GREATEST(5, NULL, 9) retorna NULL no MySQL: um NULL envenena a expressao inteira.
  • ClickHouse: semanticamente esta mais perto do MySQL. Se ao menos um argumento do tipo Nullable for NULL, o resultado tambem e NULL. Aqui voce nao pode contar com o descarte de NULL no estilo do PostgreSQL, entao nao guarde o ClickHouse como uma copia do PostgreSQL: proteja os 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;

Pegadinha: nao conte com o "o NULL se descarta sozinho" ao migrar codigo do PostgreSQL para o MySQL ou o ClickHouse. Uma logica que funcionou em silencio por anos pode comecar de repente a retornar NULL apos a migracao, e detectar isso a olho nu em uma consulta grande e quase impossivel. Se uma coluna pode ser NULL, envolva-a em COALESCE para que o comportamento seja explicito e identico em todos os lugares.

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

Combinacoes uteis

GREATEST e LEAST se encaixam alem do SELECT. Dentro de um UPDATE e em expressoes calculadas, eles poupam voce de blocos CASE volumosos.

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

Em resumo: GREATEST e LEAST comparam valores na horizontal, dentro de uma linha; limitam um numero a um intervalo de forma elegante por meio de uma chamada aninhada; e exigem protecao explicita com COALESCE se o mesmo codigo precisa se comportar igual no PostgreSQL, no MySQL e no ClickHouse. Aprenda a diferenca de NULL uma vez e voce economizara uma noite de depuracao mais tarde.

Pratique com exercícios reais

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

Abrir o treinador