generate_series é uma das funções nativas mais úteis do PostgreSQL porque cria linhas sem precisar de uma tabela. Ela produz intervalos de inteiros, datas ou timestamps. Isso resolve relatórios que precisam exibir dias sem dados, dashboards com buckets fixos e ambientes de teste que precisam de dados realistas rapidamente.
A função em um minuto
-- Inteiros: início, fim, passo
SELECT generate_series(1, 10, 2);
-- Datas: uma linha por dia
SELECT generate_series(
DATE '2026-07-01',
DATE '2026-07-07',
INTERVAL '1 day'
)::date AS dia;
O resultado é um conjunto de linhas; use-o no FROM como usaria uma tabela.
Caso 1: exibir dias sem pedidos
Uma agregação comum só retorna datas que possuem linhas. Em um dashboard, isso faz um dia sem pedidos sumir, o que é enganoso. Gere primeiro o calendário esperado e depois faça LEFT JOIN com os fatos.
WITH dias AS (
SELECT generate_series(
DATE '2026-07-01',
DATE '2026-07-07',
INTERVAL '1 day'
)::date AS dia
)
SELECT
dias.dia,
COALESCE(COUNT(orders.id), 0) AS total_pedidos,
COALESCE(SUM(orders.total), 0) AS receita
FROM dias
LEFT JOIN orders
ON orders.created_at >= dias.dia
AND orders.created_at < dias.dia + INTERVAL '1 day'
GROUP BY dias.dia
ORDER BY dias.dia;
O filtro por intervalo é intencional. Não escreva DATE(orders.created_at) = dias.dia quando created_at está indexada: envolver a coluna em função pode impedir o uso do índice.
Caso 2: encontrar intervalos de métricas ausentes
Imagine que um coletor deveria gravar uma linha por minuto. Esta query mostra cada minuto em que isso não ocorreu.
WITH minutos_esperados AS (
SELECT generate_series(
TIMESTAMPTZ '2026-07-13 09:00:00+00',
TIMESTAMPTZ '2026-07-13 10:00:00+00',
INTERVAL '1 minute'
) AS minuto
)
SELECT minutos_esperados.minuto
FROM minutos_esperados
LEFT JOIN metric_samples
ON metric_samples.recorded_at >= minutos_esperados.minuto
AND metric_samples.recorded_at < minutos_esperados.minuto + INTERVAL '1 minute'
WHERE metric_samples.id IS NULL
ORDER BY minutos_esperados.minuto;
Dica de produção: limite o período. Gerar uma linha por segundo durante um ano cria mais de 31 milhões de linhas antes de sua query real começar.
Caso 3: agrupar eventos em janelas de cinco minutos
Para relatórios, gere todos os buckets e associe os eventos a eles. O resultado preserva buckets vazios, deixando gráficos e janelas de alerta corretos.
WITH buckets AS (
SELECT generate_series(
date_trunc('hour', now() - INTERVAL '1 hour'),
date_trunc('hour', now()),
INTERVAL '5 minutes'
) AS inicio_bucket
)
SELECT
inicio_bucket,
COUNT(events.id) AS total_eventos
FROM buckets
LEFT JOIN events
ON events.created_at >= inicio_bucket
AND events.created_at < inicio_bucket + INTERVAL '5 minutes'
GROUP BY inicio_bucket
ORDER BY inicio_bucket;
Caso 4: gerar dados de teste sem loop
SQL orientado a conjuntos é mais rápido e simples de limpar que loops na aplicação. Use valores determinísticos quando o teste precisar ser reproduzível.
INSERT INTO demo_orders (customer_id, total, created_at)
SELECT
((n - 1) % 100) + 1,
round((10 + random() * 490)::numeric, 2),
now() - (n || ' minutes')::interval
FROM generate_series(1, 10000) AS n;
Use apenas em desenvolvimento ou banco de teste isolado. Um gerador conveniente também consegue encher uma tabela de produção muito rápido.
Checklist
- Use
LEFT JOINeCOALESCEquando intervalos vazios precisam aparecer. - Associe timestamps com intervalo semiaberto:
>= início AND < fim. - Limite o período gerado e escolha um passo sensato.
- Indexe o timestamp do join, por exemplo
CREATE INDEX ON orders (created_at).
Em produção, uma lacuna costuma importar mais que uma média. O PG Monitoring pode mostrar janelas de métricas ausentes ao lado da query, agente ou condição de banco que as causou.