SQL Server Filtered Index – Wie korrigiere ich Qualitätsmerkmale einer Architektur?

Qualität lebt durch Leidenschaft und nicht von Vorschriften. – Georg Wilhelm Exler

Im Beitrag SQL Server Abfrage Tuning – Wenn Qualitätskriterien wie Performanz schmerzen, ja was mach ich dann? beschrieb ich die Optimierung einer Abfrage nach einer Migration auf SQL Server. Diese Performance-Bremse war jedoch nur ein Problem von vielen, die in Legacy-Anwendungen auf einen warten. Häufige Ursache die zu solchen Problemen führen ist der fehlende Bezug zu den Qualitätskriterien. Für die Softwareentwicklung lässt sich die ISO 25010 Norm anwenden. Diese unterteilt die Qualitätsmerkmale in die Bereiche:

  • Funktionalität
  • Performanz
  • Kompatibilität
  • Benutzbarkeit
  • Zuverlässigkeit
  • Sicherheit
  • Wartbarkeit
  • Portierbarkeit

Häufig spricht man in diesem Zusammenhang von nichtfunktionalen Anforderungen. Beim Begriff «Nichfunktional» finden gelegentlich kontroverse Diskussionen statt, welche unnötige Projektzeit verbrennen können. Aus diesem Grund spreche ich von Qualitätskriterien/ -merkmalen, damit die Gespräche im Projekt auf der sachlichen Ebene bleiben. Neben der ISO Norm 25010 für Software bzw. Produktqualität, existiert auch die ISO-Norm 25012 für Datenqualität, die weniger geläufig ist. Diese gibt Hilfestellungen beim Entwurf des Datenmodells unter Berücksichtigung von Charaktereigenschaften wie:

  • Richtigkeit
  • Vollständigkeit
  • Konsistenz
  • Plausibilität
  • Aktualität
  • Zugänglichkeit
  • Konformität
  • Vertraulichkeit
  • Effizienz
  • Genauigkeit
  • Nachvollziehbarkeit
  • Verständlichkeit
  • Verfügbarkeit
  • Übertragbarkeit
  • Wiederherstellbarkeit

Diese gilt es für jeden Anwendungsfall zu berücksichtigen, damit eine robuste Softwarelösung entstehen kann. Werden zum Beispiel die Mengengerüste im jeweiligen Kontext nicht berücksichtigt, dann hat dies negative Auswirkungen auf Effizienz und Perfomanz, bei Over-Engineering auf die initialen Kosten. In der Anfangszeit des agilen Zeitalters, als der Reifegrad noch niedrig war, wurden diese Punkte selten berücksichtigt. Sprach ich dies auf Konferenzen an, dann wurde ich belächelt und man erklärte mir: «Bei agilen Projekten ist das nicht notwendig, dafür gibt’s Refactorings». Die Zeit hat mir gezeigt, dass solche Aussagen von Speakern sehr naiv waren. Heute treffe ich solche Einstellungen in Bezug auf die Qualitätskriterien weniger an. Die Folgen solcher Aussagen sind dafür noch in vielen Legacy-Anwendungen der jüngeren Generation präsent.

Eine weitere Abfrage, die ich optimieren musste, hatte folgenden Aufbau:

SELECT DISTINCT 
  RELMLO_ID
FROM TABELLE1 
WHERE ID 
  IN (SELECT RELCHA_ID 
  FROM TABELLE2 WITH
  WHERE YEAR_NR = 2016 
  AND MONTH_NR = 10 
  AND DAY_NR = 3)

Die Abfrage hatte die Aufgabe eindeutige Elemente zurückzuliefern, welche ihre täglichen Bewegungsdaten liefern. Im DWH-Umfeld in Form eines Batchjobs wäre die Abfragedauer vertretbar gewesen. Bei einer Anwendung, von der eine schnelle Reaktionszeit erfordert wird, ist die Abfragedauer sehr schmerzhaft. Diese betrug 55’844 ms. Mich machte das neugierig und ich liess diese Abfrage im SQL Developer auf Oracle laufen. Dort betrug die Laufzeit ca. 45 Sekunden, ein wenig besser. Die Statistiken dazu lieferten folgendes Bild:

  • Table ‘Worktable’. Scan count 0, logical reads 0, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
  • Table ‘Workfile’. Scan count 0, logical reads 0, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
  • Table ‘TABELLE2’. Scan count 39460, logical reads 468658, physical reads 15787, read-ahead reads 59646, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
  • Table ‘TABELLE1’. Scan count 5, logical reads 1542, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
  • Table ‘Worktable’. Scan count 0, logical reads 0, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.

Aus mehreren Milliarden Datensätzen wurden mit dieser Abfrage eindeutige Elemente ermittelt, welche maximal in der Grössenordung zwischen 300 bis 500 Datensätzen lagen.

Der Ausführungsplan zeigte folgende Details:

Die Abbildung zeigt einen aufwändigen Ausführungsplan für eine Abfrage, welche nur wenige Datensätze für die Anwendungslogik benötigt

Eine Möglichkeit, welche hier zum Einsatz kommen könnte, wären Filtered Indexe, da bei der Abfragelogik der Anwendung immer nur ein Jahr von Bedeutung ist. Zu Testzwecken erstellte ich einen gefilterten Index für das Jahr 2016.

CREATE NONCLUSTERED INDEX FIX_TABELLE2_YEAR_NR_2016  
    ON TABELLE2 (RELCHA_ID, YEAR_NR, MONTH_NR, DAY_NR)  
    WHERE TSV_YEAR_NR = 2016 
    WITH ( DROP_EXISTING = OFF) ON [INDEX]

Nach der Erstellung führte ich die Abfrage erneut aus. Mit diesem Index lag die Abfragedauer bei 24596 ms. Damit war die Abfrage zweimal schneller und definitiv besser als die Abfrage auf Oracle.

Die Statistiken zeigten folgende Details:

  • Table ‘Worktable’. Scan count 0, logical reads 0, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
  • Table ‘Workfile’. Scan count 0, logical reads 0, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
  • Table TABELLE2′. Scan count 39460, logical reads 429646, physical reads 262, read-ahead reads 34343, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
  • Table ‘TABELLE1’. Scan count 5, logical reads 1542, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
  • Table ‘Worktable’. Scan count 0, logical reads 0, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.

Die logical und physical reads sind etwas tiefer. Im Ausführungsplan ist zudem ersichtlich, dass SQL Server den Filtered Index verwendet:

Die Abbildung zeigt einen Filtered Index, welcher die Abfrageperformance der bestehenden Abfrage verbessert.

Mit einem Filtered Index können Optimierungen mit wenig Aufwand erreicht werden, wenn in einer grossen Datenmenge ein Set an bestimmten kleineren Datenmengen ermittelt werden soll. In diesem Beispiel aus mehreren Jahren das Beispieljahr 2016.

Für diesen Anwendungsfall war die Perfomanz jedoch nicht ausreichend genug, da die Abfrage mit dem Benutzerinterface interagiert. Der Architektur-Fehler lässt sich mit diesem Ansatz nicht korrigieren. Trotzdem kann ein Filtered Index in gewissen Situationen sehr hilfreich sein und bei jeder Abfrageoptimierung eine Option.