← Voltar ao blog

BETWEEN de datas no Firebird: o erro silencioso do fim do dia

Um BETWEEN com duas datas ignora registros depois da meia-noite do último dia. Veja como escrever faixas de data que não perdem linhas nem índice.


Um relatório "do mês" que não traz os lançamentos do último dia. Um filtro que funciona em setembro e falha em outubro. Um total que fecha no banco mas não na tela. Em Firebird, boa parte desses casos vem do mesmo lugar: uma faixa de datas escrita com BETWEEN em uma coluna TIMESTAMP.

O erro não gera exceção, não aparece no log e passa em qualquer teste feito no meio do mês. Ele só se revela quando alguém confere o último dia.


O problema: BETWEEN inclui a meia-noite, não o dia inteiro

Considere uma coluna DATA_HORA do tipo TIMESTAMP e o filtro:

SELECT ID, DATA_HORA, VALOR
FROM LANCAMENTOS
WHERE DATA_HORA BETWEEN '2026-10-01' AND '2026-10-31';

O Firebird interpreta os literais como datas à meia-noite. A condição equivale a:

WHERE DATA_HORA >= '2026-10-01 00:00:00'
  AND DATA_HORA <= '2026-10-31 00:00:00'

Tudo o que aconteceu em 31/10 às 08h, 14h ou 23h59 fica fora do resultado. O relatório perde um dia inteiro e ninguém percebe até o fechamento.

Se a coluna fosse DATE (sem hora), o BETWEEN funcionaria. O problema aparece quando o tipo é TIMESTAMP ou quando a coluna guarda data e hora mesmo que a tela mostre só a data.

A solução: intervalo semiaberto

Em vez de tentar incluir o último instante, exclua o primeiro instante do dia seguinte:

SELECT ID, DATA_HORA, VALOR
FROM LANCAMENTOS
WHERE DATA_HORA >= '2026-10-01'
  AND DATA_HORA < '2026-11-01';

A regra é simples:

  • maior ou igual ao início do período;
  • menor que o início do período seguinte.

Não existe buraco entre os intervalos, e nenhum registro é contado duas vezes. Funciona para dia, mês, ano e qualquer faixa.

Com parâmetros

SELECT ID, DATA_HORA, VALOR
FROM LANCAMENTOS
WHERE DATA_HORA >= ?
  AND DATA_HORA < ?;

Em PHP, calcule o início do próximo período em vez de somar 23h59min59s:

$ini = new DateTimeImmutable('first day of this month 00:00:00');
$fim = $ini->modify('+1 month');

$sql = "SELECT ID, DATA_HORA, VALOR
        FROM LANCAMENTOS
        WHERE DATA_HORA >= ? AND DATA_HORA < ?";

$rows = db_getAll($sql, [
    $ini->format('Y-m-d H:i:s'),
    $fim->format('Y-m-d H:i:s'),
]);

Com DATEADD no próprio SQL

No Firebird 2.1+, dá para calcular o limite no banco:

WHERE DATA_HORA >= ?
  AND DATA_HORA < DATEADD(1 DAY TO ?)

Para mês, DATEADD(1 MONTH TO ?) avança um mês. Cuidado com datas como 31 de janeiro: o Firebird ajusta o resultado, então prefira calcular o início do mês seguinte na aplicação quando a regra de negócio for sensível a isso.

Por que não usar 23:59:59

Muita gente corrige o BETWEEN assim:

WHERE DATA_HORA BETWEEN '2026-10-01' AND '2026-10-31 23:59:59';

Funciona na maioria dos casos, mas não em todos. O TIMESTAMP do Firebird tem precisão de frações de segundo; um registro gravado às 23:59:59.5000 continua fora. Além disso, a intenção fica ambígua: o próximo desenvolvedor não sabe se o intervalo inclui ou não o limite.

O intervalo semiaberto elimina a dúvida e o caso de borda de uma vez.

O CAST que mata o índice

Outro padrão comum é "normalizar" a data no filtro:

WHERE CAST(DATA_HORA AS DATE) = '2026-10-31'

A consulta funciona, mas o Firebird precisa calcular CAST para cada linha antes de comparar. O índice em DATA_HORA deixa de ser usado e a tabela inteira é varrida.

Reescreva como faixa:

WHERE DATA_HORA >= '2026-10-31'
  AND DATA_HORA < '2026-11-01'

Mesmo resultado, com índice. A regra vale para qualquer função na coluna filtrada: EXTRACT, UPPER, SUBSTRING. Se a função envolve a coluna, o índice comum não participa.

Datas como texto: o perigo do formato

Evite comparar datas com strings no formato brasileiro:

-- depende de configuração e pode falhar
WHERE DATA_HORA BETWEEN '01/10/2026' AND '31/10/2026'

O Firebird aceita literais no formato AAAA-MM-DD de forma previsível. Se a data vem da tela em dd/mm/aaaa, converta antes de enviar:

$dt = DateTimeImmutable::createFromFormat('d/m/Y', $_POST['data']);
if (!$dt) {
    throw new InvalidArgumentException('Data inválida');
}
$param = $dt->format('Y-m-d 00:00:00');

Validar a data na entrada também evita que um valor inválido vire NULL silencioso no bind.

CURRENT_DATE e CURRENT_TIMESTAMP

CURRENT_DATE retorna a data do servidor à meia-noite; CURRENT_TIMESTAMP retorna data e hora. Para "hoje", o intervalo correto é:

WHERE DATA_HORA >= CURRENT_DATE
  AND DATA_HORA < DATEADD(1 DAY TO CURRENT_DATE)

Usar WHERE DATA_HORA = CURRENT_DATE em coluna TIMESTAMP não retorna nada, porque a hora nunca será exatamente meia-noite. É o mesmo erro do BETWEEN, em outra roupa.

Armadilhas comuns

Padrão Problema Correção
BETWEEN '2026-10-01' AND '2026-10-31' em TIMESTAMP perde o último dia < '2026-11-01'
BETWEEN ... 23:59:59 perde frações de segundo intervalo semiaberto
CAST(DATA_HORA AS DATE) = ? ignora o índice faixa de datas
EXTRACT(DAY FROM DATA_HORA) = 31 varredura completa faixa de datas
DATA_HORA = CURRENT_DATE não casa com hora >= e <
data em dd/mm/aaaa formato ambíguo converter para Y-m-d
somar 30 dias para "um mês" meses têm tamanhos diferentes início do mês seguinte

Checklist

  • A coluna é DATE ou TIMESTAMP? O filtro reflete isso?
  • O limite superior é o início do período seguinte?
  • Usei < e não <= no limite superior?
  • Evitei CAST, EXTRACT e funções na coluna filtrada?
  • A data chega validada e no formato AAAA-MM-DD?
  • Testei com um registro no último dia, perto da meia-noite?
  • O plano de execução usa o índice da coluna de data?

Conclusão

BETWEEN em datas parece inocente e funciona nos testes feitos no meio do mês. O intervalo semiaberto — >= início e < início do próximo período — é a forma que não perde o último dia, não depende de frações de segundo e mantém o índice utilizável.

Se você quer gerar esse tipo de filtro sem decorar a sintaxe do Firebird, o Sqlify transforma a descrição em português na consulta pronta, com os limites e parâmetros no lugar. Descreva "lançamentos do mês" e revise o SQL gerado antes de usar.

Leia também: