Caso real: o relatório que contava o dia errado
Um bug de fuso horário do sintoma à correção, em quatro iterações — com o SQL de cada etapa e o teste que impede a volta.
O relatório diário de vendas fechava sempre um pouco diferente do que o time comercial contava. Nunca muito. O suficiente para alguém abrir um chamado dizendo "acho que o número está errado".
- Ver o ciclo curto aplicado a um bug real, do sintoma ao teste de regressão.
- Reconhecer a família de bugs de fuso — a mais comum em relatório.
- Escrever o teste que trava a correção no lugar.
Iteração 1 — descrever o sintoma sem propor causa
Não proponha correção ainda. Sintoma: o relatório "vendas de ontem" fecha com números levemente diferentes do que o time comercial conta manualmente. A diferença é sempre pequena (1 a 4 pedidos) e sempre para menos em alguns dias, para mais em outros. Ambiente: MySQL 8.0, servidor em UTC, aplicação PHP com timezone America/Sao_Paulo. Pedidos gravam created_at com NOW() do MySQL. Query atual: SELECT COUNT(*) FROM pedidos WHERE DATE(created_at) = '2026-03-13'; Me dê hipóteses ordenadas por probabilidade, cada uma com o experimento mínimo que a confirma ou elimina. Nada de correção.
[prompt acima]
HIPÓTESE 1 (alta) — Descasamento de fuso entre gravação e leitura. created_at está em UTC; o recorte DATE() também é feito em UTC; mas o "dia" que o time comercial usa é America/Sao_Paulo (UTC-3). Efeito: pedidos feitos entre 21:00 e 23:59 de um dia (horário local) gravam com data do dia seguinte em UTC, e caem no relatório errado. Experimento: contar quantos pedidos existem na faixa '2026-03-14 00:00:00' a '2026-03-14 02:59:59' em UTC. Se esse número bater com a diferença relatada, é isso. HIPÓTESE 2 (média) — Pedidos cancelados ou de teste entrando na contagem. Experimento: comparar COUNT(*) com COUNT(*) filtrando status. HIPÓTESE 3 (baixa) — Horário de verão em dados históricos. Experimento: verificar se a diferença muda de tamanho em outubro/fevereiro de anos anteriores. OBSERVAÇÃO: DATE(created_at) também impede o uso de índice em created_at. Isso não causa o erro de contagem, mas provavelmente deixa a query lenta.
- A hipótese 1 explica os dois sinais do sintoma: diferença pequena e ora para mais, ora para menos. Uma hipótese que explica só metade do sintoma é uma hipótese fraca.
- O experimento proposto é barato: uma query de contagem, sem alterar nada.
- A observação final sobre o índice é um achado lateral — não é a causa, e veio corretamente separada dela.
- Nenhuma correção foi proposta, porque o pedido proibiu. Isso evita fechar o diagnóstico cedo demais.
Iteração 2 — rodar o experimento
-- Pedidos que a query atual joga para o dia 14 (UTC)
-- mas que aconteceram no dia 13 em horário de Brasília
SELECT COUNT(*) AS pedidos_deslocados
FROM pedidos
WHERE created_at >= '2026-03-14 00:00:00'
AND created_at < '2026-03-14 03:00:00';
-- resultado: 3Iteração 3 — a correção mínima
SELECT COUNT(*) FROM pedidos
WHERE DATE(CONVERT_TZ(created_at, '+00:00', '-03:00'))
= '2026-03-13';Conta certo — e continua sem usar índice, porque a coluna está dentro de função. Numa tabela grande, troca um bug por uma lentidão.
-- Faixa fechada-aberta, calculada no fuso do negócio -- e convertida para UTC antes de ir ao banco SELECT COUNT(*) FROM pedidos WHERE created_at >= '2026-03-13 03:00:00' AND created_at < '2026-03-14 03:00:00';
A coluna fica livre para o índice, e o limite superior aberto (<) elimina o clássico erro de contar duas vezes o registro exato da meia-noite.
A diferença: A conversão de fuso passa a acontecer na aplicação, que sabe o fuso do negócio, e o banco recebe uma faixa simples. Isso também resolve horário de verão de graça, se a aplicação usar uma biblioteca de data com base de fusos atualizada — em vez de somar três horas na mão.
/**
* Faixa UTC [inicio, fim) correspondente a um dia civil em São Paulo.
* Somar horas na mão quebra no horário de verão; DateTimeZone não.
*/
function faixaDoDia(string $dia, string $fuso = 'America/Sao_Paulo'): array
{
$tz = new DateTimeZone($fuso);
$inicio = new DateTimeImmutable($dia . ' 00:00:00', $tz);
$fim = $inicio->modify('+1 day');
$utc = new DateTimeZone('UTC');
return [
$inicio->setTimezone($utc)->format('Y-m-d H:i:s'),
$fim->setTimezone($utc)->format('Y-m-d H:i:s'),
];
}Iteração 4 — o teste que impede a volta
public function test_pedido_das_22h_conta_no_dia_local_e_nao_no_seguinte(): void
{
// 13/03 às 22:00 em São Paulo = 14/03 às 01:00 em UTC
$this->criarPedidoEm('2026-03-14 01:00:00'); // gravado em UTC
$total = $this->relatorio->pedidosDoDia('2026-03-13');
$this->assertSame(1, $total, 'pedido das 22h caiu no dia seguinte');
}
public function test_meia_noite_exata_pertence_ao_dia_que_comeca(): void
{
$this->criarPedidoEm('2026-03-13 03:00:00'); // 00:00 em São Paulo
$this->assertSame(1, $this->relatorio->pedidosDoDia('2026-03-13'));
$this->assertSame(0, $this->relatorio->pedidosDoDia('2026-03-12'));
}Fuso é só uma das formas. A mesma classe de erro aparece em: comparar datetime com date e perder as horas; usar BETWEEN com dois limites fechados e contar a fronteira duas vezes; guardar data de nascimento com fuso (não deve ter); e assumir que o servidor, o banco e o navegador concordam sobre "hoje".
Nenhum desses aparece em desenvolvimento local, porque a máquina de quem programa costuma estar no mesmo fuso do negócio. Todos aparecem em produção.
Como perceber: Números "quase certos" em relatório quase sempre são fronteira de data, não erro de soma.
- A hipótese explicava todos os sinais do sintoma, não parte deles.
- O experimento produziu um número que bateu com o relato.
- A correção não colocou a coluna indexada dentro de função.
- A faixa usa limite superior aberto.
- Existe teste na fronteira, não só no caso do meio.
- Quatro iterações: descrever sem propor, medir, corrigir o mínimo, travar com teste na fronteira.
Comentários e dúvidas
Inscreva-se grátis para comentar, tirar dúvidas, marcar seu progresso e emitir o certificado ao fim do curso.
Inscrever-se grátis com GoogleAinda não há comentários nesta aula.