QA Learning / PostgreSQL

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.
  • INTERVAL pra 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(*) e COUNT(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.
  • AVG também ignora NULL, 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, use AVG(COALESCE(duracao, 0)).
  • WHERE filtra antes de agrupar; HAVING, depois. Filtrar por valor de linha no HAVING funciona em alguns bancos e é lento em todos. E o contrário, WHERE COUNT(*) > 5, simplesmente não existe: agregação não cabe no WHERE.
  • 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…

Query livre

Terminou este módulo? Marque: o ✓ aparece no índice da trilha e a barra de progresso anda no começo do site. Fica salvo só no seu navegador.