MCP Firebird 0.4.0: l'advisor che sa dirti di non creare l'indice
🇬🇧 English • 🇪🇸 Español • 🇩🇪 Deutsch • 🇧🇷 Português
La 0.4.0 di MCP Firebird riscrive il consigliere di indici. Adesso misura la query prima di parlare, propone il rimedio che costa meno, conosce indici su espressione, parziali e per l'ORDER BY, e quando lo scan è la scelta giusta risponde con i numeri e nessuna SQL da eseguire.
Hai una query lenta. Il piano dice PLAN (ORDERS NATURAL), cioè scansione completa della tabella ORDERS, e sulla colonna filtrata non c’è nessun indice. Il consiglio che riceverai, da un umano, da un’AI o da un “DBA della domenica” è quasi sempre lo stesso: crea l’indice.
Guarda i numeri, però. Quello scan legge 20.048 record e ne restituisce 18.000. L'89,8% di quello che tocca finisce nel risultato. Un indice lì ti farebbe fare due letture invece di una per quasi ogni riga della tabella, e lo pagheresti con una scrittura in più a ogni INSERT, UPDATE e DELETE, da oggi fino al giorno in cui qualcuno lo cancellerà. Sai bene che non lo cancellerà nessuno, perché “potrebbe rompersi qualcosa”. Un’operazione pensata per migliorare le prestazioni finirebbe per peggiorarle, per tutta la vita dell’applicazione.
In questo caso lo scan era il piano giusto. Il consiglio giusto era non fare niente, altro che “crea un indice”.
La 0.4.0 di MCP Firebird, uscita il 7 agosto 2026, insegna a fb_suggest_indexes a dirti “va bene così” invece di “crea questo indice”.
In breve
fb_suggest_indexesriscritto attorno a una scala di rimedi, dal più economico al più caro. Prima le statistiche, poi la riattivazione di un indice spento, e solo alla fine un indice nuovo.- Lo scan può essere il piano giusto. La query viene misurata: se lo scan tiene la maggior parte di quello che legge, la risposta sono i numeri e nessuna SQL.
- Tre forme di indice in più: su espressione, parziale su Firebird 5.0, e per risolvere un
ORDER BY.- Il piano lo spiega il motore. Su Firebird 3.0 e successivi arriva il piano “explained”: nomi degli indici e quanti segmenti la query raggiunge davvero.
- Tutto nell’edizione gratuita, in sola lettura, su Firebird da 2.5 a 5.0.
- Repository: github.com/danieleteti/mcp-firebird
Se non conosci il progetto, l’articolo di presentazione spiega cos’è e come si installa. In breve: un eseguibile scritto in Delphi che il tuo assistente AI avvia da solo e con cui parla su stdin/stdout, senza servizi né porte aperte.
La scala del rimedio
Fino alla 0.3.x l’advisor ragionava sui metadati. Trovava NATURAL nel piano, ricavava dalla SQL le colonne dei predicati, proponeva un indice dove non ce n’era già uno. Con le informazioni che aveva era una risposta ragionevole, e il più delle volte era anche quella giusta.
Fuori dal ragionamento restava tutto quello che i metadati non contengono:
- quante righe seleziona davvero quello scan?
- le statistiche di un indice descrivono ancora i dati di oggi?
- quali forme di indice permette il motore che hai in produzione, solo B-tree, o anche indici parziali e su espressione?
Queste valutazioni puoi farle solo misurando, e per misurare bisogna far girare la query. La 0.4.0 fa questo passo.
La regola che l’advisor segue è questa:
L’advisor prova i gradini in ordine di costo e si ferma al primo che spiega quello che ha visto. SET STATISTICS INDEX aggiorna la selettività di un indice che hai già: non lascia niente dietro di sé, è sicuro anche sotto carico concorrente, e in un numero di casi imbarazzante è tutto quello che serviva. ALTER INDEX ... ACTIVE riattiva un indice disabilitato, e il costo di creazione l’hai già pagato tempo fa. CREATE INDEX viene per ultimo, perché è l’unico dei tre che continui a pagare a ogni scrittura finché esiste.
Lo scan che è il piano giusto
Per dire “questo scan va bene” serve sapere quanto produce, e nei metadati quel numero non c’è. Per questo la 0.4.0 esegue la query: una volta sola, dentro la transazione read-only, e solo quando il piano mostra uno scan o un sort.
Uno scan che tiene la maggior parte di quello che legge, sopra circa il 20% di selettività, è il piano corretto. Lo stesso vale per una tabella che sta in poche pagine. In quei casi la risposta contiene la misura e nessuno statement da eseguire:
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).
Il prezzo lo dichiaro subito: su una tabella grande, una chiamata che prima leggeva solo metadati adesso fa una scansione completa. È quello che costa avere un numero al posto di una supposizione.
Le statistiche vengono prima della struttura
Firebird calcola la selettività di un indice quando lo crei, e non la ricalcola mai da solo. La tabella cresce, la distribuzione cambia, e l’ottimizzatore continua a pianificare su una fotografia vecchia. Quindi un indice che l’ottimizzatore ignora potrebbe essere trascurato per colpa delle statistiche, non perché sia inutile.
La 0.4.0 confronta la selettività memorizzata con quella reale prima di dire che un indice non serve, e quando le due cifre divergono risponde SET STATISTICS INDEX. Stesso controllo dentro fb_suggest_index_drops, che prima di proporre un DROP INDEX verifica la selettività reale: un indice che lavora su statistiche vecchie merita l’aggiornamento, non il cestino. C’è anche un nuovo goal per optimization_goal, no_stale_index_statistics, l’unico dei quattro la cui prima mossa non scrive niente di strutturale.
Una sessione vera, con Claude Desktop
Fin qui esempi. Questa invece è una sessione reale che ho tenuto io: Claude Desktop, MCP Firebird configurato su un motore Firebird 5.0.4, il database di test del repository. Non l’ho ricostruita per l’articolo, l’ho solo tagliata dove si ripeteva.
La conversazione qui sotto è in inglese per semplicità: con MCP Firebird l’assistente risponde nella lingua in cui scrivi, quindi la stessa domanda fatta in italiano avrebbe avuto una risposta in italiano.
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).
L’assistente aveva già letto la struttura della tabella, e la cita per dare peso alla diagnosi:
| Column | Type | Nullable |
|---|---|---|
| CUSTOMER_ID | INTEGER | No |
| NAME | VARCHAR(400) | No |
| CITY | VARCHAR(240) | Yes |
| STATUS | CHAR(4) | Yes |
Indici: IDX_CUST_CITY (CITY, INACTIVE), IDX_CUST_NAME (NAME), IDX_CUST_NAME_CITY (NAME, CITY), IDX_CUST_STATUS (STATUS), RDB$PRIMARY1 (CUSTOMER_ID, chiave primaria).
Poi arrivano due correzioni, non 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.L’ultima riga è quella che mi ha convinto a chiudere l’articolo con questa sessione invece che con un esempio inventato: lo strumento aveva risolto il problema tecnico e poi si è fermato a farmi notare che forse il problema vero era un altro, il dato che cercavo non c’era. Nessun tool lo fa da solo se non gli hai detto di farlo.
Chiusura, sempre nella stessa risposta:
Indici parziali e per l’ORDER BY
Ci sono altre due forme di indice nella 0.4.0. Gli indici parziali, introdotti da Firebird con la 5.0: prendi WHERE STATE = 'ACTIVE' AND TAG = ?: il valore di TAG cambia a ogni chiamata, mentre STATE = 'ACTIVE' è fisso e seleziona sempre lo stesso sottoinsieme di righe. Un indice normale su TAG indicizza tutta la tabella, comprese le righe non ACTIVE che quella query non guarderà mai. Un indice parziale indicizza solo quelle che ti servono:
CREATE INDEX IDX_CUSTOMERS_ACTIVE_TAG ON CUSTOMERS (TAG) WHERE STATE = 'ACTIVE';
Se le righe ACTIVE sono il 3% della tabella, quell’indice è una trentina di volte più piccolo, e l’INSERT di una riga non ACTIVE non lo tocca nemmeno. L’advisor ti propone entrambe le forme, quella normale e quella parziale. Su Firebird 4.0 e precedenti la clausola WHERE non esiste, quindi arriva solo l’indice normale, con una nota su cosa avresti in più passando alla 5.0.
Infine l’ORDER BY. fb_analyze_query segnala i SORT esterni dalla prima release, e adesso fb_suggest_indexes propone l’indice che li elimina, con i segmenti nella direzione giusta, perché Firebird non può percorrere al contrario un indice ascendente. Se chiedi ORDER BY a ASC, b DESC ti dice che nessun indice singolo può servirti, invece di proporne uno che il motore ignorerà.
Il piano lo spiega il motore
Su Firebird 3.0 e successivi, fb_analyze_query cattura il piano “explained” con SET EXPLAIN ON. Prima la tabella dietro un alias si deduceva con una regex sulla FROM, adesso è il motore stesso a dichiarare i nomi degli indici, quanto di ciascun indice la query usa davvero, la strategia di join e la mappa alias-tabella.
Con questi dettagli, l’advisor trova due problemi che prima non poteva nemmeno vedere.
Il primo riguarda gli indici composti. Hai un indice su (CITY, NAME, STATUS) e una query che filtra solo su CITY e NAME: il piano “explained” mostra che Firebird ha usato solo i primi due segmenti dell’indice, mai il terzo. STATUS viene scritto dentro l’indice a ogni INSERT e UPDATE, ma nessuna query di questo tipo lo legge mai. È un costo di scrittura che non compra niente, ed è visibile solo perché adesso sai quanti segmenti la query raggiunge davvero.
Il secondo riguarda i join. Prima si vedeva solo il piano complessivo. Ora, se il motore scansiona per intero sia la tabella a sinistra sia quella a destra del join, invece di usare un indice su almeno una delle due, l’advisor te lo dice e specifica quale tabella.
Su Firebird 2.5 questa parte semplicemente non parte: SET EXPLAIN non esiste su quella versione, e mandarlo comunque romperebbe lo script con un comando che il motore non conosce. Quindi lì il piano resta quello dedotto dalla query, come prima.
Se dimentichi di dare un valore a un parametro
Quando analizzi una query con un placeholder, tipo SELECT * FROM CUSTOMERS WHERE CITY = ?, e non gli passi un valore, fb_analyze_query adesso si rifiuta di eseguirla e te lo dice. Prima invece la eseguiva comunque: il parametro finiva associato a NULL, CITY = NULL non trova niente per definizione, e il report tornava con “0 righe lette” su una tabella intera scandita da cima a fondo, un numero che sembrava la prova definitiva che ti serviva un indice mentre stava solo misurando una query diversa da quella che volevi davvero testare.
Altre due correzioni minori nello stesso pacchetto. La prima: un confronto di disuguaglianza come A <> 5 non propone più un indice. Un B-tree qui non aiuta: per escludere un solo valore il motore deve comunque leggere quasi tutto l’indice, quindi Firebird preferisce lo scan, e proporre l’indice sarebbe stato tempo perso. La seconda: il controllo sulle statistiche vecchie ora copre anche gli indici su più colonne, non solo quelli su una colonna sola.
Come si prova
Scarica MCPFirebird-0.4.0-win64.zip dalla release su GitHub, copia .env.example in .env e punta firebird.client_lib alla fbclient.dll della tua installazione. Nello zip non la trovi, di proposito: solo tu sai con quale server stai parlando. Poi registra l’exe come server MCP stdio nel tuo agente.
A quel punto mettigli davanti la query più lenta che hai e chiedigli che indice serve. La risposta più interessante che puoi ricevere è “nessuno, e questi sono i numeri”.
Il changelog completo elenca tutta la 0.4.0, e nel repository c’è il design doc dell’advisor con le misure da cui vengono le soglie.
Comments
comments powered by Disqus