Ein Kunde beklagte sich über die schlechte Ausführungsgeschwindigkeit einer Funktion, die er von einem Programmierer erhalten hatte. Bei der Durchsicht des Codes der Funktion war das Problem schnell gefunden. Statt eines performanten Indexseek hat die Abfrage einen Indexscan durchgeführt. Ursache für die schlechte Performance war, dass die WHERE-Klausel keine SARGable Argumente verwendete.

Was bedeutet SARGable?

Hinter dem Begriff “SARGable” verbirgt sich ein Akronym für “Search ARGumentable Query”. Dieser Ausdruck sagt aus, dass ein RDBMS-System mit Hilfe eines Prädikats in der Lage ist, Werte innerhalb eines Index zu finden. Dabei kann das RDBMS-System gezielt nach den Werten innerhalb des Index mit Hilfe einer SEEK-Operation suchen. Wenn als Prädikat (WHERE-Klausel) eine Transformation der Werte einer Spalte durchführen muss, spricht man von “NONSARGable” Abfragen. In diesem Fall kann das RDBMS-System einen Index nicht gezielt durchsuchen sondern muss jeden Eintrag transformieren und mit dem Prädikat vergleichen. Eine SEEK-Operation ist dann nicht mehr möglich und es muss eine SCAN-Operation verwendet werden.

Testumgebung

Für die Demonstration der unterschiedlichen Verfahren dient eine einfache Tabelle, in der Mitarbeiter und deren Kostenstellen gespeichert werden.

Tabelle

CREATE TABLE dbo.Employees
(
    ID            INT        NOT NULL,
    [Name]        CHAR(1000) NOT NULL,
    [CostCenter]  CHAR(5)    NOT NULL
);
GO
 
CREATE NONCLUSTERED INDEX nix_Employees_CostCenter ON dbo.Employees (CostCenter);
GO

Die Tabelle besitzt ~12.500 Datensätze.

Distribution der Daten

Die Beispieldaten in der indexierten Spalte [CostCenter] sind wie folgt verteilt

DBCC SHOW_STATISTICS (N'dbo.Employees', N'nix_Employees_CostCenter') WITH HISTOGRAM;

DATA_DISTRIBUTION_01

Für die nachfolgenden Beispiele sollen alle Mitarbeiter der Kostenstellen Cxxxx dienen.

Funktion

Die vom Programmierer zur Verfügung gestellte Funktion hatte den – korrespondierend zum Beispiel – folgenden Inhalt:

CREATE FUNCTION dbo.GetEmployeesByCostCenter
    (@CostCenterPrefix CHAR(1))
RETURNS TABLE
AS
RETURN
(
    SELECT  Id, Name, CostCenter
    FROM    dbo.Employees
    WHERE   SUBSTRING(CostCenter, 1, 1) = @CostCenterPrefix
);

Die Funktion erwartet als Parameter lediglich das Präfix der gewünschten Kostenstellen. Der Programmierer hat die Funktion SUBSTRING verwendet, um aus dem gespeicherten Wert das Präfix zu filtern.

Demonstrationen

Ursprüngliche Funktion

Die Abfrage nach Daten der Kostenstellen, die mit “C” beginnen (160 Datensätze) generieren den folgenden Ausführungsplan.

SELECT * FROM dbo.GetEmployeesByCostCenter('C');

EXECUTION_PLAN_01

Die Abfrage verwendet einen TABLE SCAN und kann den Index auf [CostCenter] nicht verwenden. Ein Blick in die Eigenschaften des Operators zeigt das Problem. Statt einen performanten INDEX SEEK verwendet Microsoft SQL Server für ALLE Datensätze einen transformierten Wert, der aus der Spalte [ConstCenter] resultiert. Das erste Zeichen aus [CostCenter] wird extrahiert. Anschließend kann dieser extrahierte Wert mit dem Übergabeparameter verglichen werden. Da erst JEDER Wert transformiert werden muss, ist ein INDEX SEEK nicht möglich.

DATA_SET_01

Die obige Abbildung zeigt, wie sich die Abfrage verhält. Das Ergebnis der [Transformation] wird mit dem Präfix verglichen, das der Funktion übergeben wird. Alle Datensätze, deren Transformation der Spalte [CostCenter] ein “C” ergeben, werden an den Client zurückgeliefert. Dazu ist es aber erforderlich, erst für ALLE Datensätze die Transformation durchzuführen.

Modifizierte Funktion

Um Microsoft SQL Server zu veranlassen, einen Index effektiv zu nutzen, darf ein Attribut nicht durch eine Funktion oder durch Operationen geändert werden, bevor das Ergebnis ausgewertet werden soll. Das Ziel muss sein, dass unmittelbar im Wert selbst gesucht werden soll. Die Funktion wurde in der Auswertung des Prädikats geringfügig geändert – im Ergebnis kann Microsoft SQL Server einen performanten INDEX SEEK verwenden.

CREATE FUNCTION dbo.GetEmployeesByCostCenter
    (@CostCenterPrefix CHAR(1))
RETURNS TABLE
AS
RETURN
(
    SELECT  Id, Name, CostCenter
    FROM    dbo.Employees
    WHERE   CostCenter >= @CostCenterPrefix
            AND CostCenter < CHAR(ASCII(@CostCenterPrefix) + 1)
);

EXECUTION_PLAN_02

Zusammenfassung

Es ist unumgänglich, bei der Suche nach Optimierungsmöglichkeiten Details innerhalb eines Ausführungsplans zu beachten. Bei Einschränkungen mittels WHERE-Klausel sollte immer darauf geachtet werden, eine SEEK-Einschränkung (Predicate) zu erzielen. Funktionen und Operationen von Attributen verhindern in der Regel diesen Optimierungsschritt.

Herzlichen Dank fürs Lesen!