MCP Firebird 0.4.0: el asesor que sabe decirte que no crees el índice
🇬🇧 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.
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_indexesreescrito 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:
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.
SELECT * FROM CUSTOMERS WHERE UPPER(NAME) = 'ROSSI' slow even though NAME has an index?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).
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:
CREATE INDEX IDX_CUSTOMERS_UPPER_NAME ON CUSTOMERS COMPUTED BY (UPPER(NAME));
SET STATISTICS INDEX IDX_CUSTOMERS_UPPER_NAME;
ALTER TABLE CUSTOMERS ALTER NAME TYPE VARCHAR(400)
CHARACTER SET UTF8 COLLATE UNICODE_CI;
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.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:
Í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