ISNULL als Prädikat – SEEK oder SCAN

Im Gespräch mit einem Kunden haben wir uns über NON SARGable Abfragen unterhalten. Dabei ist unter anderem ausgeführt worden, dass Funktionen grundsätzlich zu Index-Scans führen, da sie immer jede Zeile überprüfen müssen. Dieses Thema habe ich im Artikel “Optimierung von Datenbankmodellen – SARGable Abfragen” bereits ausführlich behandelt. Viele Funktionen arbeiten tatsächlich nach diesem Prinzip; dennoch ist z. B. die Arbeitsweise von ISNULL als Prädikat davon abhängig, wie das Attribut in der Tabelle definiert wird.

Testumgebung

Um die unterschiedliche Arbeitsweise von ISNULL zu demonstrieren, werden zwei Tabellen mit – fast – identischer Struktur definiert und mit 10 Datensätzen gefüllt.

-- Tabelle mit NULL-Attribut
CREATE TABLE dbo.ZIP_NULL
(
    Id     INT       NOT NULL,
    CCode  CHAR(3)   NULL,
    ZIP    CHAR(10)  NULL,
 
    CONSTRAINT pk_ZIP_NULL_Id PRIMARY KEY CLUSTERED (Id)
);
GO
 
CREATE INDEX ix_ZIP_NULL_CCode ON dbo.ZIP_NULL (CCode);
GO
 
CREATE TABLE dbo.ZIP_NOT_NULL
(
    Id     INT       NOT NULL,
    CCode  CHAR(3)   NOT NULL,
    ZIP    CHAR(10)  NULL,
 
    CONSTRAINT pk_ZIP_NOT_NULL_Id PRIMARY KEY CLUSTERED (Id)
);
GO
 
CREATE INDEX ix_ZIP_CCode ON dbo.ZIP_NOT_NULL (CCode);
GO
 
INSERT INTO dbo.ZIP_NULL (Id, CCode, ZIP)
VALUES
    (1, 'DE', '12345'),
    (2, 'DE', '12346'),
    (3, 'DE', '12347'),
    (4, 'DE', '12348'),
    (5, 'CH', '12349');
 
INSERT INTO dbo.ZIP_NOT_NULL
SELECT * FROM dbo.ZIP_NULL;
GO

Die Tabelle [dbo].[ZIP_NULL] besitzt ein Attribut [CCode], das NULL erlaubt während die zweite Tabelle [dbo].[ZIP_NOT_NULL] für dieses Attribut keine NULL-Einträge zulässt. Auf das Attribut [CCode] wird in beiden Fällen ein nonclustered Index erstellt. Nachdem die Testumgebung fertig gestellt ist, werden identische Abfragen auf beide Tabellen ausgeführt. Um die Unterschiede im Ausführungsplan besser deutlich zu machen, werden UNION ALL Abfragen ausgeführt.

Abfragen

Um zu überprüfen, wie sich ISNULL als Prädikat in Abfragen verhält, wird eine Abfrage verwendet, die den Index auf dem Attribut [CCode] nutzt.

SELECT Id, CCode FROM dbo.ZIP_NULL WHERE ISNULL(CCode, 'CH') != 'DE'
UNION ALL
SELECT Id, CCode FROM dbo.ZIP_NOT_NULL WHERE ISNULL(CCode , 'CH') != 'DE';
GO

Die obige Abfrage wird als UNION ALL-Abfrage ausgeführt, um die Suchoperationen in einem Ausführungsplan sichtbar zu machen. Führt man die Abfrage aus, erhält man den folgenden Ausführungsplan:

Während für die Tabelle [dbo].[ZIP_NULL] ein Index Scan verwendet wird, kann für die zweite Tabelle [dbo].[ZIP_NOT_NULL] ein deutlich effektiverer Index Seek angewendet werden. Die Eigenschaften der beiden Operatoren zeigen, wie ISNULL auf den Index angewendet werden kann:

 

Die linke Abbildung zeigt die Eigenschaften des INDEX SCAN. Für die Operation wurde der Index [ix_ZIP_NULL_CCode] verwendet. Für den Index Scan konnte kein SEEK-Prädikat verwendet werden; die Funktion ISNULL muss auf jeden Datensatz im Index angewendet werden. Ein Blick auf die rechte Abbildung zeigt, dass ein – effektiverer – Index Seek angewendet werden kann. Die Funktion ISNULL wurde vollständig ignoriert und die Abfrage wird behandelt wie ein Prädikat [CCode <> DE].

Begründung

Der Query Optimizer von Microsoft SQL Server ist “intelligent” genug, um festzustellen, dass für das Attribut [CCode] in der Tabelle [dbo].[ZIP_NOT_NULL] eine Einschränkung besteht, die sicherstellt, dass das Attribut nie NULL sein kann. Der Query Optimizer muss also nicht jede Zeile prüfen, um festzustellen, ob ein NULL-Wert im Attribut vorhanden ist; die Funktion ISNULL ist für den Index [ix_ZIP_NOT_NULL_CCode] sinnlos.

Zusammenfassung

Bereits bei der Definition von Tabellenstrukturen kann im Vorfeld für eine Unterstützung des Query Optimizers gesorgt werden. ISNULL ist nur eines von vielen Beispielen, wie Einschränkungen den Query Optimizer die Arbeit erleichtern.

Herzlichen Dank fürs Lesen!