Become a member!

MCP Firebird 0.4.0: el asesor que sabe decirte que no crees el índice

🌐
Este artículo también está disponible en otros idiomas:
🇬🇧 English  •  🇮🇹 Italiano  •  🇩🇪 Deutsch  •  🇧🇷 Português

La 0.4.0 de MCP Firebird reescribe el asesor de índices. Ahora mide la consulta antes de hablar, propone el remedio que cuesta menos, conoce índices sobre expresión, parciales y para el ORDER BY, y cuando el escaneo es la elección correcta responde con los números y ninguna SQL que ejecutar.

Logo de MCP Firebird, el servidor MCP que permite a los asistentes de IA diagnosticar bases de datos Firebird.

Tienes una consulta lenta. El plan dice PLAN (ORDERS NATURAL), es decir, escaneo completo de la tabla ORDERS, y sobre la columna filtrada no hay ningún índice. El consejo que vas a recibir, de un humano, de una IA o de un “DBA dominguero”, es casi siempre el mismo: crea el índice.

Mira los números, sin embargo. Ese escaneo lee 20.048 registros y devuelve 18.000. El 89,8% de lo que toca acaba en el resultado. Un índice ahí te obligaría a hacer dos lecturas en vez de una para casi cada fila de la tabla, y lo pagarías con una escritura más en cada INSERT, UPDATE y DELETE, desde hoy hasta el día en que alguien lo borre. Y ya sabes que no lo va a borrar nadie, porque “a lo mejor se rompe algo”. Una operación pensada para mejorar el rendimiento acabaría empeorándolo, durante toda la vida de la aplicación.

En este caso el escaneo era el plan correcto. El consejo acertado era no hacer nada, todo lo contrario de “crea un índice”.

La 0.4.0 de MCP Firebird, publicada el 7 de agosto de 2026, le enseña a fb_suggest_indexes a decirte “así está bien” en vez de “crea este índice”.

En resumen

  • fb_suggest_indexes reescrito en torno a una escala de remedios, del más barato al más caro. Primero las estadísticas, luego la reactivación de un índice apagado, y solo al final un índice nuevo.
  • El escaneo puede ser el plan correcto. La consulta se mide: si el escaneo conserva la mayor parte de lo que lee, la respuesta son los números y ninguna SQL.
  • Tres formas de índice más: sobre expresión, parcial en Firebird 5.0, y para resolver un ORDER BY.
  • El plan lo explica el motor. En Firebird 3.0 y posteriores llega el plan “explained”: nombres de los índices y cuántos segmentos alcanza de verdad la consulta.
  • Todo en la edición gratuita, de solo lectura, en Firebird de la 2.5 a la 5.0.
  • Repositorio: github.com/danieleteti/mcp-firebird

Si no conoces el proyecto, el artículo de presentación explica qué es y cómo se instala. En resumen: un ejecutable escrito en Delphi que tu asistente de IA arranca por sí mismo y con el que habla por stdin/stdout, sin servicios ni puertos abiertos.

La escala del remedio

Hasta la 0.3.x el asesor razonaba sobre los metadatos. Encontraba NATURAL en el plan, sacaba de la SQL las columnas de los predicados, proponía un índice donde todavía no había uno. Con la información que tenía era una respuesta razonable, y la mayoría de las veces también era la acertada.

Fuera de ese razonamiento quedaba todo lo que los metadatos no contienen:

  • ¿cuántas filas selecciona de verdad ese escaneo?
  • ¿las estadísticas de un índice siguen describiendo los datos de hoy?
  • ¿qué formas de índice permite el motor que tienes en producción, solo B-tree, o también índices parciales y sobre expresión?

Esas evaluaciones solo se pueden hacer midiendo, y para medir hay que ejecutar la consulta. La 0.4.0 da ese paso.

La regla que sigue el asesor es esta:

Un índice no es gratis: se escribe en cada INSERT, UPDATE y DELETE durante toda su vida. Así que propón el remedio más barato que explique el plan, y está dispuesto a no proponer nada.

El asesor prueba los peldaños en orden de coste y se detiene en el primero que explica lo que ha visto. SET STATISTICS INDEX actualiza la selectividad de un índice que ya tienes: no deja nada detrás, es seguro incluso bajo carga concurrente, y en un número embarazoso de casos es todo lo que hacía falta. ALTER INDEX ... ACTIVE reactiva un índice deshabilitado, y el coste de crearlo ya lo pagaste hace tiempo. CREATE INDEX llega el último, porque es el único de los tres que sigues pagando en cada escritura mientras exista.

El escaneo que es el plan correcto

Para decir “este escaneo está bien” hace falta saber cuánto produce, y ese número no está en los metadatos. Por eso la 0.4.0 ejecuta la consulta: una sola vez, dentro de la transacción de solo lectura, y solo cuando el plan muestra un escaneo o un sort.

Un escaneo que conserva la mayor parte de lo que lee, por encima de aproximadamente el 20% de selectividad, es el plan correcto. Lo mismo vale para una tabla que cabe en pocas páginas. En esos casos la respuesta contiene la medida y ninguna sentencia que ejecutar:

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).

El precio lo digo ya: en una tabla grande, una llamada que antes solo leía metadatos ahora hace un escaneo completo. Eso es lo que cuesta tener un número en vez de una suposición.

Las estadísticas van antes que la estructura

Firebird calcula la selectividad de un índice cuando lo creas, y nunca la recalcula por su cuenta. La tabla crece, la distribución cambia, y el optimizador sigue planificando sobre una fotografía vieja. Así que un índice que el optimizador ignora podría estar descartado por culpa de las estadísticas, no porque sea inútil.

La 0.4.0 compara la selectividad guardada con la real antes de decir que un índice no sirve, y cuando las dos cifras difieren responde SET STATISTICS INDEX. Mismo control dentro de fb_suggest_index_drops, que antes de proponer un DROP INDEX verifica la selectividad real: un índice que trabaja con estadísticas viejas merece la actualización, no la papelera. También hay un nuevo goal para optimization_goal, no_stale_index_statistics, el único de los cuatro cuyo primer movimiento no escribe nada estructural.

Una sesión real, con Claude Desktop

Hasta aquí, ejemplos. Esto en cambio es una sesión real que tuve yo: Claude Desktop, MCP Firebird configurado sobre un motor Firebird 5.0.4, la base de datos de prueba del repositorio. No la reconstruí para el artículo, solo la recorté donde se repetía.

La conversación de abajo está en inglés por simplicidad: con MCP Firebird el asistente responde en el idioma en el que le escribes, así que la misma pregunta hecha en español habría tenido una respuesta en español.

👤 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.

El asistente ya había leído la estructura de la tabla, y la cita para darle peso al 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, clave primaria).

Luego llegan dos correcciones, no una sola:

🤖 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 dominio, preferible: si la comparación sin mayúsculas es una propiedad de la columna y no de esta única consulta, muévela a la 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.

La última línea es la que me convenció de cerrar el artículo con esta sesión en vez de con un ejemplo inventado: la herramienta había resuelto el problema técnico y luego se detuvo a hacerme notar que quizás el problema real era otro, el dato que buscaba no estaba. Ninguna herramienta hace esto por su cuenta si no le has dicho que lo haga.

Cierre, siempre en la misma respuesta:

🤖 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 parciales y para el ORDER BY

Hay otras dos formas de índice en la 0.4.0. Los índices parciales, introducidos por Firebird en la 5.0: toma WHERE STATE = 'ACTIVE' AND TAG = ?: el valor de TAG cambia en cada llamada, mientras que STATE = 'ACTIVE' es fijo y selecciona siempre el mismo subconjunto de filas. Un índice normal sobre TAG indexa toda la tabla, incluidas las filas no ACTIVE que esa consulta nunca va a tocar. Un índice parcial indexa solo las que necesitas:

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

Si las filas ACTIVE son el 3% de la tabla, ese índice es unas treinta veces más pequeño, y el INSERT de una fila no ACTIVE ni siquiera lo toca. El asesor te propone las dos formas, la normal y la parcial. En Firebird 4.0 y anteriores la cláusula WHERE no existe, así que llega solo el índice normal, con una nota sobre lo que ganarías pasando a la 5.0.

Por último el ORDER BY. fb_analyze_query señala los SORT externos desde la primera versión, y ahora fb_suggest_indexes propone el índice que los elimina, con los segmentos en la dirección correcta, porque Firebird no puede recorrer al revés un índice ascendente. Si pides ORDER BY a ASC, b DESC te dice que ningún índice único puede servirte, en lugar de proponerte uno que el motor va a ignorar.

El plan lo explica el motor

En Firebird 3.0 y posteriores, fb_analyze_query captura el plan “explained” con SET EXPLAIN ON. Antes la tabla detrás de un alias se deducía con una expresión regular sobre el FROM, ahora es el propio motor el que declara los nombres de los índices, cuánto de cada índice usa de verdad la consulta, la estrategia de join y el mapa alias-tabla.

Con estos detalles, el asesor encuentra dos problemas que antes ni siquiera podía ver.

El primero tiene que ver con los índices compuestos. Tienes un índice sobre (CITY, NAME, STATUS) y una consulta que filtra solo por CITY y NAME: el plan “explained” muestra que Firebird usó solo los dos primeros segmentos del índice, nunca el tercero. STATUS se escribe dentro del índice en cada INSERT y UPDATE, pero ninguna consulta de este tipo lo lee jamás. Es un coste de escritura que no compra nada, y es visible solo porque ahora sabes cuántos segmentos alcanza de verdad la consulta.

El segundo tiene que ver con los joins. Antes solo se veía el plan global. Ahora, si el motor escanea por completo tanto la tabla de la izquierda como la de la derecha del join, en vez de usar un índice sobre al menos una de las dos, el asesor te lo dice y especifica cuál tabla.

En Firebird 2.5 esta parte simplemente no arranca: SET EXPLAIN no existe en esa versión, y enviarlo de todos modos rompería el script con un comando que el motor no conoce. Así que ahí el plan sigue siendo el deducido de la consulta, como antes.

Si olvidas darle un valor a un parámetro

Cuando analizas una consulta con un marcador de posición, del tipo SELECT * FROM CUSTOMERS WHERE CITY = ?, y no le pasas un valor, fb_analyze_query ahora se niega a ejecutarla y te lo dice. Antes, en cambio, la ejecutaba de todos modos: el parámetro acababa asociado a NULL, CITY = NULL no encuentra nada por definición, y el informe volvía con “0 filas leídas” sobre una tabla entera recorrida de principio a fin, un número que parecía la prueba definitiva de que necesitabas un índice cuando en realidad solo estaba midiendo una consulta distinta de la que de verdad querías probar.

Otras dos correcciones menores en el mismo paquete. La primera: una comparación de desigualdad como A <> 5 ya no propone un índice. Un B-tree aquí no ayuda: para excluir un solo valor el motor tiene que leer de todos modos casi todo el índice, así que Firebird prefiere el escaneo, y proponer el índice habría sido tiempo perdido. La segunda: el control sobre las estadísticas viejas ahora cubre también los índices sobre varias columnas, no solo los de una sola columna.

Cómo probarlo

Descarga MCPFirebird-0.4.0-win64.zip desde la release en GitHub, copia .env.example a .env y apunta firebird.client_lib a la fbclient.dll de tu instalación. En el zip no la encuentras, a propósito: solo tú sabes con qué servidor estás hablando. Luego registra el exe como servidor MCP stdio en tu agente.

Llegados a ese punto, ponle delante la consulta más lenta que tengas y pregúntale qué índice hace falta. La respuesta más interesante que puedes recibir es “ninguno, y estos son los números”.

El changelog completo recoge toda la 0.4.0, y en el repositorio está el design doc del asesor con las medidas de las que salen los umbrales.

Comments

comments powered by Disqus