Capa do Ebook gratuito SQL para Análise de Dados no Dia a Dia: Consultas, Relatórios e Insights com Dados Reais

SQL para Análise de Dados no Dia a Dia: Consultas, Relatórios e Insights com Dados Reais

Novo curso

26 páginas

Funções de data para períodos, calendários e comparações temporais

Capítulo 8

Tempo estimado de leitura: 0 minutos

+ Exercício

Por que funções de data são essenciais em análises do dia a dia

Grande parte das perguntas de negócio envolve tempo: vendas por mês, comparação com o mesmo período do ano anterior, retenção em 7/30 dias, atrasos de entrega, sazonalidade por dia da semana, metas por trimestre, entre outras. Para responder com consistência, você precisa transformar datas em “períodos” (dia, semana, mês, trimestre), alinhar calendários (ex.: semanas que começam na segunda), e criar comparações temporais (ex.: mês atual vs mês anterior) sem cair em armadilhas como intervalos incompletos, fusos horários e limites de período.

Neste capítulo, o foco é dominar padrões de consulta com funções de data para: (1) criar colunas de período (calendarização), (2) filtrar intervalos corretamente, (3) calcular diferenças entre datas, (4) comparar períodos equivalentes e (5) lidar com datas/hora e timezone. Os exemplos usam SQL “genérico”, mas cada banco pode ter nomes diferentes para as funções. Onde houver variação, a ideia é entender o padrão e adaptar.

Conceitos fundamentais: data, timestamp, período e granularidade

Tipos comuns: DATE vs TIMESTAMP

DATE representa apenas o dia (sem hora). TIMESTAMP/DATETIME representa dia e hora (e às vezes fuso). Em análises, isso impacta filtros e agregações: se você agrupa por dia, um timestamp precisa ser “truncado” para o dia; se você filtra um mês, precisa garantir que o intervalo inclua todas as horas do mês.

Granularidade e truncamento

Granularidade é o nível de detalhe temporal: por hora, por dia, por semana, por mês. “Truncar” uma data/hora significa reduzir para o início do período (ex.: 2026-01-09 14:33 vira 2026-01-09 para dia; ou 2026-01-01 para mês). Esse padrão é a base para relatórios consistentes.

Calendário x período móvel

Período de calendário é algo como “mês de janeiro”, “trimestre Q1”, “semana ISO”. Período móvel é “últimos 7 dias”, “últimos 30 dias”, “últimas 12 semanas”. Ambos são úteis, mas respondem perguntas diferentes. Períodos móveis tendem a ser melhores para monitoramento contínuo; períodos de calendário são melhores para fechamento e comparações formais.

Continue em nosso aplicativo

Você poderá ouvir o audiobook com a tela desligada, ganhar gratuitamente o certificado deste curso e ainda ter acesso a outros 5.000 cursos online gratuitos.

ou continue lendo abaixo...
Download App

Baixar o aplicativo

Funções e padrões mais usados (com equivalências)

Os nomes variam por banco, mas os padrões são recorrentes:

  • Data atual: CURRENT_DATE (ou CAST(CURRENT_TIMESTAMP AS DATE)).
  • Timestamp atual: CURRENT_TIMESTAMP / NOW().
  • Truncar para período: DATE_TRUNC('month', ts) (PostgreSQL/BigQuery), TRUNC(date, 'MM') (Oracle), DATETRUNC(MONTH, dt) (SQL Server), DATE_FORMAT/STRFTIME (MySQL/SQLite via formatação).
  • Extrair parte: EXTRACT(YEAR FROM dt), EXTRACT(DOW FROM dt), YEAR(dt), MONTH(dt).
  • Somar/subtrair tempo: dt + INTERVAL '7 day', DATEADD(day, 7, dt), dt + INTERVAL 7 DAY.
  • Diferença entre datas: DATE_DIFF(end, start, DAY), DATEDIFF(day, start, end), end - start (alguns bancos retornam intervalo).
  • Último dia do mês: LAST_DAY(dt) (MySQL/Oracle), EOMONTH(dt) (SQL Server), ou cálculo via truncamento + 1 mês - 1 dia.

Ao escrever consultas, prefira padrões que evitem dependência de formatação textual de datas. Formatar para string (ex.: '2026-01') é útil para exibição, mas para agrupamento e filtros o ideal é trabalhar com tipos de data.

Passo a passo: criar períodos de calendário para relatórios

Passo 1 — Normalizar o timestamp para a data (quando necessário)

Se sua tabela tem um timestamp (ex.: created_at), e você quer analisar por dia, primeiro transforme em DATE. Isso evita que horas diferentes criem “dias” diferentes por erro de fuso ou por truncamento inconsistente.

SELECT  CAST(created_at AS DATE) AS dia,  COUNT(*) AS qtd_pedidosFROM pedidosGROUP BY CAST(created_at AS DATE)ORDER BY dia;

Em bancos com função específica, pode ser DATE(created_at) ou CONVERT(date, created_at). O objetivo é o mesmo: obter uma coluna “dia” estável.

Passo 2 — Truncar para mês, semana e trimestre

Para relatórios mensais, crie uma coluna “mes_inicio” (primeiro dia do mês). Isso facilita join com calendário e comparações.

SELECT  DATE_TRUNC('month', created_at) AS mes_inicio,  SUM(valor_total) AS receitaFROM pedidosGROUP BY DATE_TRUNC('month', created_at)ORDER BY mes_inicio;

Para trimestre:

SELECT  DATE_TRUNC('quarter', created_at) AS trimestre_inicio,  COUNT(*) AS pedidosFROM pedidosGROUP BY DATE_TRUNC('quarter', created_at)ORDER BY trimestre_inicio;

Para semana, defina qual padrão você usa. Em muitos contextos corporativos, semana começa na segunda (ISO). Se seu banco suportar DATE_TRUNC('week', ...), confirme se ele segue domingo ou segunda. Quando não houver suporte direto, uma alternativa é “ancorar” a semana subtraindo o dia da semana.

SELECT  (CAST(created_at AS DATE) - (EXTRACT(DOW FROM created_at) * INTERVAL '1 day'))::date AS semana_inicio,  COUNT(*) AS pedidosFROM pedidosGROUP BY 1ORDER BY 1;

Esse exemplo é ilustrativo (PostgreSQL). Em outros bancos, a lógica muda, mas a ideia é: calcular o início da semana de forma determinística.

Passo 3 — Exibir rótulos (sem perder o tipo)

Você pode manter a coluna de período como DATE/TIMESTAMP para ordenação e joins, e criar um rótulo apenas para exibição.

SELECT  DATE_TRUNC('month', created_at) AS mes_inicio,  TO_CHAR(DATE_TRUNC('month', created_at), 'YYYY-MM') AS mes_label,  SUM(valor_total) AS receitaFROM pedidosGROUP BY 1, 2ORDER BY 1;

Se seu banco não tiver TO_CHAR, use FORMAT_DATE/DATE_FORMAT/CONVERT. O ponto importante: ordene pelo campo de data, não pelo texto.

Filtragem correta por intervalos: evitando “buracos” e duplicidades

Regra prática: intervalo fechado-aberto

Para timestamps, uma regra robusta é filtrar com início inclusivo e fim exclusivo: >= inicio e < fim. Isso evita problemas com registros exatamente no último segundo do período e funciona bem com qualquer precisão (segundos, milissegundos).

Exemplo: pedidos de janeiro de 2026 (considerando created_at como timestamp):

SELECT  COUNT(*) AS pedidos_janFROM pedidosWHERE created_at >= TIMESTAMP '2026-01-01 00:00:00'  AND created_at <  TIMESTAMP '2026-02-01 00:00:00';

Se você usar DATE (sem hora), pode usar BETWEEN com cuidado, mas ainda assim o padrão fechado-aberto é consistente e reduz ambiguidades.

Filtrar “mês atual” e “mês anterior” sem hardcode

Em relatórios recorrentes, você não quer trocar datas manualmente. Use a data atual e derive os limites.

SELECT  SUM(valor_total) AS receita_mes_atualFROM pedidosWHERE created_at >= DATE_TRUNC('month', CURRENT_DATE)  AND created_at <  DATE_TRUNC('month', CURRENT_DATE) + INTERVAL '1 month';

Mês anterior:

SELECT  SUM(valor_total) AS receita_mes_anteriorFROM pedidosWHERE created_at >= DATE_TRUNC('month', CURRENT_DATE) - INTERVAL '1 month'  AND created_at <  DATE_TRUNC('month', CURRENT_DATE);

Esse padrão é especialmente útil para painéis e rotinas automatizadas.

Comparações temporais: MoM, YoY e período equivalente

Comparar mês atual vs mês anterior (MoM)

Um jeito prático é calcular a receita por mês e depois trazer o mês anterior com uma função de janela (window function) como LAG. Mesmo que você ainda não tenha usado funções de janela, pense nelas como “olhar para a linha anterior” dentro de uma ordenação.

WITH receita_mensal AS (  SELECT    DATE_TRUNC('month', created_at) AS mes_inicio,    SUM(valor_total) AS receita  FROM pedidos  GROUP BY 1)SELECT  mes_inicio,  receita,  LAG(receita) OVER (ORDER BY mes_inicio) AS receita_mes_anterior,  receita - LAG(receita) OVER (ORDER BY mes_inicio) AS delta_abs,  (receita / NULLIF(LAG(receita) OVER (ORDER BY mes_inicio), 0) - 1) AS delta_pctFROM receita_mensalORDER BY mes_inicio;

Detalhes importantes: use NULLIF para evitar divisão por zero; mantenha a ordenação por mes_inicio (tipo data) para não bagunçar a sequência.

Comparar com o mesmo mês do ano anterior (YoY)

Para YoY, você quer comparar “jan/2026” com “jan/2025”, “fev/2026” com “fev/2025”. Você pode usar LAG com deslocamento de 12 linhas se a série for mensal e completa. Mas se houver meses faltando, a comparação por deslocamento pode falhar. Uma alternativa mais robusta é fazer um self-join por mes_inicio - 1 ano.

WITH receita_mensal AS (  SELECT    DATE_TRUNC('month', created_at) AS mes_inicio,    SUM(valor_total) AS receita  FROM pedidos  GROUP BY 1)SELECT  r.mes_inicio,  r.receita,  r_ly.receita AS receita_ano_anterior,  r.receita - r_ly.receita AS delta_abs,  (r.receita / NULLIF(r_ly.receita, 0) - 1) AS delta_pctFROM receita_mensal rLEFT JOIN receita_mensal r_ly  ON r_ly.mes_inicio = r.mes_inicio - INTERVAL '1 year'ORDER BY r.mes_inicio;

Esse padrão continua funcionando mesmo se alguns meses estiverem ausentes, porque a chave é o próprio período.

Período equivalente: últimos 30 dias vs 30 dias anteriores

Comparar “últimos 30 dias” com “30 dias anteriores” é comum em acompanhamento de performance. O cuidado aqui é definir claramente os limites e evitar sobreposição.

WITH limites AS (  SELECT    CURRENT_DATE AS hoje,    CURRENT_DATE - INTERVAL '30 day' AS inicio_30d,    CURRENT_DATE - INTERVAL '60 day' AS inicio_60d)SELECT  SUM(CASE WHEN created_at >= inicio_30d AND created_at < hoje THEN valor_total END) AS receita_ult_30d,  SUM(CASE WHEN created_at >= inicio_60d AND created_at < inicio_30d THEN valor_total END) AS receita_30d_anterioresFROM pedidosCROSS JOIN limites;

Observe o padrão fechado-aberto em ambos os blocos para não contar o mesmo dia em dois períodos.

Diferença entre datas: aging, SLA, tempo até conversão

Calcular dias entre dois eventos

Você frequentemente precisa medir tempo entre criação e fechamento, pedido e entrega, cadastro e primeira compra. Dependendo do banco, a função pode ser DATEDIFF/DATE_DIFF ou subtração direta.

SELECT  id_pedido,  created_at,  entregue_em,  DATE_DIFF(entregue_em, created_at, DAY) AS dias_ate_entregaFROM pedidosWHERE entregue_em IS NOT NULL;

Se você precisa de horas/minutos, use a unidade apropriada (HOUR, MINUTE) ou calcule a diferença em segundos e converta.

Classificar em faixas (buckets) de atraso

Depois de calcular a diferença, é comum agrupar em faixas para relatórios operacionais.

WITH base AS (  SELECT    id_pedido,    DATE_DIFF(entregue_em, created_at, DAY) AS dias_ate_entrega  FROM pedidos  WHERE entregue_em IS NOT NULL)SELECT  CASE    WHEN dias_ate_entrega <= 1 THEN '0-1 dia'    WHEN dias_ate_entrega BETWEEN 2 AND 3 THEN '2-3 dias'    WHEN dias_ate_entrega BETWEEN 4 AND 7 THEN '4-7 dias'    ELSE '8+ dias'  END AS faixa,  COUNT(*) AS pedidosFROM baseGROUP BY 1ORDER BY 1;

Esse tipo de bucket ajuda a enxergar distribuição e identificar caudas longas (casos muito atrasados).

Calendário (date dimension): quando e como usar

Por que uma tabela calendário ajuda

Quando você agrega por dia/semana/mês diretamente da tabela de fatos (ex.: pedidos), dias sem movimento simplesmente não aparecem. Isso atrapalha gráficos, médias móveis e comparações. Uma tabela calendário (dim_date) resolve isso, pois contém uma linha para cada dia e atributos úteis: ano, mês, nome do mês, trimestre, semana ISO, dia da semana, indicador de fim de semana, feriado (se você mantiver), etc.

Estrutura típica de dim_date

  • date_day (DATE)
  • year, month, day
  • month_start (DATE), month_end (DATE)
  • week_start (DATE), iso_week, iso_year
  • quarter
  • day_of_week (1-7), is_weekend

Passo a passo — Preencher lacunas de dias sem pedidos

Suponha que você tenha dim_date com todos os dias do período. Você quer receita diária, incluindo dias com zero.

WITH receita_por_dia AS (  SELECT    CAST(created_at AS DATE) AS dia,    SUM(valor_total) AS receita  FROM pedidos  GROUP BY 1)SELECT  d.date_day AS dia,  COALESCE(r.receita, 0) AS receitaFROM dim_date dLEFT JOIN receita_por_dia r  ON r.dia = d.date_dayWHERE d.date_day >= DATE '2026-01-01'  AND d.date_day <  DATE '2026-02-01'ORDER BY d.date_day;

Repare no COALESCE para transformar NULL em zero. Isso é essencial para séries temporais contínuas.

Passo a passo — Relatório por semana ISO com calendário

Semanas ISO podem atravessar anos (ex.: semana 1 pode começar em dezembro). A dim_date ajuda a padronizar. Você agrega pedidos por dia e depois soma por week_start (ou iso_week/iso_year).

WITH receita_diaria AS (  SELECT    CAST(created_at AS DATE) AS dia,    SUM(valor_total) AS receita  FROM pedidos  GROUP BY 1)SELECT  d.week_start,  d.iso_year,  d.iso_week,  SUM(COALESCE(r.receita, 0)) AS receita_semanaFROM dim_date dLEFT JOIN receita_diaria r  ON r.dia = d.date_dayWHERE d.date_day >= DATE '2026-01-01'  AND d.date_day <  DATE '2026-03-01'GROUP BY 1, 2, 3ORDER BY d.week_start;

Esse padrão evita “semanas quebradas” e garante consistência de calendário em toda a empresa.

Datas e horários: fuso horário e “dia de negócio”

O problema do fuso em timestamps

Se o banco armazena timestamps em UTC, mas o negócio opera em um fuso local (ex.: America/Sao_Paulo), o “dia” de negócio pode mudar. Um pedido feito às 00:30 UTC pode ser 21:30 do dia anterior no Brasil. Se você agrupar por CAST(created_at AS DATE) sem converter fuso, seu relatório diário pode ficar deslocado.

Padrão: converter para fuso de negócio antes de truncar

O jeito correto é: (1) converter timestamp para o fuso desejado, (2) truncar para dia/mês. As funções variam: AT TIME ZONE, CONVERT_TZ, etc.

SELECT  CAST(created_at AT TIME ZONE 'America/Sao_Paulo' AS DATE) AS dia_negocio,  COUNT(*) AS pedidosFROM pedidosGROUP BY 1ORDER BY 1;

Se seu banco não suportar nomes de timezone, você pode ter que aplicar offset fixo, mas isso falha em horário de verão (quando aplicável). Em ambientes corporativos, prefira suporte nativo a timezone.

Dia de negócio que não começa à meia-noite

Algumas operações consideram o “dia” começando às 06:00, por exemplo (turnos, logística). Você pode ajustar o timestamp antes de truncar: subtrair 6 horas e então truncar para dia.

SELECT  CAST((created_at - INTERVAL '6 hour') AS DATE) AS dia_operacional,  COUNT(*) AS eventosFROM eventosGROUP BY 1ORDER BY 1;

Isso realinha o corte diário ao que o time operacional considera “um dia”.

Armadilhas comuns e como evitar

1) Filtrar mês com BETWEEN em timestamp

Usar BETWEEN '2026-01-01' AND '2026-01-31' em timestamp pode excluir registros do dia 31 após 00:00:00 (dependendo de conversão implícita) ou incluir/excluir de forma inesperada. Prefira sempre início inclusivo e fim exclusivo com o primeiro dia do mês seguinte.

2) Agrupar por string em vez de data

Agrupar por TO_CHAR(created_at, 'YYYY-MM') pode funcionar, mas você perde a facilidade de ordenar corretamente e de fazer joins por período. Mantenha uma coluna de período do tipo DATE/TIMESTAMP e crie string apenas para exibição.

3) Misturar timezone na mesma consulta

Se parte da consulta converte timezone e outra parte não, você pode comparar datas de “dias” diferentes sem perceber. Defina um padrão: sempre converter para o fuso de negócio no início (em uma CTE) e usar esse campo derivado em todo o restante.

4) Comparar períodos incompletos

Comparar “mês atual” (ainda em andamento) com “mês anterior” (completo) pode gerar interpretações erradas. Em muitos relatórios, faz sentido comparar “mês até hoje” com “mês anterior até o mesmo dia”. Isso exige limitar ambos pelo mesmo número de dias transcorridos.

WITH params AS (  SELECT    CURRENT_DATE AS hoje,    DATE_TRUNC('month', CURRENT_DATE) AS inicio_mes_atual,    DATE_TRUNC('month', CURRENT_DATE) - INTERVAL '1 month' AS inicio_mes_anterior,    (CURRENT_DATE - DATE_TRUNC('month', CURRENT_DATE)) AS dias_decorridos)SELECT  SUM(CASE WHEN created_at >= inicio_mes_atual AND created_at < hoje THEN valor_total END) AS receita_mtd,  SUM(CASE WHEN created_at >= inicio_mes_anterior AND created_at < (inicio_mes_anterior + dias_decorridos) THEN valor_total END) AS receita_mes_anterior_mesmo_periodoFROM pedidosCROSS JOIN params;

Aqui, dias_decorridos representa quantos dias se passaram desde o início do mês atual; aplicamos o mesmo “comprimento” ao mês anterior para uma comparação mais justa.

Checklist prático para consultas temporais confiáveis

  • Defina se o campo é DATE ou TIMESTAMP e trate adequadamente.
  • Para timestamps, filtre com intervalo fechado-aberto (>= inicio e < fim).
  • Trunque para o período (dia/semana/mês) e use esse campo como chave de agregação.
  • Se houver fuso horário, converta antes de truncar e antes de filtrar por “dia”.
  • Para comparações (MoM/YoY), prefira self-join por período ou janelas com cuidado para séries incompletas.
  • Use dim_date para preencher lacunas e padronizar semanas, trimestres e atributos de calendário.
  • Evite agrupar/ordenar por strings; use datas e só formate para exibição.

Agora responda o exercício sobre o conteúdo:

Ao filtrar registros por um mês em uma coluna TIMESTAMP, qual padrão de intervalo é mais robusto para evitar buracos e duplicidades no período?

Você acertou! Parabéns, agora siga para a próxima página

Você errou! Tente novamente.

O padrão mais seguro para TIMESTAMP é início inclusivo e fim exclusivo (>= início e < fim), usando como fim o primeiro instante do mês seguinte. Isso evita problemas com registros no limite final e funciona com qualquer precisão de tempo.

Próximo capitúlo

Funções de texto para padronização, limpeza e categorização de campos

Arrow Right Icon
Baixe o app para ganhar Certificação grátis e ouvir os cursos em background, mesmo com a tela desligada.