PostgreSQL: Funções de Janela no PostgreSQL
Última atualização: 2026-08-26
1. O Que Você Vai Aprender
- A cláusula OVER e o modelo de execução das funções de janela
- PARTITION BY para particionamento e ORDER BY para ordenação
- As três especificações de quadro: ROWS / RANGE / GROUPS
- Funções de classificação: ROW_NUMBER / RANK / DENSE_RANK / NTILE
- Funções de deslocamento: LAG / LEAD / FIRST_VALUE / LAST_VALUE / NTH_VALUE
- Somas acumuladas e médias móveis
- A diferença fundamental entre funções de janela e funções de agregação
2. A História
Charlie é um analista de crescimento em uma plataforma SaaS. O gerente de produto pediu a ele para produzir um relatório de retenção de usuários:
- Contagem diária de novos usuários
- Taxa de retenção no dia 7
- Taxa de retenção no dia 30
- Classificação de inscrição de cada usuário (a N-ésima pessoa que se registrou no mesmo dia)
A abordagem tradicional precisa de várias instruções SQL mais tabelas temporárias. Após aprender funções de janela, Charlie obtém todas as estatísticas em uma única instrução SQL—sem GROUP BY para colapsar linhas; cada linha mantém seus dados originais junto com o resultado calculado.
3. Conceito: Fundamentos das Funções de Janela
(1) O Que É uma Função de Janela
Uma função de janela calcula sobre um conjunto de linhas relacionadas (a "janela") mas não colapsa linhas—cada linha recebe um resultado de volta. Esta é sua maior diferença em relação às funções de agregação.
| Característica | Função de agregação | Função de janela |
|---|---|---|
| Contagem de linhas | Muitas linhas → uma linha | Contagem de linhas inalterada |
| Sintaxe | SUM(col) |
SUM(col) OVER (...) |
| GROUP BY | Necessário | Não necessário |
| Mantém colunas originais | Não | Sim |
| Uso típico | Estatísticas de resumo | Classificação, deslocamento, totais acumulados |
(2) Estrutura da Cláusula OVER
nome_funcao() OVER (
[PARTITION BY expr]
[ORDER BY expr [ASC|DESC] [NULLS FIRST|NULLS LAST]]
[clausula_quadro]
)
flowchart TD
A[OVER] --> B[PARTITION BY]
B --> C[ORDER BY]
C --> D[Cláusula de Quadro]
D --> E{ROWS / RANGE / GROUPS}
E --> F[ROWS BETWEEN ... AND ...]
E --> G[RANGE BETWEEN ... AND ...]
E --> H[GROUPS BETWEEN ... AND ...]
B -.->|opcional| C
C -.->|opcional| D
style A fill:#e1f5fe
style B fill:#fff9c4
style C fill:#fff9c4
style D fill:#c8e6c9
▶ Exemplo: A Função de Janela Mais Simples
SELECT
order_id,
customer_id,
amount,
SUM(amount) OVER () AS total_all
FROM orders;
order_id | customer_id | amount | total_all
----------+-------------+--------+-----------
101 | 1 | 15000 | 750000
102 | 1 | 8000 | 750000
201 | 2 | 25000 | 750000
Cada linha retorna o SUM de toda a tabela, e a contagem de linhas permanece inalterada.
4. Conceito: PARTITION BY e ORDER BY
(1) PARTITION BY para Particionamento
PARTITION BY divide os dados em "partições" independentes, e a função de janela calcula dentro de cada partição separadamente.
| Cláusula | Efeito | Analogia |
|---|---|---|
PARTITION BY col |
Particionar por coluna | Como GROUP BY, mas sem colapsar |
| Sem PARTITION BY | Toda a tabela é uma partição | Como GROUP BY sem coluna de agrupamento |
▶ Exemplo: Soma Particionada por Cliente
SELECT
order_id,
customer_id,
amount,
SUM(amount) OVER (PARTITION BY customer_id) AS customer_total
FROM orders
ORDER BY customer_id, order_id;
order_id | customer_id | amount | customer_total
----------+-------------+--------+----------------
101 | 1 | 15000 | 23000
102 | 1 | 8000 | 23000
201 | 2 | 25000 | 25000
301 | 3 | 12000 | 57000
302 | 3 | 45000 | 57000
(2) ORDER BY para Ordenação
ORDER BY determina a ordenação das linhas dentro de uma partição e é essencial para funções de classificação e deslocamento.
| Cenário | Precisa de ORDER BY? | Motivo |
|---|---|---|
| ROW_NUMBER / RANK | Necessário | Classificação depende da ordem |
| LAG / LEAD | Necessário | Linhas anterior/posterior dependem da ordem |
| SUM() OVER (PARTITION BY) | Opcional | Sem ordem, calcula o total da partição |
| FIRST_VALUE / LAST_VALUE | Necessário | Primeiro/último valor depende da ordem |
▶ Exemplo: Classificação por Valor
SELECT
order_id,
customer_id,
amount,
ROW_NUMBER() OVER (ORDER BY amount DESC) AS rn_desc
FROM orders;
Output:
id | name | value
----+----------+-------
1 | example | 42
(1 row)
(3) Combinando PARTITION BY + ORDER BY
Particione primeiro, depois ordene; a classificação é calculada independentemente dentro de cada partição.
▶ Exemplo: Classificação de Valor de Pedido por Cliente
SELECT
order_id,
customer_id,
amount,
ROW_NUMBER() OVER (
PARTITION BY customer_id
ORDER BY amount DESC
) AS rank_in_customer
FROM orders;
order_id | customer_id | amount | rank_in_customer
----------+-------------+--------+------------------
102 | 1 | 8000 | 2
101 | 1 | 15000 | 1
201 | 2 | 25000 | 1
302 | 3 | 45000 | 1
301 | 3 | 12000 | 2
5. Conceito: Cláusula de Quadro
(1) Os Três Tipos de Quadro
A cláusula de quadro decide quais linhas uma função de janela "vê" para seu cálculo.
| Tipo de quadro | Limite baseado em | Melhor para |
|---|---|---|
| ROWS | Deslocamento físico de linha | Controle preciso de linha, ex.: "as 3 linhas anteriores" |
| RANGE | Deslocamento lógico de valor | Linhas com mesmo valor agrupadas, ex.: "mesmo valor" |
| GROUPS | Deslocamento de grupo de mesmo valor | Específico do PostgreSQL; agrupa por valor ORDER BY |
(2) Palavras-Chave de Limite de Quadro
| Palavra-chave | Significado |
|---|---|
UNBOUNDED PRECEDING |
Primeira linha da partição |
UNBOUNDED FOLLOWING |
Última linha da partição |
CURRENT ROW |
A linha atual |
N PRECEDING |
N linhas / valores antes |
N FOLLOWING |
N linhas / valores depois |
(3) Regras de Quadro Padrão
| Tem ORDER BY? | Quadro padrão |
|---|---|
| Sim | RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW |
| Não | ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING |
▶ Exemplo: Soma Acumulada com Quadro ROWS
SELECT
order_date,
amount,
SUM(amount) OVER (
ORDER BY order_date
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
) AS running_sum
FROM daily_sales;
order_date | amount | running_sum
-------------+--------+-------------
2025-01-01 | 5000 | 5000
2025-01-02 | 8000 | 13000
2025-01-03 | 3000 | 16000
2025-01-04 | 12000 | 28000
▶ Exemplo: Média Móvel de 3 Linhas com ROWS
SELECT
order_date,
amount,
ROUND(AVG(amount) OVER (
ORDER BY order_date
ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING
), 2) AS moving_avg_3
FROM daily_sales;
Output:
result
----------
42.50
(1 row)
▶ Exemplo: Quadro RANGE sobre um Intervalo de Data
SELECT
order_date,
amount,
SUM(amount) OVER (
ORDER BY order_date
RANGE BETWEEN INTERVAL '7 days' PRECEDING AND CURRENT ROW
) AS sum_last_7_days
FROM daily_sales;
Output:
result
----------
42.50
(1 row)
6. Conceito: Funções de Classificação
(1) Comparação das Quatro Funções de Classificação
| Função | Tratamento de mesmo valor | Saída | Consecutiva? |
|---|---|---|---|
| ROW_NUMBER | Estritamente crescente | 1,2,3,4 | Sim |
| RANK | Mesma classificação, pula | 1,1,3,4 | Não |
| DENSE_RANK | Mesma classificação, não pula | 1,1,2,3 | Sim |
| NTILE(N) | Divide em N grupos | 1,1,2,2,3,3 | — |
▶ Exemplo: ROW_NUMBER vs RANK vs DENSE_RANK
SELECT
name,
score,
ROW_NUMBER() OVER (ORDER BY score DESC) AS rn,
RANK() OVER (ORDER BY score DESC) AS rnk,
DENSE_RANK() OVER (ORDER BY score DESC) AS drnk
FROM students;
name | score | rn | rnk | drnk
--------+-------+----+-----+------
Alice | 95 | 1 | 1 | 1
Bob | 90 | 2 | 2 | 2
Charlie| 90 | 3 | 2 | 2
Dave | 85 | 4 | 4 | 3
(2) Agrupamento NTILE
NTILE(N) divide linhas ordenadas em N grupos aproximadamente iguais, comumente usado para análise de quartis.
▶ Exemplo: Dividir Clientes em 4 Grupos por Gasto
SELECT
customer_id,
total_spent,
NTILE(4) OVER (ORDER BY total_spent DESC) AS quartile
FROM customer_summary;
customer_id | total_spent | quartile
-------------+-------------+----------
12 | 500000 | 1
5 | 350000 | 1
8 | 280000 | 2
19 | 150000 | 2
3 | 90000 | 3
22 | 60000 | 3
41 | 25000 | 4
15 | 5000 | 4
7. Conceito: Funções de Deslocamento e Valor
(1) Referência de Funções de Deslocamento
| Função | Efeito | Uso típico |
|---|---|---|
| LAG(col, N, padrao) | Valor da N-ésima linha antes da atual | Crescimento período a período |
| LEAD(col, N, padrao) | Valor da N-ésima linha depois da atual | Previsão, comparação |
| FIRST_VALUE(col) | Primeiro valor na janela | Valor do primeiro pedido |
| LAST_VALUE(col) | Último valor na janela | Valor do último pedido |
| NTH_VALUE(col, N) | N-ésimo valor na janela | O N-ésimo pedido |
▶ Exemplo: LAG para Variação Dia a Dia
SELECT
order_date,
daily_revenue,
LAG(daily_revenue, 1) OVER (ORDER BY order_date) AS prev_day,
daily_revenue - LAG(daily_revenue, 1) OVER (ORDER BY order_date) AS diff,
ROUND(
(daily_revenue - LAG(daily_revenue, 1) OVER (ORDER BY order_date))
* 100.0 / NULLIF(LAG(daily_revenue, 1) OVER (ORDER BY order_date), 0),
2) AS pct_change
FROM daily_revenue
ORDER BY order_date;
Output:
id | name | value
----+----------+-------
1 | example | 42
(1 row)
▶ Exemplo: LEAD para Olhar o Próximo Mês
SELECT
month,
revenue,
LEAD(revenue, 1) OVER (ORDER BY month) AS next_month_revenue
FROM monthly_revenue;
Output:
id | name | value
----+----------+-------
1 | example | 42
(1 row)
(2) Observações sobre FIRST_VALUE / LAST_VALUE
O quadro padrão de LAST_VALUE é RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW, não a partição inteira. Você deve especificar o quadro explicitamente para obter a última linha da partição.
| Função | Quadro padrão | Como obter a última linha da partição |
|---|---|---|
| FIRST_VALUE | Até CURRENT ROW (coincidentemente correto) | Sem necessidade de alteração |
| LAST_VALUE | Até CURRENT ROW (NÃO é a última linha!) | Adicione ROWS BETWEEN ... AND UNBOUNDED FOLLOWING |
▶ Exemplo: FIRST_VALUE e LAST_VALUE
SELECT
order_id,
customer_id,
amount,
FIRST_VALUE(amount) OVER (
PARTITION BY customer_id ORDER BY order_date
) AS first_order_amount,
LAST_VALUE(amount) OVER (
PARTITION BY customer_id ORDER BY order_date
ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
) AS last_order_amount
FROM orders;
Output:
id | name | value
----+----------+-------
1 | example | 42
(1 row)
▶ Exemplo: NTH_VALUE para o 2º Pedido
SELECT
order_id,
customer_id,
amount,
NTH_VALUE(amount, 2) OVER (
PARTITION BY customer_id ORDER BY order_date
ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
) AS second_order_amount
FROM orders;
Output:
id | name | value
----+----------+-------
1 | example | 42
(1 row)
8. Conceito: Soma Acumulada e Média Móvel
(1) Soma Acumulada
| Quadro | Significado | SQL |
|---|---|---|
| Quadro padrão | Do início da partição até a linha atual | SUM() OVER (ORDER BY col) |
| ROWS explícito | Igual ao acima | ROWS UNBOUNDED PRECEDING |
| RANGE explícito | Linhas de mesmo valor agrupadas | RANGE UNBOUNDED PRECEDING |
▶ Exemplo: Receita Acumulada Mensal
SELECT
month,
revenue,
SUM(revenue) OVER (ORDER BY month) AS running_revenue
FROM monthly_revenue;
month | revenue | running_revenue
----------+---------+----------------
2025-01 | 500000 | 500000
2025-02 | 620000 | 1120000
2025-03 | 580000 | 1700000
2025-04 | 710000 | 2410000
(2) Média Móvel
▶ Exemplo: Média Móvel de 7 Dias
SELECT
order_date,
daily_revenue,
ROUND(AVG(daily_revenue) OVER (
ORDER BY order_date
ROWS BETWEEN 6 PRECEDING AND CURRENT ROW
), 2) AS ma_7day
FROM daily_revenue;
Output:
result
----------
42.50
(1 row)
(3) Soma Acumulada Particionada
▶ Exemplo: Gasto Acumulado por Cliente
SELECT
order_id,
customer_id,
order_date,
amount,
SUM(amount) OVER (
PARTITION BY customer_id
ORDER BY order_date
) AS cumulative_spent
FROM orders
ORDER BY customer_id, order_date;
Output:
result
----------
42.50
(1 row)
9. Funções de Janela vs Funções de Agregação
| Dimensão | Função de agregação | Função de janela |
|---|---|---|
| Contagem de linhas | Colapsada para uma linha | Contagem original de linhas preservada |
| Sintaxe | SUM(col) |
SUM(col) OVER(...) |
| GROUP BY | Necessário | Não necessário |
| Cálculo por linha | Uma dimensão | Múltiplas janelas diferentes possíveis |
| Desempenho | Geralmente mais rápido | Precisa de ordenação + particionamento, ligeiramente mais lento |
| Melhor para | Relatórios de resumo | Classificação, deslocamento, totais acumulados |
▶ Exemplo: Estilos de Escrita Equivalentes Comparados
Estilo de função de agregação:
SELECT customer_id, SUM(amount) AS total
FROM orders
GROUP BY customer_id;
Output:
result
----------
42.50
(1 row)
Estilo de função de janela (mantém linhas originais):
SELECT
order_id,
customer_id,
amount,
SUM(amount) OVER (PARTITION BY customer_id) AS customer_total
FROM orders;
10. Exemplo Completo
Análise de retenção de usuários de Charlie—contando novos usuários diários e suas taxas de retenção no Dia 7 / Dia 30:
WITH first_login AS (
SELECT
user_id,
MIN(login_date) AS first_date,
ROW_NUMBER() OVER (PARTITION BY MIN(login_date) ORDER BY user_id) AS reg_rank
FROM user_logins
GROUP BY user_id
),
daily_new_users AS (
SELECT
first_date AS cohort_date,
COUNT(*) AS new_users
FROM first_login
GROUP BY first_date
),
retention_base AS (
SELECT
fl.first_date AS cohort_date,
fl.user_id,
ul.login_date,
ul.login_date - fl.first_date AS day_offset
FROM first_login fl
JOIN user_logins ul ON fl.user_id = ul.user_id
),
retention_count AS (
SELECT
cohort_date,
day_offset,
COUNT(DISTINCT user_id) AS retained_users
FROM retention_base
WHERE day_offset IN (0, 7, 30)
GROUP BY cohort_date, day_offset
)
SELECT
r.cohort_date,
n.new_users,
MAX(CASE WHEN r.day_offset = 0 THEN r.retained_users END) AS d0,
MAX(CASE WHEN r.day_offset = 7 THEN r.retained_users END) AS d7,
MAX(CASE WHEN r.day_offset = 30 THEN r.retained_users END) AS d30,
ROUND(
MAX(CASE WHEN r.day_offset = 7 THEN r.retained_users END) * 100.0
/ NULLIF(n.new_users, 0), 1
) AS day7_rate,
ROUND(
MAX(CASE WHEN r.day_offset = 30 THEN r.retained_users END) * 100.0
/ NULLIF(n.new_users, 0), 1
) AS day30_rate
FROM retention_count r
JOIN daily_new_users n ON r.cohort_date = n.cohort_date
GROUP BY r.cohort_date, n.new_users
ORDER BY r.cohort_date;
11. Ordem de Execução
Onde as funções de janela se situam na ordem de execução SQL:
flowchart TD
A[FROM] --> B[WHERE]
B --> C[GROUP BY]
C --> D[HAVING]
D --> E["Funções de Janela<br/>OVER / PARTITION / ORDER / FRAME"]
E --> F[SELECT]
F --> G[DISTINCT]
G --> H[ORDER BY]
H --> I[LIMIT]
style E fill:#c8e6c9
style D fill:#fff9c4
| Passo | Cláusula | Descrição |
|---|---|---|
| 1 | FROM | Determinar a fonte de dados |
| 2 | WHERE | Filtrar linhas |
| 3 | GROUP BY | Agrupamento de agregação |
| 4 | HAVING | Filtrar grupos |
| 5 | Funções de janela | Calcular sobre o resultado filtrado |
| 6 | SELECT | Escolher colunas de saída |
| 7 | DISTINCT | Desduplicar |
| 8 | ORDER BY | Ordenação final |
| 9 | LIMIT | Limitar linhas |
❓ Perguntas Frequentes
P: Funções de janela podem aparecer na cláusula WHERE? R: Não. Funções de janela executam depois de WHERE, então WHERE não pode referenciar seus resultados. Envolva-as em uma subconsulta ou CTE e filtre lá.
P: Qual a diferença entre ROW_NUMBER e RANK? R: ROW_NUMBER é estritamente crescente (1,2,3,4); RANK dá a mesma classificação para valores iguais e pula (1,1,3,4). Para Top-1 desduplicado, use ROW_NUMBER.
P: Por que LAST_VALUE não retorna a última linha da partição? R: O quadro padrão é RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW. Você deve escrever explicitamente ROWS BETWEEN ... AND UNBOUNDED FOLLOWING.
P: Múltiplas funções de janela podem compartilhar uma cláusula OVER? R: Sim. O PostgreSQL suporta o alias da cláusula WINDOW:
WINDOW w AS (PARTITION BY ...), então escrevaOVER wem múltiplas funções.
P: Qual a diferença entre quadros ROWS e RANGE? R: ROWS desloca por linhas físicas; RANGE desloca por valor lógico. RANGE trata linhas com o mesmo valor ORDER BY como um limite. A maioria dos casos de soma acumulada usa ROWS.
P: Como otimizar o desempenho das funções de janela? R: Certifique-se de que há índices nas colunas PARTITION BY + ORDER BY; reduza o número de partições; evite cálculos complexos de quadro em partições grandes; filtre primeiro com uma CTE, depois calcule a janela.
📖 Resumo
- Funções de janela não colapsam linhas; cada linha retorna um resultado, e a cláusula OVER define a janela
- PARTITION BY particiona, ORDER BY ordena, e o quadro controla o escopo do cálculo
- ROW_NUMBER / RANK / DENSE_RANK se comportam de maneira diferente; escolha com cuidado
- LAG / LEAD acessam linhas anteriores/posteriores; cuidado com os quadros padrão de FIRST_VALUE / LAST_VALUE
- ROWS desloca por linha física, RANGE por valor lógico, GROUPS por valor agrupado
- Somas acumuladas e médias móveis são casos de uso típicos de funções de janela
- Funções de janela executam depois de WHERE/GROUP BY e não podem ser usadas em WHERE
- A cláusula WINDOW reutiliza definições OVER, reduzindo duplicação
📝 Exercícios
-
⭐ Use ROW_NUMBER para consultar os 2 pedidos de maior valor de cada cliente.
-
⭐ Use LAG para calcular a diferença de valor entre pedidos adjacentes para cada cliente.
-
⭐⭐ Calcule a média móvel de 7 dias da receita diária por região.
-
⭐⭐ Use DENSE_RANK e NTILE(4) para dividir clientes em 4 níveis por gasto total.
-
⭐⭐⭐ Escreva uma única instrução SQL que calcule a taxa de retenção no Dia 1 / Dia 7 / Dia 30 de novos usuários diários, usando funções de janela em vez de self-joins.