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