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 é
DATEouTIMESTAMP? O filtro reflete isso? - O limite superior é o início do período seguinte?
- Usei
<e não<=no limite superior? - Evitei
CAST,EXTRACTe 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: