← Voltar ao blog

Como encontrar a query que trava o Firebird com as tabelas MON$

MON$STATEMENTS, MON$ATTACHMENTS e MON$TRANSACTIONS mostram qual consulta está rodando, há quanto tempo e quantas páginas leu. Veja como investigar.


O servidor está lento, os usuários reclamam, mas ninguém sabe qual consulta está causando o problema. Reiniciar resolve por alguns minutos e a causa volta. Quem trabalha com Firebird já viveu esse ciclo.

O Firebird expõe um conjunto de tabelas de monitoramento em memória — as MON$ — que mostram, em tempo real, quais conexões estão abertas, quais statements estão ativos e quantas páginas cada um leu. Com elas, dá para sair do "achismo" e apontar a consulta culpada.

Este guia mostra como consultar essas tabelas, interpretar as métricas e diferenciar três problemas que parecem iguais: query sem índice, query repetida demais e transação antiga segurando o banco.


O que são as tabelas MON$

As tabelas MON$ existem desde o Firebird 2.1. Elas não são tabelas físicas: são visões em memória do estado do servidor naquele instante. Não ocupam espaço no banco e não precisam de manutenção.

As principais são:

Tabela O que mostra
MON$ATTACHMENTS conexões abertas (usuário, IP, processo, estado)
MON$TRANSACTIONS transações ativas e o isolamento de cada uma
MON$STATEMENTS comandos SQL em execução ou parados, com o texto
MON$RECORD_STATS leituras indexadas e não indexadas por statement
MON$PAGE_STATS páginas lidas, escritas e buscadas em cache
MON$DATABASE idade das transações mais antigas do banco

Por padrão, apenas o SYSDBA enxerga todas as linhas. Para usar em produção com outro usuário, conceda SELECT nas tabelas MON$ ao usuário de monitoramento.

Achando o que está rodando agora

Comece pelos statements ativos:

SELECT
  s.MON$STATEMENT_ID,
  s.MON$ATTACHMENT_ID,
  s.MON$STATE,
  s.MON$TIMESTAMP,
  s.MON$SQL_TEXT
FROM MON$STATEMENTS s
WHERE s.MON$STATE = 1
ORDER BY s.MON$TIMESTAMP;

MON$STATE = 1 significa ativo; 0 é idle. Um statement ativo há muito tempo é o primeiro suspeito.

Para ligar o statement ao usuário e ao endereço de origem:

SELECT
  a.MON$USER,
  a.MON$REMOTE_ADDRESS,
  a.MON$REMOTE_PROCESS,
  s.MON$STATEMENT_ID,
  s.MON$TIMESTAMP,
  s.MON$SQL_TEXT
FROM MON$STATEMENTS s
JOIN MON$ATTACHMENTS a ON a.MON$ATTACHMENT_ID = s.MON$ATTACHMENT_ID
WHERE s.MON$STATE = 1;

Agora você sabe quem está rodando o quê.

Lendo as métricas que importam

O texto da query sozinho não diz se ela leu 10 linhas ou 10 milhões. As estatísticas dizem:

SELECT
  s.MON$STATEMENT_ID,
  s.MON$ATTACHMENT_ID,
  s.MON$SQL_TEXT,
  r.MON$RECORD_SEQ_READS,
  r.MON$RECORD_IDX_READS,
  p.MON$PAGE_FETCHES,
  p.MON$PAGE_READS
FROM MON$STATEMENTS s
LEFT JOIN MON$RECORD_STATS r ON r.MON$STAT_ID = s.MON$STAT_ID
LEFT JOIN MON$PAGE_STATS  p ON p.MON$STAT_ID = s.MON$STAT_ID
WHERE s.MON$STATE = 1;
Métrica Significado O que indica
MON$RECORD_SEQ_READS leituras sequenciais de registros varredura sem índice
MON$RECORD_IDX_READS leituras via índice índice sendo usado
MON$PAGE_FETCHES páginas buscadas (cache + disco) volume de dados lido
MON$PAGE_READS páginas lidas do disco cache frio ou tabela grande
MON$PAGE_WRITES páginas gravadas escrita pesada

A leitura mais útil é a proporção entre sequencial e indexado. Muitas leituras sequenciais com poucas indexadas quase sempre significam um WHERE que o índice não cobre — coluna com função, tipo diferente ou índice inexistente.

Três problemas diferentes que parecem o mesmo

1. Query sem índice

Sintoma: MON$RECORD_SEQ_READS alto, MON$RECORD_IDX_READS baixo, MON$PAGE_FETCHES alto.

Ação: revisar o plano com SET PLAN ON, conferir a ordem das colunas e criar o índice adequado. Se a consulta usa função na coluna (UPPER, CAST), o índice comum não serve.

2. Query repetida demais (N+1)

Sintoma: muitos statements triviais, cada um lendo poucas linhas, todos com o mesmo texto e parâmetros diferentes.

Ação: não é índice, é padrão de acesso. Agrupe as consultas em um IN (...) ou JOIN. O volume de execuções é o problema, não o custo de cada uma.

3. Transação antiga segurando o banco

Sintoma: tudo fica lento sem uma query específica aparecer como culpada. O banco acumula versões antigas de registros e o garbage collection não consegue limpar.

Verifique a idade das transações:

SELECT
  MON$OLDEST_TRANSACTION,
  MON$OLDEST_ACTIVE,
  MON$OLDEST_SNAPSHOT
FROM MON$DATABASE;

Se MON$OLDEST_ACTIVE está muito atrás do último ID de transação, existe uma transação aberta há muito tempo — às vezes uma conexão esquecida, às vezes um COMMIT que nunca chegou. Enquanto ela existir, o sweep não avança e as consultas pagam a conta.

Para localizar a transação:

SELECT
  t.MON$TRANSACTION_ID,
  t.MON$ATTACHMENT_ID,
  t.MON$STATE,
  t.MON$TIMESTAMP,
  a.MON$USER,
  a.MON$REMOTE_ADDRESS
FROM MON$TRANSACTIONS t
JOIN MON$ATTACHMENTS a ON a.MON$ATTACHMENT_ID = t.MON$ATTACHMENT_ID
ORDER BY t.MON$TIMESTAMP;

O que fazer quando o statement está travado

Um statement ativo há minutos pode estar esperando lock, I/O ou uma transação concorrente. Antes de cancelar:

  1. Confirme o tempo. MON$TIMESTAMP mostra quando começou.
  2. Veja a origem. Usuário e IP indicam se é tela, job ou integração.
  3. Verifique locks. MON$RECORD_STATS com conflitos altos sugere disputa de registro.
  4. Só então cancele. DELETE FROM MON$STATEMENTS WHERE MON$STATEMENT_ID = ? interrompe o comando. É uma ação administrativa, não um teste.

Cancelar sem identificar a causa apenas transfere o problema para o próximo horário de pico.

Monitorando de forma contínua

Consultar as MON$ manualmente resolve o incidente de hoje. Para evitar o próximo:

  • Registre periodicamente os statements com maior MON$PAGE_FETCHES em uma tabela de histórico.
  • Alerte quando MON$OLDEST_ACTIVE ficar muito distante do topo.
  • Acompanhe o número de conexões ativas por usuário.
  • Correlacione picos de CPU/RAM com o horário dos statements mais pesados.

Uma consulta a cada minuto em MON$STATEMENTS custa pouco e transforma "o banco travou" em "esta consulta rodou às 10h02".

Passo a passo para investigar

  1. Liste os statements ativos (MON$STATE = 1).
  2. Junte com MON$ATTACHMENTS para identificar usuário e origem.
  3. Compare leituras sequenciais e indexadas.
  4. Verifique a idade das transações em MON$DATABASE.
  5. Classifique: índice, N+1 ou transação antiga.
  6. Corrija a causa e meça de novo com as mesmas métricas.

Checklist

  • Sei quais statements estão ativos agora.
  • Consigo ligar cada statement ao usuário e ao IP.
  • Sei diferenciar leitura sequencial de leitura indexada.
  • Verifico a transação mais antiga antes de culpar a query.
  • Tenho um histórico, não só uma foto do momento.
  • Só cancelo um statement depois de identificar a origem.

Conclusão

As tabelas MON$ são o raio-X do Firebird em produção. Elas mostram o que está rodando, quanto cada comando leu e se existe uma transação antiga impedindo a limpeza. Com esses três sinais, a investigação deixa de ser tentativa e erro.

Se você precisa transformar uma descrição em português na consulta Firebird certa — com os filtros e índices no lugar — o Sqlify gera o SQL e explica cada cláusula. Descreva o que quer consultar e revise a query pronta antes de colocar em produção.

Leia também: