Em relatórios SQL, é comum precisarmos calcular diferentes totais a partir do mesmo conjunto de dados.

Imagine uma tabela de pedidos com a seguinte estrutura:

CREATE TABLE pedidos (
    id          bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    cliente_id  bigint NOT NULL,
    status      text NOT NULL,
    valor       numeric(12, 2) NOT NULL,
    criado_em   timestamp NOT NULL DEFAULT current_timestamp
);

Queremos apresentar, em uma única consulta:

  • quantidade total de pedidos;
  • pedidos pagos;
  • pedidos pendentes;
  • pedidos cancelados;
  • valor total dos pedidos pagos.

A solução tradicional com CASE

Uma forma comum de resolver o problema é colocar expressões CASE dentro das funções de agregação:

SELECT
    COUNT(*) AS total_pedidos,

    SUM(
        CASE
            WHEN status = 'pago' THEN 1
            ELSE 0
        END
    ) AS pedidos_pagos,

    SUM(
        CASE
            WHEN status = 'pendente' THEN 1
            ELSE 0
        END
    ) AS pedidos_pendentes,

    SUM(
        CASE
            WHEN status = 'cancelado' THEN 1
            ELSE 0
        END
    ) AS pedidos_cancelados,

    SUM(
        CASE
            WHEN status = 'pago' THEN valor
            ELSE 0
        END
    ) AS valor_pago
FROM pedidos;

A consulta funciona, mas se torna repetitiva à medida que novos indicadores são adicionados.

Utilizando FILTER

No PostgreSQL, podemos aplicar uma condição diretamente sobre cada agregação com a cláusula FILTER:

SELECT
    COUNT(*) AS total_pedidos,

    COUNT(*) FILTER (
        WHERE status = 'pago'
    ) AS pedidos_pagos,

    COUNT(*) FILTER (
        WHERE status = 'pendente'
    ) AS pedidos_pendentes,

    COUNT(*) FILTER (
        WHERE status = 'cancelado'
    ) AS pedidos_cancelados,

    COALESCE(
        SUM(valor) FILTER (
            WHERE status = 'pago'
        ),
        0
    ) AS valor_pago
FROM pedidos;

A intenção de cada coluna fica mais clara: executar determinada agregação apenas sobre as linhas que atendem à condição.

Sintaxe geral

A estrutura básica é:

funcao_agregadora(expressao)
FILTER (WHERE condicao)

A cláusula pode ser usada com funções como:

  • COUNT;
  • SUM;
  • AVG;
  • MIN;
  • MAX;
  • ARRAY_AGG;
  • STRING_AGG;
  • agregações definidas pelo usuário.

Exemplo agrupado por cliente

Também podemos combinar FILTER com GROUP BY:

SELECT
    cliente_id,

    COUNT(*) AS total_pedidos,

    COUNT(*) FILTER (
        WHERE status = 'pago'
    ) AS pedidos_pagos,

    COUNT(*) FILTER (
        WHERE status = 'pendente'
    ) AS pedidos_pendentes,

    COALESCE(
        SUM(valor) FILTER (
            WHERE status = 'pago'
        ),
        0
    ) AS valor_pago
FROM pedidos
GROUP BY cliente_id
ORDER BY cliente_id;

Nesse caso, cada cliente terá seus próprios indicadores.

O COALESCE foi utilizado porque SUM retorna NULL quando nenhuma linha atende ao filtro. Para relatórios, muitas vezes é mais conveniente apresentar zero.

FILTER com intervalo de datas

As condições não precisam se limitar a uma única coluna.

Podemos calcular valores referentes a períodos diferentes:

SELECT
    COUNT(*) FILTER (
        WHERE criado_em >= current_date
    ) AS pedidos_hoje,

    COUNT(*) FILTER (
        WHERE criado_em >= current_timestamp - interval '7 days'
    ) AS pedidos_ultimos_7_dias,

    COUNT(*) FILTER (
        WHERE criado_em >= date_trunc('month', current_date)
    ) AS pedidos_no_mes
FROM pedidos;

Uma única consulta produz diferentes indicadores temporais sem repetir subconsultas.

FILTER com window functions

A cláusula também pode ser utilizada em agregações executadas como funções de janela.

SELECT
    id,
    cliente_id,
    status,
    valor,

    COUNT(*) FILTER (
        WHERE status = 'pago'
    ) OVER (
        PARTITION BY cliente_id
    ) AS total_pagos_cliente
FROM pedidos;

A consulta mantém cada pedido no resultado, mas acrescenta a quantidade de pedidos pagos do respectivo cliente.

Qual é o ganho?

O principal ganho é a legibilidade.

Compare:

SUM(CASE WHEN status = 'pago' THEN valor ELSE 0 END)

com:

SUM(valor) FILTER (WHERE status = 'pago')

Na segunda forma, a agregação e sua condição ficam explicitamente separadas.

Isso facilita:

  • leitura;
  • revisão;
  • manutenção;
  • inclusão de novos indicadores;
  • redução de expressões repetitivas.

Quando utilizar

FILTER é especialmente útil em:

  • dashboards;
  • relatórios gerenciais;
  • indicadores de status;
  • métricas por período;
  • tabelas de resumo;
  • consultas com várias agregações condicionais.

Ele não elimina todos os usos de CASE.

Quando a transformação precisa ocorrer dentro da própria expressão, ou quando cada condição deve retornar valores diferentes, CASE continua sendo adequado.

Mas, quando a necessidade é simplesmente restringir quais linhas participam de uma agregação, FILTER costuma expressar melhor a intenção.

Conclusão

A cláusula FILTER é um recurso simples, mas capaz de deixar consultas analíticas muito mais claras.

Em vez de repetir expressões CASE dentro de várias agregações, podemos declarar diretamente a condição de cada indicador:

COUNT(*) FILTER (WHERE condicao)

Para relatórios com múltiplas métricas calculadas sobre o mesmo conjunto de dados, esse recurso pode reduzir bastante o ruído da consulta e melhorar sua manutenção.

Referências