Módulo 3: Datas e tempo
date, INTERVAL, subtração de datas, AGE, EXTRACT, DATE_TRUNC, TO_CHAR
Antes de começar
No SQLite data era texto '2025-03-01' e você fazia conta na mão com
strftime. No Postgres date é um tipo real, e isso destrava
um monte de coisa:
- Subtrair datas dá número de dias (int):
data_fechamento - data_abertura→12. INTERVALpra somar/subtrair tempo:CURRENT_DATE - INTERVAL '30 days',data_pedido + INTERVAL '7 days'.AGE(fim, inicio)→ intervalo "2 mons 3 days".AGE(data)sozinho = quanto tempo desde então até hoje.EXTRACT(campo FROM data): puxa um pedaço:EXTRACT(YEAR FROM data_pedido),MONTH,DOW(dia da semana, 0 = domingo),WEEK.DATE_TRUNC('month', data): joga a data pro começo do mês (2025-03-01). Serve pra agrupar por mês/semana/dia.TO_CHAR(data, 'YYYY-MM'): formata como texto pra rótulo.
Olhar de QA: "quanto tempo em média um bug crítico fica aberto?", "quantos pedidos por mês?", "cadastro no futuro?": tudo é conta de data.
Dois exemplos
Teoria
data2 - data1→ dias (inteiro).data + INTERVAL '1 month'→ outra data.CURRENT_DATE/now(): hoje / agora.AGE(fim, inicio)→ intervalo legível.EXTRACT(DAY FROM AGE(...))pra virar número.EXTRACT(YEAR|MONTH|DOW|WEEK FROM data): componente da data.DATE_TRUNC('month'|'week'|'day', data): "arredonda" a data pra baixo.TO_CHAR(data, 'YYYY-MM')/'DD/MM/YYYY': formata como texto.
Exemplos de aplicação
Um exemplo montado pra cada item da teoria acima. Não precisa rodar, é pra ver a cara do recurso e entender cada pedaço. Os valores são ilustrativos.
data - data e data + INTERVAL
SELECT
id,
data_fechamento - data_abertura AS dias_para_fechar,
data_abertura + INTERVAL '30 days' AS prazo_sla
FROM defeitos
WHERE data_fechamento IS NOT NULL;
Subtrair duas date dá um inteiro de dias (data_fechamento - data_abertura). Somar um INTERVAL a uma data dá outra data: aqui, o prazo de 30 dias a partir da abertura.
CURRENT_DATE / now()
SELECT
id,
data_abertura,
CURRENT_DATE - data_abertura AS dias_em_aberto
FROM defeitos
WHERE data_fechamento IS NULL;
CURRENT_DATE é a data de hoje; now() traz data + hora. Aqui: pra cada defeito ainda aberto, há quantos dias ele está aberto (hoje menos a abertura).
AGE(...) + EXTRACT
SELECT
id,
AGE(data_fechamento, data_abertura) AS tempo,
EXTRACT(DAY FROM AGE(data_fechamento, data_abertura)) AS dias
FROM defeitos
WHERE data_fechamento IS NOT NULL;
AGE(fim, inicio) devolve um intervalo legível ("1 mon 3 days"). EXTRACT(DAY FROM ...) puxa só um pedaço desse intervalo como número, pra dar pra somar/ordenar.
EXTRACT(YEAR|MONTH|DOW FROM data)
SELECT
EXTRACT(YEAR FROM data_pedido) AS ano,
EXTRACT(MONTH FROM data_pedido) AS mes,
COUNT(*) AS qtd_pedidos
FROM pedidos
GROUP BY 1, 2
ORDER BY 1, 2;
EXTRACT tira um componente da data (ano, mês, dia da semana com DOW…). Aqui vira base de agrupamento: pedidos por ano/mês. O GROUP BY 1, 2 agrupa pela 1ª e 2ª colunas do SELECT.
DATE_TRUNC('month', data)
SELECT
DATE_TRUNC('month', data_pedido)::date AS mes,
COUNT(*) AS qtd,
SUM(valor_total) AS total
FROM pedidos
GROUP BY 1
ORDER BY 1;
DATE_TRUNC('month', d) "arredonda a data pra baixo": toda data do mês vira o dia 1 daquele mês. Assim todas as linhas de um mês caem no mesmo valor e o GROUP BY soma por mês. Dá pra trocar por 'week' ou 'day'.
TO_CHAR(data, formato)
SELECT
id,
TO_CHAR(data_pedido, 'DD/MM/YYYY') AS data_br,
TO_CHAR(data_pedido, 'YYYY-MM') AS competencia
FROM pedidos
LIMIT 5;
TO_CHAR transforma a data em texto no formato que você pedir: 'DD/MM/YYYY' pra exibir no padrão BR, 'YYYY-MM' pra rótulo de mês. Serve pra apresentação; pra ordenar, prefira a data crua ou o formato YYYY-MM.
Pegadinhas deste módulo
Os erros que quase todo mundo comete aqui. Ler agora poupa meia hora de confusão depois, e alguns deles são a causa de relatório errado em projeto real.
COUNT(*)eCOUNT(coluna)dão respostas diferentes.COUNT(*)conta linhas;COUNT(coluna)ignora as linhas em que aquela coluna éNULL. Usar um pelo outro é a origem de metade dos números errados em relatório de QA.AVGtambém ignoraNULL, e isso muda a média.AVG(duracao)sobre 100 linhas com 40 nulas é a média de 60, não de 100. Se o vazio deveria contar como zero, useAVG(COALESCE(duracao, 0)).WHEREfiltra antes de agrupar;HAVING, depois. Filtrar por valor de linha noHAVINGfunciona em alguns bancos e é lento em todos. E o contrário,WHERE COUNT(*) > 5, simplesmente não existe: agregação não cabe noWHERE.- Grupo sem nenhuma linha não aparece no resultado. Se você agrupa defeitos por módulo, o módulo com zero defeitos não vem na lista, ele simplesmente some. Quem lê o relatório conclui que o módulo não existe, e não que ele está limpo.
- Cuidado ao somar percentuais já calculados. A média das taxas de falha por suíte não é a taxa de falha geral: suítes com tamanhos diferentes pesam diferente. Some os numeradores e os denominadores, e divida no fim.
Prática
Escreva a query e clique Rodar (ou Ctrl+Enter).
carregando banco…