PostgreSQL: Funções de Janela no PostgreSQL

Última atualização: 2026-08-26

1. O Que Você Vai Aprender


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:

  1. Contagem diária de novos usuários
  2. Taxa de retenção no dia 7
  3. Taxa de retenção no dia 30
  4. 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

SQL
nome_funcao() OVER (
  [PARTITION BY expr]
  [ORDER BY expr [ASC|DESC] [NULLS FIRST|NULLS LAST]]
  [clausula_quadro]
)
100%
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

SQL
SELECT
  order_id,
  customer_id,
  amount,
  SUM(amount) OVER () AS total_all
FROM orders;
TEXT 📖 Somente leitura
 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

SQL
SELECT
  order_id,
  customer_id,
  amount,
  SUM(amount) OVER (PARTITION BY customer_id) AS customer_total
FROM orders
ORDER BY customer_id, order_id;
TEXT 📖 Somente leitura
 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

SQL
SELECT
  order_id,
  customer_id,
  amount,
  ROW_NUMBER() OVER (ORDER BY amount DESC) AS rn_desc
FROM orders;

Output:

TEXT 📖 Somente leitura
 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

SQL
SELECT
  order_id,
  customer_id,
  amount,
  ROW_NUMBER() OVER (
    PARTITION BY customer_id
    ORDER BY amount DESC
  ) AS rank_in_customer
FROM orders;
TEXT 📖 Somente leitura
 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

SQL
SELECT
  order_date,
  amount,
  SUM(amount) OVER (
    ORDER BY order_date
    ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
  ) AS running_sum
FROM daily_sales;
TEXT 📖 Somente leitura
 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

SQL
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:

TEXT 📖 Somente leitura
  result  
----------
   42.50
(1 row)

▶ Exemplo: Quadro RANGE sobre um Intervalo de Data

SQL
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:

TEXT 📖 Somente leitura
  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

SQL
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;
TEXT 📖 Somente leitura
 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

SQL
SELECT
  customer_id,
  total_spent,
  NTILE(4) OVER (ORDER BY total_spent DESC) AS quartile
FROM customer_summary;
TEXT 📖 Somente leitura
 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

SQL
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:

TEXT 📖 Somente leitura
 id | name     | value 
----+----------+-------
  1 | example  | 42
(1 row)

▶ Exemplo: LEAD para Olhar o Próximo Mês

SQL
SELECT
  month,
  revenue,
  LEAD(revenue, 1) OVER (ORDER BY month) AS next_month_revenue
FROM monthly_revenue;

Output:

TEXT 📖 Somente leitura
 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

SQL
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:

TEXT 📖 Somente leitura
 id | name     | value 
----+----------+-------
  1 | example  | 42
(1 row)

▶ Exemplo: NTH_VALUE para o 2º Pedido

SQL
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:

TEXT 📖 Somente leitura
 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

SQL
SELECT
  month,
  revenue,
  SUM(revenue) OVER (ORDER BY month) AS running_revenue
FROM monthly_revenue;
TEXT 📖 Somente leitura
  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

SQL
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:

TEXT 📖 Somente leitura
  result  
----------
   42.50
(1 row)

(3) Soma Acumulada Particionada

▶ Exemplo: Gasto Acumulado por Cliente

SQL
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:

TEXT 📖 Somente leitura
  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:

SQL
SELECT customer_id, SUM(amount) AS total
FROM orders
GROUP BY customer_id;

Output:

TEXT 📖 Somente leitura
  result  
----------
   42.50
(1 row)

Estilo de função de janela (mantém linhas originais):

SQL
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:

SQL
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:

100%
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 escreva OVER w em 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


📝 Exercícios

  1. ⭐ Use ROW_NUMBER para consultar os 2 pedidos de maior valor de cada cliente.

  2. ⭐ Use LAG para calcular a diferença de valor entre pedidos adjacentes para cada cliente.

  3. ⭐⭐ Calcule a média móvel de 7 dias da receita diária por região.

  4. ⭐⭐ Use DENSE_RANK e NTILE(4) para dividir clientes em 4 níveis por gasto total.

  5. ⭐⭐⭐ 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.

Web-Tutorial.com

Equipe Técnica Web-Tutorial

Uma plataforma de tutoriais mantida por diversos desenvolvedores. Cada tutorial é escrito e revisado por profissionais da área correspondente. Trabalhamos para manter nosso conteúdo preciso e confiável — se encontrar algum problema, avise-nos.

100%