Index-Filterprädikate sind ein häufiges Symptom ineffizienter Indexnutzung bei einer falschen Spaltenreihenfolge. Sie können aber auch gezielt eingesetzt werden. Allerdings nicht um den Indexzugriff zu optimieren, sondern um die dazugehörenden Tabellendaten „griffbereit“ im Index abzulegen.
So kann man zum Beispiel Bedingungen, die ohnehin nicht als Zugriffsprädikate taugen, dennoch in den Index aufnehmen, um sie absichtlich als Index-Filterprädikat zu benutzen.
SELECT first_name, last_name, subsidiary_id, phone_number
FROM employees
WHERE subsidiary_id = ?
AND UPPER(last_name) LIKE '%INA%'
Der LIKE
-Filter mit führendem Wildcard-Zeichen kann den Indexbaum nicht nutzen. Das bedeutet,
dass das Indizieren der Spalte LAST_NAME
den durchsuchten
Indexbereich nicht schmälern kann. Das gilt unabhängig davon, ob die
LAST_NAME
-Spalte selbst oder die großgeschriebene Variante
UPPER(last_name)
indiziert wird. Die
LIKE
-Bedingung ist daher kein guter Kandidat für einen
Index.
Die SUBSIDIARY_ID
-Bedingung kann aber gut indiziert
werden. Dazu ist nicht einmal ein neuer Index notwendig, da die
SUBSIDIARY_ID
ohnehin die erste Spalte im
Primärschlüssel-Index ist.
- DB2
Explain Plan ------------------------------------------------------------ ID | Operation | Rows | Cost 1 | RETURN | | 40 2 | FETCH EMPLOYEES | 33 of 333 ( 9.91%) | 40 3 | RIDSCN | 333 of 333 (100.00%) | 12 4 | SORT (UNIQUE) | 333 of 333 (100.00%) | 12 5 | IXSCAN EMPLOYEES_PK | 333 of 10000 ( 3.33%) | 12 Predicate Information 2 - SARG (Q1.SUBSIDIARY_ID = ?) SARG ( UPPER(Q1.LAST_NAME) LIKE '%INA%') 5 - START (Q1.SUBSIDIARY_ID = ?) STOP (Q1.SUBSIDIARY_ID = ?)
- Oracle
--------------------------------------------------------------- |Id | Operation | Name | Rows | Cost | --------------------------------------------------------------- | 0 | SELECT STATEMENT | | 17 | 230 | |*1 | TABLE ACCESS BY INDEX ROWID| EMPLOYEES | 17 | 230 | |*2 | INDEX RANGE SCAN | EMPLOYEEs_PK| 333 | 2 | --------------------------------------------------------------- Predicate Information (identified by operation id): --------------------------------------------------- 1 - filter(UPPER("LAST_NAME") LIKE '%INA%') 2 - access("SUBSIDIARY_ID"=TO_NUMBER(:A))
Der Ausführungsplan zeigt die SUBSIDIARY_ID
-Bedingung
als Zugriffsprädikat beim Indexzugriff und die
LIKE
-Bedingung als Filterprädikat beim Tabellenzugriff.
Anhand der Cost-Werte kann man erahnen, dass der Hauptaufwand nicht der
INDEX RANGE SCAN
, sondern der Tabellenzugriff ist. Das ist
ein typisches Muster, das für sich genommen noch kein Problem darstellt.
Dennoch zeigt es, dass der Tabellenzugriff den größten Teil der
Ausführungszeit beansprucht.
Hinweis in eigener Sache
Ich biete SQL Schulungen, Optimierung und Beratung an. Auch der Kauf meines Buches „SQL Performance Explained“ (ab €9,95) unterstützt meine Arbeit an dieser Webseite.
Der Tabellenzugriff muss aber nicht zwangsläufig ein Flaschenhals
sein. Es kommt darauf an, auf wie viele Tabellenblöcke die Zeilen, die
nach dem INDEX RANGE SCAN
aus der Tabelle gelesen werden
müssen, verteilt sind. Wenn die Tabellenzeilen in einer ähnlichen
Reihenfolge wie im Index gespeichert sind, muss die Datenbank weniger
Tabellenblöcke lesen, um die Zeilen zu laden. Die Zeilen, die im Index
hintereinander stehen, stehen dann auch in der Tabelle eng
beieinander.
Beachte
Die Beziehung der Indexreihenfolge zur Tabellenreihenfolge ist ein Performancemaßstab (der sog. Clustering-Faktor).
Man kann die Zeilen in einer Tabelle tatsächlich so anordnen, dass die Reihenfolge mit dem Index übereinstimmt und damit weniger Tabellenblöcke gelesen werden müssen. Diese Methode ist aber nur sehr beschränkt anwendbar, da man die Tabellendaten nur in einer Reihenfolge abspeichern kann. In den Indizes werden die Zeilen aber nach der jeweiligen Indexreihenfolge gespeichert. Man muss sich also entscheiden, für welchen Indexzugriff man die Tabelle optimieren will. Selbst wenn man sich für einen Index entscheiden kann, stellen die Datenbanken bestenfalls rudimentäre Werkzeuge zur Verfügung, die Reihenfolge in der Tabelle zu steuern. Alles in allem ist dieser Ansatz keine praktikable Lösung.
Genau hier kommt die zweite Macht der Indizierung ins Spiel. Man kann nahezu beliebig viele Spalten in einen Index aufnehmen, die automatisch mit einsortiert werden. Dadurch ist ein Index ein einfaches, aber dennoch mächtiges Werkzeug um Daten zu clustern.
Um diese Idee auf die Abfrage von oben anzuwenden, muss man alle
Spalten der where
-Klausel in den Index
aufnehmen – auch wenn sie den durchsuchten Indexbereich nicht schmälern
kann.
CREATE INDEX empsubupnam ON employees
(subsidiary_id, UPPER(last_name))
Die Spalte SUBSIDIARY_ID
bleibt an erster Position,
damit sie als Zugriffsprädikat benutzt werden kann. Der Ausdruck
UPPER(last_name)
deckt den LIKE
-Filter als
gezieltes Index-Filterprädikat ab. Die Indizierung in
Großschreibung spart einige CPU-Zyklen. Da die
LIKE
-Bedingung ohnehin nicht als Zugriffsprädikat genutzt
werden kann, würde ein normaler Index auf LAST_NAME
auch
funktionieren. Die direkte Indizierung kann sogar Vorteile haben, wie wir
im nächsten Abschnitt sehen
werden.
- DB2
Explain Plan -------------------------------------------------------- ID | Operation | Rows | Cost 1 | RETURN | | 15 2 | FETCH EMPLOYEES | 0 of 0 | 15 3 | IXSCAN EMPSUBUPNAM2 | 0 of 10000 ( .00%) | 15 Predicate Information 3 - START (Q1.SUBSIDIARY_ID = ?) STOP (Q1.SUBSIDIARY_ID = ?) SARG (Q1.LAST_NAME LIKE '%INA%')
Um den gewünschten Ausführungsplan zu erhalten, wurde das
UPPER
sowohl im Index, als auch in derwhere
-Klausel entfernt. Mit Stand DB2 LUW 10.5 ist funktions-basierte Indizierung eine junge Funktion – es scheint, dass der Optimizer damit noch nicht immer richtig umzugehen weiß. Weiters sind Collations ohnehin die bessere Variante, die Groß- und Kleinschreibung in DB2 zu ignorieren.- Oracle
-------------------------------------------------------------- |Id | Operation | Name | Rows | Cost | -------------------------------------------------------------- | 0 | SELECT STATEMENT | | 17 | 20 | | 1 | TABLE ACCESS BY INDEX ROWID| EMPLOYEES | 17 | 20 | |*2 | INDEX RANGE SCAN | EMPSUBUPNAM| 17 | 3 | -------------------------------------------------------------- Predicate Information (identified by operation id): --------------------------------------------------- 2 - access("SUBSIDIARY_ID"=TO_NUMBER(:A)) filter(UPPER("LAST_NAME") LIKE '%INA%')
Der neue Ausführungsplan zeigt dieselben Operationen wie zuvor. Der
Cost-Wert ist insgesamt aber stark gefallen. Im Predicate
Information
-Bereich sieht man, dass der LIKE
-Filter
bereits beim INDEX RANGE SCAN
angewandt wird. Die Zeilen, die
den LIKE
-Filter nicht erfüllen, werden also schon beim
Indexzugriff verworfen. Beim Tabellenzugriff gibt es kein Filterprädikat
mehr. Das bedeutet, dass es keinen Tabellenzugriff für Zeilen gibt, die
die where
-Klausel nicht
erfüllen.
An der Rows
-Spalte erkennt man den Unterschied der
beiden Ausführungspläne besonders deutlich. Laut Schätzung des Optimizers
liefert die Abfrage letztendlich 17 Zeilen. Beim ersten Ausführungsplan
sind es nach dem Indexzugriff aber 333 Ergebnisse, die erst beim
Tabellenzugriff auf 17 reduziert werden. Im zweiten Ausführungsplan
liefert der Indexzugriff die Einträge, die den LIKE
-Filter
nicht erfüllen, erst gar nicht. Dadurch muss die TABLE ACCESS BY
INDEX ROWID
-Operation nur 17 Mal statt 333 Mal ausgeführt
werden.
Weiters ist zu beachten, dass der Cost-Wert der INDEX RANGE
SCAN
-Operation von zwei auf drei gestiegen ist. Das liegt daran,
dass der neue Index größer ist und daher mehr Speicher belegt. Angesichts
des Gewinnes ist das aber ein akzeptabler Kompromiss.
Beachte
Für Filterprädikate alleine sollte man keinen neuen Index einführen.
Wenn man stattdessen einen bestehenden Index erweitert, bleibt der Wartungsaufwand gering. Bei manchen Datenbanken kann man sogar den Primärschlüssel-Index um Spalten erweitern, die nicht im Primärschlüssel sind.
Die folgende Animation zeigt den Unterschied zwischen den beiden Ausführungsplänen:
Abbildung 5.1 Gezielte Index-Filterprädikate

Ein Index wächst auch mit der Spaltenzahl – insbesondere wenn man
große Textspalten aufnimmt. Die Performance wird dadurch natürlich nicht
besser. Man sollte also keineswegs alle Spalten der where
-Klausel in den Index aufnehmen, sondern
Index-Filterprädikate nur gezielt einsetzen, um das Datenvolumen
frühzeitig zu reduzieren.
Dieses Beispiel scheint die Binsenweisheit, dass jede Spalte der
where
-Klausel in den Index gehört, zu
bestätigen. Dabei bleibt die Spaltenreihenfolge allerdings auf der
Strecke. Es ist aber ausgerechnet die Spaltenreihenfolge, die über
Zugriffs- und Filterprädikate entscheidet und damit den wichtigsten
Beitrag zur Performance liefert. Diese Entscheidung sollte man keinesfalls
dem Zufall überlassen.
Tipp
Glossar: Index-Filterprädikate
Index Zugriffs- und Filterprädikate an einem Beispiel erklärt
Die Auswirkungen von unbeabsichtigten Index-Filterprädikaten
Warum „egal wo“
LIKE
-Suchen nicht als Zugriffsprädikat taugenIndex-Filterprädikate in Oracle, PostgreSQL und SQL Server Ausführungsplänen erkennen.