Become a member!

MCP Firebird 0.4.0: der Advisor, der dir sagt, wenn du keinen Index brauchst

🌐
Dieser Artikel ist auch in anderen Sprachen verfügbar:
🇬🇧 English  •  🇮🇹 Italiano  •  🇪🇸 Español  •  🇧🇷 Português

Die 0.4.0 von MCP Firebird schreibt den Index-Berater neu. Sie misst die Abfrage jetzt, bevor sie etwas vorschlägt, nennt die günstigste Abhilfe, kennt Indizes auf Ausdrücke, partielle Indizes und Indizes für ORDER BY, und wenn der Scan die richtige Wahl ist, antwortet sie mit Zahlen und ohne SQL zum Ausführen.

Logo von MCP Firebird, dem MCP-Server, mit dem KI-Assistenten Firebird-Datenbanken diagnostizieren können.

Eine Abfrage ist langsam. Der Plan sagt PLAN (ORDERS NATURAL), also ein kompletter Scan der Tabelle ORDERS, und auf der gefilterten Spalte gibt es keinen Index. Der Rat, den du bekommst, von einem Menschen, einer KI oder einem “Sonntags-DBA”, ist fast immer derselbe: leg den Index an.

Sieh dir aber die Zahlen an. Dieser Scan liest 20.048 Datensätze und gibt 18.000 davon zurück. 89,8% von dem, was er anfasst, landet im Ergebnis. Ein Index würde dich dort zwei Lesezugriffe statt einem für fast jede Zeile der Tabelle kosten, und du würdest ihn mit einer zusätzlichen Schreiboperation bei jedem INSERT, UPDATE und DELETE bezahlen, von heute bis zu dem Tag, an dem ihn jemand löscht. Du weißt genau, dass ihn niemand löschen wird, weil “dann könnte etwas kaputtgehen”. Eine Maßnahme, die für bessere Performance gedacht war, würde sie für die gesamte Lebensdauer der Anwendung verschlechtern.

In diesem Fall war der Scan der richtige Plan. Der richtige Rat war, nichts zu tun, alles andere als “leg einen Index an”.

Die 0.4.0 von MCP Firebird, erschienen am 7. August 2026, bringt fb_suggest_indexes bei, “so ist es gut” zu sagen statt “leg diesen Index an”.

Kurz gesagt

  • fb_suggest_indexes neu geschrieben, entlang einer Stufenleiter von Abhilfen, von günstig zu teuer. Zuerst die Statistik, dann die Reaktivierung eines deaktivierten Index, und erst ganz am Ende ein neuer Index.
  • Der Scan kann der richtige Plan sein. Die Abfrage wird gemessen: behält der Scan den Großteil von dem, was er liest, besteht die Antwort aus Zahlen und keinem SQL.
  • Drei zusätzliche Indexformen: auf Ausdrücke, partiell auf Firebird 5.0, und zur Auflösung eines ORDER BY.
  • Den Plan erklärt die Engine. Auf Firebird 3.0 und neuer kommt der “explained”-Plan: Namen der Indizes und wie viele Segmente die Abfrage wirklich erreicht.
  • Alles in der kostenlosen Edition, rein lesend, auf Firebird von 2.5 bis 5.0.
  • Repository: github.com/danieleteti/mcp-firebird

Wenn du das Projekt nicht kennst: der Vorstellungsartikel erklärt, was es ist und wie man es installiert. Kurz gesagt: eine in Delphi geschriebene ausführbare Datei, die dein KI-Assistent selbst startet und mit der er über stdin/stdout spricht, ohne Dienste und ohne offene Ports.

Die Stufenleiter der Abhilfe

Bis zur 0.3.x hat der Advisor mit den Metadaten gearbeitet. Er fand NATURAL im Plan, leitete aus dem SQL die Spalten der Prädikate ab, schlug einen Index vor, wo noch keiner existierte. Mit den Informationen, die er hatte, war das eine vernünftige Antwort, und meistens war es auch die richtige.

Außerhalb dieser Überlegung blieb alles, was die Metadaten nicht enthalten:

  • wie viele Zeilen dieser Scan wirklich selektiert?
  • beschreiben die Statistiken eines Index noch die heutigen Daten?
  • welche Indexformen die Engine in deiner Produktion überhaupt erlaubt, nur B-Tree, oder auch partielle und Ausdrucksindizes?

Diese Einschätzungen kannst du nur durch Messen treffen, und um zu messen, muss die Abfrage laufen. Die 0.4.0 geht diesen Schritt.

Die Regel, der der Advisor folgt, lautet:

Ein Index ist nicht kostenlos: er wird bei jedem INSERT, UPDATE und DELETE sein ganzes Leben lang geschrieben. Schlage also die günstigste Abhilfe vor, die den Plan erklärt, und sei bereit, gar nichts vorzuschlagen.

Der Advisor probiert die Stufen in Kostenreihenfolge durch und hält bei der ersten an, die erklärt, was er gesehen hat. SET STATISTICS INDEX aktualisiert die Selektivität eines Index, den du schon hast: es hinterlässt nichts, ist auch unter gleichzeitiger Last sicher, und in einer peinlich hohen Zahl von Fällen war das schon alles, was gefehlt hat. ALTER INDEX ... ACTIVE reaktiviert einen deaktivierten Index, dessen Erstellungskosten du längst bezahlt hast. CREATE INDEX kommt zuletzt, weil es von den dreien das einzige ist, das du bei jeder Schreiboperation weiter bezahlst, solange es existiert.

Der Scan, der der richtige Plan ist

Um zu sagen “dieser Scan ist in Ordnung”, muss man wissen, was er liefert, und diese Zahl steht nicht in den Metadaten. Deshalb führt die 0.4.0 die Abfrage aus: einmal, innerhalb der Read-only-Transaktion, und nur wenn der Plan einen Scan oder einen Sort zeigt.

Ein Scan, der den Großteil von dem behält, was er liest, oberhalb von etwa 20% Selektivität, ist der korrekte Plan. Dasselbe gilt für eine Tabelle, die auf wenige Seiten passt. In diesen Fällen enthält die Antwort die Messung und kein Statement zum Ausführen:

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

Den Preis nenne ich gleich: bei einer großen Tabelle macht ein Aufruf, der vorher nur Metadaten gelesen hat, jetzt einen kompletten Scan. Das ist es, was es kostet, eine Zahl statt einer Vermutung zu haben.

Statistik kommt vor Struktur

Firebird berechnet die Selektivität eines Index, wenn du ihn anlegst, und aktualisiert sie danach nie von selbst. Die Tabelle wächst, die Verteilung ändert sich, und der Optimizer plant weiter auf einer alten Momentaufnahme. Ein Index, den der Optimizer ignoriert, kann also wegen der Statistik übergangen werden, nicht weil er nutzlos ist.

Die 0.4.0 vergleicht die gespeicherte mit der tatsächlichen Selektivität, bevor sie einen Index für überflüssig erklärt, und wenn die beiden Werte auseinanderlaufen, antwortet sie mit SET STATISTICS INDEX. Dieselbe Prüfung findet sich in fb_suggest_index_drops, das vor einem vorgeschlagenen DROP INDEX die tatsächliche Selektivität checkt: ein Index, der mit veralteten Statistiken arbeitet, verdient ein Update, nicht den Papierkorb. Dazu kommt ein neues Goal für optimization_goal, no_stale_index_statistics, das einzige der vier, dessen erster Schritt nichts Strukturelles schreibt.

Eine echte Sitzung mit Claude Desktop

Bis hierhin waren das Beispiele. Das Folgende ist dagegen eine echte Sitzung, die ich selbst geführt habe: Claude Desktop, MCP Firebird konfiguriert auf einer Firebird-5.0.4-Engine, die Testdatenbank des Repositorys. Ich habe sie für den Artikel nicht nachgebaut, nur dort gekürzt, wo sie sich wiederholte.

Das Gespräch unten ist der Einfachheit halber auf Englisch: Mit MCP Firebird antwortet der Assistent in der Sprache, in der du schreibst. Dieselbe Frage auf Deutsch hätte also auch eine Antwort auf Deutsch bekommen.

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

Der Assistent hatte die Tabellenstruktur schon gelesen und zitiert sie, um der Diagnose Gewicht zu geben:

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

Indizes: IDX_CUST_CITY (CITY, INACTIVE), IDX_CUST_NAME (NAME), IDX_CUST_NAME_CITY (NAME, CITY), IDX_CUST_STATUS (STATUS), RDB$PRIMARY1 (CUSTOMER_ID, Primärschlüssel).

Dann kommen zwei Korrekturen, nicht nur eine:

🤖 Fix 1, mechanisch:
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, auf Domänenebene, vorzuziehen: wenn der Vergleich ohne Berücksichtigung der Groß-/Kleinschreibung eine Eigenschaft der Spalte ist und nicht nur dieser einen Abfrage, verlagere sie in die 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.

Die letzte Zeile ist es, die mich überzeugt hat, den Artikel mit dieser Sitzung zu schließen statt mit einem erfundenen Beispiel: das Tool hatte das technische Problem gelöst und hielt dann inne, um mich darauf hinzuweisen, dass vielleicht ein anderes Problem das eigentliche war, der gesuchte Datensatz existierte gar nicht. Kein Tool tut das von sich aus, wenn du es ihm nicht aufgetragen hast.

Abschluss, immer noch in derselben Antwort:

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

Partielle Indizes und Indizes für ORDER BY

Es gibt zwei weitere Indexformen in der 0.4.0. Die partiellen Indizes, von Firebird mit der 5.0 eingeführt: nimm WHERE STATE = 'ACTIVE' AND TAG = ?: der Wert von TAG ändert sich bei jedem Aufruf, während STATE = 'ACTIVE' fest ist und immer dieselbe Teilmenge der Zeilen selektiert. Ein normaler Index auf TAG indiziert die ganze Tabelle, einschließlich der nicht-ACTIVE-Zeilen, die diese Abfrage nie ansehen wird. Ein partieller Index indiziert nur die, die du wirklich brauchst:

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

Wenn die ACTIVE-Zeilen 3% der Tabelle ausmachen, ist dieser Index etwa dreißigmal kleiner, und das INSERT einer nicht-ACTIVE-Zeile berührt ihn gar nicht erst. Der Advisor schlägt dir beide Formen vor, die normale und die partielle. Auf Firebird 4.0 und älter gibt es die WHERE-Klausel nicht, also kommt nur der normale Index, mit einem Hinweis, was du beim Umstieg auf 5.0 zusätzlich hättest.

Zum Schluss der ORDER BY. fb_analyze_query meldet externe SORT-Operationen schon seit dem ersten Release, und jetzt schlägt fb_suggest_indexes den Index vor, der sie eliminiert, mit den Segmenten in der richtigen Richtung, weil Firebird einen aufsteigenden Index nicht rückwärts durchlaufen kann. Fragst du nach ORDER BY a ASC, b DESC, sagt er dir, dass kein einzelner Index dich bedienen kann, statt dir einen vorzuschlagen, den die Engine ignorieren wird.

Den Plan erklärt die Engine

Auf Firebird 3.0 und neuer erfasst fb_analyze_query den “explained”-Plan mit SET EXPLAIN ON. Vorher wurde die Tabelle hinter einem Alias mit einer Regex auf die FROM-Klausel abgeleitet, jetzt gibt die Engine selbst die Namen der Indizes an, wie viel von jedem Index die Abfrage wirklich nutzt, die Join-Strategie und die Alias-Tabellen-Zuordnung.

Mit diesen Details findet der Advisor zwei Probleme, die er vorher nicht einmal sehen konnte.

Das erste betrifft zusammengesetzte Indizes. Du hast einen Index auf (CITY, NAME, STATUS) und eine Abfrage, die nur nach CITY und NAME filtert: der “explained”-Plan zeigt, dass Firebird nur die ersten beiden Segmente des Index genutzt hat, nie das dritte. STATUS wird bei jedem INSERT und UPDATE in den Index geschrieben, aber keine Abfrage dieser Art liest es je. Das sind Schreibkosten, die nichts einbringen, und sichtbar wird das nur, weil du jetzt weißt, wie viele Segmente die Abfrage wirklich erreicht.

Das zweite betrifft Joins. Vorher sah man nur den Gesamtplan. Jetzt, wenn die Engine sowohl die linke als auch die rechte Tabelle des Joins komplett scannt, statt einen Index auf mindestens einer der beiden zu nutzen, sagt dir der Advisor das und nennt die betroffene Tabelle.

Auf Firebird 2.5 läuft dieser Teil einfach nicht: SET EXPLAIN gibt es auf dieser Version nicht, und es trotzdem zu senden würde das Skript mit einem Befehl brechen, den die Engine nicht kennt. Dort bleibt der Plan also der aus der Abfrage abgeleitete, wie zuvor.

Wenn du vergisst, einem Parameter einen Wert zu geben

Wenn du eine Abfrage mit einem Platzhalter analysierst, etwa SELECT * FROM CUSTOMERS WHERE CITY = ?, und ihm keinen Wert übergibst, weigert sich fb_analyze_query jetzt, sie auszuführen, und sagt dir das. Vorher führte es sie trotzdem aus: der Parameter wurde an NULL gebunden, CITY = NULL findet per Definition nichts, und der Bericht kam mit “0 gelesenen Zeilen” auf einer komplett durchsuchten Tabelle zurück, eine Zahl, die wie der endgültige Beweis aussah, dass du einen Index brauchst, während sie in Wirklichkeit nur eine andere Abfrage gemessen hat als die, die du eigentlich testen wolltest.

Zwei weitere kleinere Korrekturen im selben Paket. Die erste: ein Ungleichheitsvergleich wie A <> 5 schlägt keinen Index mehr vor. Ein B-Tree hilft hier nicht: Um einen einzigen Wert auszuschließen, muss die Engine ohnehin fast den ganzen Index lesen. Firebird bevorzugt daher den Scan, und der Indexvorschlag wäre verschwendete Zeit gewesen. Die zweite: die Prüfung auf veraltete Statistiken deckt jetzt auch Indizes über mehrere Spalten ab, nicht nur die über eine einzelne Spalte.

So probierst du es aus

Lade MCPFirebird-0.4.0-win64.zip aus dem Release auf GitHub herunter, kopiere .env.example nach .env und lass firebird.client_lib auf die fbclient.dll deiner Installation zeigen. Im Zip findest du sie absichtlich nicht: nur du weißt, mit welchem Server du sprichst. Registriere die Exe dann als stdio-MCP-Server in deinem Agenten.

Leg ihm dann deine langsamste Abfrage vor und frag, welcher Index gebraucht wird. Die interessanteste Antwort, die du bekommen kannst, ist “keiner, und hier sind die Zahlen”.

Das vollständige Changelog listet die ganze 0.4.0 auf, und im Repository liegt das Design-Doc des Advisors mit den Messungen, aus denen die Schwellenwerte stammen.

Comments

comments powered by Disqus