Become a member!

MCP Firebird 0.4.0: o consultor que sabe dizer para não criar o índice

🌐
Este artigo também está disponível em outros idiomas:
🇬🇧 English  •  🇮🇹 Italiano  •  🇪🇸 Español  •  🇩🇪 Deutsch

A 0.4.0 do MCP Firebird reescreve o consultor de índices. Agora ele mede a consulta antes de falar, propõe o remédio que custa menos, conhece índices de expressão, parciais e para o ORDER BY, e quando o scan é a escolha certa responde com os números e nenhum SQL para executar.

Logo do MCP Firebird, o servidor MCP que permite a assistentes de IA diagnosticar bancos de dados Firebird.

Sua consulta está lenta. O plano diz PLAN (ORDERS NATURAL), ou seja, varredura completa da tabela ORDERS, e não existe nenhum índice na coluna filtrada. O conselho que você vai receber, de um humano, de uma IA ou de um “DBA amador de domingo”, é quase sempre o mesmo: crie o índice.

Mas olhe os números. Aquele scan lê 20.048 registros e retorna 18.000 deles. 89,8% do que ele toca acaba no resultado. Um índice ali faria você fazer duas leituras em vez de uma para quase toda linha da tabela, e você pagaria isso com uma escrita a mais a cada INSERT, UPDATE e DELETE, de hoje até o dia em que alguém apagar o índice. E você sabe muito bem que ninguém vai apagar, porque “pode quebrar alguma coisa”. Uma operação pensada para melhorar o desempenho acabaria piorando, durante toda a vida da aplicação.

Nesse caso o scan era o plano certo. O conselho certo era não fazer nada, bem longe de “crie um índice”.

A 0.4.0 do MCP Firebird, lançada em 7 de agosto de 2026, ensina o fb_suggest_indexes a dizer “está bom assim” em vez de “crie este índice”.

Em resumo

  • fb_suggest_indexes reescrito em torno de uma escala de remédios, do mais barato ao mais caro. Primeiro as estatísticas, depois a reativação de um índice desligado, e só no final um índice novo.
  • O scan pode ser o plano certo. A consulta é medida: se o scan mantém a maior parte do que lê, a resposta são os números e nenhum SQL.
  • Três formas de índice a mais: de expressão, parcial no Firebird 5.0, e para resolver um ORDER BY.
  • O plano é explicado pelo motor. No Firebird 3.0 e posteriores chega o plano “explained”: nomes dos índices e quantos segmentos a consulta realmente alcança.
  • Tudo na edição gratuita, somente leitura, do Firebird 2.5 ao 5.0.
  • Repositório: github.com/danieleteti/mcp-firebird

Se você não conhece o projeto, o artigo de apresentação explica o que é e como instalar. Resumindo: um executável escrito em Delphi que o seu assistente de IA inicia sozinho e com o qual conversa via stdin/stdout, sem serviços nem portas abertas.

A escala do remédio

Até a 0.3.x o consultor raciocinava sobre os metadados. Encontrava NATURAL no plano, extraía da SQL as colunas dos predicados, propunha um índice onde ainda não havia nenhum. Com as informações que tinha, era uma resposta razoável, e na maioria das vezes também era a certa.

Fora do raciocínio ficava tudo aquilo que os metadados não contêm:

  • quantas linhas aquele scan seleciona de verdade?
  • as estatísticas de um índice ainda descrevem os dados de hoje?
  • quais formas de índice o motor que você tem em produção permite, só B-tree, ou também índices parciais e de expressão?

Essas avaliações só dá para fazer medindo, e para medir é preciso rodar a consulta. A 0.4.0 dá esse passo.

A regra que o consultor segue é esta:

Um índice não é grátis: é gravado a cada INSERT, UPDATE e DELETE durante toda a sua vida. Portanto proponha o remédio mais barato que explique o plano, e esteja disposto a não propor nada.

O consultor testa os degraus em ordem de custo e para no primeiro que explica o que viu. SET STATISTICS INDEX atualiza a seletividade de um índice que você já tem: não deixa nada para trás, é seguro mesmo sob carga concorrente, e num número embaraçoso de casos é tudo o que era preciso. ALTER INDEX ... ACTIVE reativa um índice desabilitado, e o custo de criação você já pagou faz tempo. CREATE INDEX vem por último, porque é o único dos três que você continua pagando a cada escrita enquanto ele existir.

O scan que é o plano certo

Para dizer “esse scan está bom” é preciso saber quanto ele produz, e esse número não existe nos metadados. Por isso a 0.4.0 executa a consulta: uma única vez, dentro da transação read-only, e somente quando o plano mostra um scan ou um sort.

Um scan que mantém a maior parte do que lê, acima de cerca de 20% de seletividade, é o plano correto. O mesmo vale para uma tabela que cabe em poucas páginas. Nesses casos a resposta traz a medição e nenhum statement para executar:

Table ORDERS is scanned when filtered by STATE, and that is the right plan:
the scan returns 89.8% of what it reads (18000 of 20048 records).

Já aviso o preço: numa tabela grande, uma chamada que antes só lia metadados agora faz uma varredura completa. É esse o custo de ter um número no lugar de um palpite.

As estatísticas vêm antes da estrutura

O Firebird calcula a seletividade de um índice quando você o cria, e nunca a recalcula sozinho. A tabela cresce, a distribuição muda, e o otimizador continua planejando sobre uma fotografia antiga. Então um índice que o otimizador ignora pode estar sendo deixado de lado por culpa das estatísticas, não porque seja inútil.

A 0.4.0 compara a seletividade armazenada com a real antes de dizer que um índice não serve, e quando os dois números divergem responde SET STATISTICS INDEX. Mesma checagem dentro do fb_suggest_index_drops, que antes de propor um DROP INDEX verifica a seletividade real: um índice que trabalha com estatísticas velhas merece atualização, não a lixeira. Também há um novo goal para optimization_goal, no_stale_index_statistics, o único dos quatro cuja primeira ação não escreve nada estrutural.

Uma sessão de verdade, com o Claude Desktop

Até aqui, exemplos. Esta em vez disso é uma sessão real que eu mesmo conduzi: Claude Desktop, MCP Firebird configurado num motor Firebird 5.0.4, o banco de teste do repositório. Não reconstruí nada para o artigo, só cortei onde se repetia.

A conversa abaixo está em inglês por simplicidade: com o MCP Firebird o assistente responde no idioma em que você escreve, então a mesma pergunta feita em português teria tido uma resposta em português.

👤 You: Why is SELECT * FROM CUSTOMERS WHERE UPPER(NAME) = 'ROSSI' slow even though NAME has an index?
🤖 Assistant: The index isn't the problem, the UPPER() around it is. The plan says it plainly:
PLAN (CUSTOMERS NATURAL)

Select Expression
    -> Filter
        -> Table "CUSTOMERS" Full Scan

Full scan: 5004 records read sequentially to return 0 rows (55 pages read, 5437 fetched, 9 ms).

🤖 A B-tree index on NAME stores the values of the column, Rossi, rossi, ROSSI, Bianchi, sorted by those literal values. The predicate doesn't ask about NAME, it asks about UPPER(NAME), a value that exists nowhere in the index. The optimizer has no way to translate "where is UPPER(NAME) = 'ROSSI'?" into a range on a tree keyed by the raw column, so it gives up and scans.
🤖 Think of a phone book sorted by surname: useless if your question is "find everyone whose surname has 5 letters." The book is sorted, just not by the thing you're asking about. Any function wrapped around the indexed column has the same effect, UPPER, SUBSTRING, CAST, EXTRACT, string concatenation. This is what people mean by a non-sargable predicate.

O assistente já tinha lido a estrutura da tabela, e a cita para dar peso ao diagnóstico:

Column Type Nullable
CUSTOMER_ID INTEGER No
NAME VARCHAR(400) No
CITY VARCHAR(240) Yes
STATUS CHAR(4) Yes

Índices: IDX_CUST_CITY (CITY, INACTIVE), IDX_CUST_NAME (NAME), IDX_CUST_NAME_CITY (NAME, CITY), IDX_CUST_STATUS (STATUS), RDB$PRIMARY1 (CUSTOMER_ID, chave primária).

Depois vêm duas correções, não uma só:

🤖 Fix 1, mecânico:
CREATE INDEX IDX_CUSTOMERS_UPPER_NAME ON CUSTOMERS COMPUTED BY (UPPER(NAME));
SET STATISTICS INDEX IDX_CUSTOMERS_UPPER_NAME;
🤖 Supported since Firebird 2.0. One sharp edge: the match is textual and exact. UPPER(NAME) and UPPER(TRIM(NAME)) are different index entries, and there's no multi-segment form for expression indexes. If some call sites trim and others don't, half the calls silently fall back to a full scan.
🤖 Fix 2, de domínio, preferível: se a comparação sem maiúsculas é uma propriedade da coluna e não só desta consulta, mova para a collation:
ALTER TABLE CUSTOMERS ALTER NAME TYPE VARCHAR(400)
  CHARACTER SET UTF8 COLLATE UNICODE_CI;
🤖 Then WHERE NAME = 'rossi' matches directly, IDX_CUST_NAME gets used, and no caller has to remember to wrap anything. Check first the current collation of NAME: if it's already _CI, UPPER() is pure overhead and an expression index would just be a redundant structure to pay for.
🤖 Two things while I was in there: IDX_CUST_NAME is fully contained in IDX_CUST_NAME_CITY as its leading segment, so the standalone index is largely redundant, a cost on every INSERT probably paid for nothing. And IDX_CUST_CITY is INACTIVE: still maintained on every write, but the optimizer won't consider it. Reactivate it or drop it.
🤖 One more thing: the query returned 0 rows. If ROSSI was expected to be in there, the slowness is the second problem, not the first.

A última linha foi o que me convenceu a fechar o artigo com esta sessão em vez de um exemplo inventado: a ferramenta tinha resolvido o problema técnico e depois parou para me avisar que talvez o problema de verdade fosse outro, o dado que eu procurava não existia. Nenhuma ferramenta faz isso sozinha se você não pediu.

Fechamento, ainda na mesma resposta:

🤖 What this analysis does not rule out: an access plan is structural evidence, it names the path the optimizer chose. It can't show what the query costs while other work is running, or a degradation that only appears above a certain concurrency. A clean plan is not a fast query. And this advice comes from the schema and one execution of one query, not from the live workload: an index that helps this query may hurt three others nobody asked about, because every index costs every INSERT.

Índices parciais e para o ORDER BY

Existem mais duas formas de índice na 0.4.0. Os índices parciais, introduzidos pelo Firebird na 5.0: pegue WHERE STATE = 'ACTIVE' AND TAG = ?: o valor de TAG muda a cada chamada, enquanto STATE = 'ACTIVE' é fixo e sempre seleciona o mesmo subconjunto de linhas. Um índice normal sobre TAG indexa a tabela inteira, incluindo as linhas não ACTIVE que essa consulta nunca vai olhar. Um índice parcial indexa só as que você precisa:

CREATE INDEX IDX_CUSTOMERS_ACTIVE_TAG ON CUSTOMERS (TAG) WHERE STATE = 'ACTIVE';

Se as linhas ACTIVE forem 3% da tabela, esse índice fica umas trinta vezes menor, e o INSERT de uma linha não ACTIVE nem chega a tocar nele. O consultor propõe as duas formas, a normal e a parcial. No Firebird 4.0 e anteriores a cláusula WHERE não existe, então chega só o índice normal, com uma nota sobre o que você ganharia passando para a 5.0.

Por fim o ORDER BY. O fb_analyze_query sinaliza os SORT externos desde a primeira versão, e agora o fb_suggest_indexes propõe o índice que os elimina, com os segmentos na direção certa, porque o Firebird não consegue percorrer ao contrário um índice ascendente. Se você pedir ORDER BY a ASC, b DESC ele te diz que nenhum índice único pode atender, em vez de propor um que o motor vai ignorar.

O plano é explicado pelo motor

No Firebird 3.0 e posteriores, o fb_analyze_query captura o plano “explained” com SET EXPLAIN ON. Antes a tabela por trás de um alias era deduzida com uma regex sobre a FROM, agora é o próprio motor que declara os nomes dos índices, quanto de cada índice a consulta realmente usa, a estratégia de join e o mapa alias-tabela.

Com esses detalhes, o consultor encontra dois problemas que antes nem conseguia enxergar.

O primeiro é sobre índices compostos. Você tem um índice sobre (CITY, NAME, STATUS) e uma consulta que filtra só por CITY e NAME: o plano “explained” mostra que o Firebird usou apenas os dois primeiros segmentos do índice, nunca o terceiro. O STATUS é gravado dentro do índice a cada INSERT e UPDATE, mas nenhuma consulta desse tipo o lê. É um custo de escrita que não compra nada, e só fica visível porque agora você sabe quantos segmentos a consulta realmente alcança.

O segundo é sobre joins. Antes só se via o plano geral. Agora, se o motor faz varredura completa tanto da tabela à esquerda quanto da tabela à direita do join, em vez de usar um índice em pelo menos uma das duas, o consultor avisa e diz qual tabela.

No Firebird 2.5 essa parte simplesmente não roda: SET EXPLAIN não existe nessa versão, e mandá-lo mesmo assim quebraria o script com um comando que o motor não conhece. Então ali o plano continua sendo aquele deduzido da consulta, como antes.

Se você esquecer de dar um valor a um parâmetro

Quando você analisa uma consulta com um placeholder, tipo SELECT * FROM CUSTOMERS WHERE CITY = ?, e não passa um valor para ele, o fb_analyze_query agora se recusa a executá-la e avisa. Antes, executava mesmo assim: o parâmetro acabava associado a NULL, CITY = NULL não encontra nada por definição, e o relatório voltava com “0 linhas lidas” numa tabela inteira varrida de ponta a ponta, um número que parecia a prova definitiva de que você precisava de um índice, quando na verdade só estava medindo uma consulta diferente daquela que você queria testar de fato.

Mais duas correções menores no mesmo pacote. A primeira: uma comparação de desigualdade como A <> 5 não propõe mais um índice. Um B-tree não ajuda aqui: para excluir um único valor o motor precisa ler quase todo o índice de qualquer jeito, então o Firebird prefere o scan, e propor o índice seria tempo perdido. A segunda: a checagem de estatísticas velhas agora cobre também os índices com mais de uma coluna, não só os de coluna única.

Como testar

Baixe MCPFirebird-0.4.0-win64.zip da release no GitHub, copie .env.example para .env e aponte firebird.client_lib para a fbclient.dll da sua instalação. Ela não vem no zip, de propósito: só você sabe com qual servidor está falando. Depois registre o exe como servidor MCP stdio no seu agente.

A partir daí, coloque na frente dele a consulta mais lenta que você tem e pergunte qual índice é necessário. A resposta mais interessante que você pode receber é “nenhum, e aqui estão os números”.

O changelog completo lista toda a 0.4.0, e no repositório está o design doc do consultor, com as medições de onde vêm os limiares.

Comments

comments powered by Disqus