NULL-Semantik in Abfragen

Einführung

Im Gegensatz zur booleschen Logik von C# arbeiten SQL-Datenbanken bei Vergleichen mit einer dreiwertigen Logik (true, false, null). Beim Übersetzen von LINQ-Abfragen in SQL versucht EF Core, den Unterschied zu kompensieren, indem zusätzliche NULL-Überprüfungen für einige Elemente der Abfrage eingeführt werden. Zur Veranschaulichung wird die folgende Entität definiert:

public class NullSemanticsEntity
{
    public int Id { get; set; }
    public int Int { get; set; }
    public int? NullableInt { get; set; }
    public string String1 { get; set; }
    public string String2 { get; set; }
}

Außerdem werden mehrere Abfragen ausgegeben:

var query1 = context.Entities.Where(e => e.Id == e.Int);
var query2 = context.Entities.Where(e => e.Id == e.NullableInt);
var query3 = context.Entities.Where(e => e.Id != e.NullableInt);
var query4 = context.Entities.Where(e => e.String1 == e.String2);
var query5 = context.Entities.Where(e => e.String1 != e.String2);

Die ersten beiden Abfragen erzeugen einfache Vergleiche. In der ersten Abfrage sind beide Spalten Non-Nullable, sodass keine NULL-Überprüfungen erforderlich sind. In der zweiten Abfrage kann NullableInt den Wert null enthalten. Id ist jedoch Non-Nullable. Ein Vergleich von null mit Nicht-NULL ergibt null, wobei das Ergebnis durch den WHERE-Vorgang herausgefiltert wird. Es sind also auch keine zusätzlichen Bedingungen erforderlich.

SELECT [e].[Id], [e].[Int], [e].[NullableInt], [e].[String1], [e].[String2]
FROM [Entities] AS [e]
WHERE [e].[Id] = [e].[Int]

SELECT [e].[Id], [e].[Int], [e].[NullableInt], [e].[String1], [e].[String2]
FROM [Entities] AS [e]
WHERE [e].[Id] = [e].[NullableInt]

Die dritte Abfrage führt eine NULL-Überprüfung ein. Wenn NullableIntnull entspricht, ergibt der Vergleich Id <> NullableIntnull, wobei das Ergebnis durch den WHERE-Vorgang herausgefiltert wird. Aus der Perspektive der booleschen Logik sollte dieser Fall jedoch als Teil des Ergebnisses zurückgegeben werden. Daher wird in EF Core die erforderliche Überprüfung hinzugefügt, um dies sicherzustellen.

SELECT [e].[Id], [e].[Int], [e].[NullableInt], [e].[String1], [e].[String2]
FROM [Entities] AS [e]
WHERE ([e].[Id] <> [e].[NullableInt]) OR [e].[NullableInt] IS NULL

Die vierte und fünfte Abfrage weisen ein Muster auf, in dem beide Spalten Nullwerte zulassen. Beachten Sie, dass der <>-Vorgang eine kompliziertere (und potenziell langsamere) Abfrage erzeugt als der ==-Vorgang.

SELECT [e].[Id], [e].[Int], [e].[NullableInt], [e].[String1], [e].[String2]
FROM [Entities] AS [e]
WHERE ([e].[String1] = [e].[String2]) OR ([e].[String1] IS NULL AND [e].[String2] IS NULL)

SELECT [e].[Id], [e].[Int], [e].[NullableInt], [e].[String1], [e].[String2]
FROM [Entities] AS [e]
WHERE (([e].[String1] <> [e].[String2]) OR ([e].[String1] IS NULL OR [e].[String2] IS NULL)) AND ([e].[String1] IS NOT NULL OR [e].[String2] IS NOT NULL)

Verarbeitung von Nullwerte zulassenden Variablen in Funktionen

Viele Funktionen in SQL können nur dann ein null-Ergebnis zurückgeben, wenn einige ihrer Argumente null sind. EF Core nutzt dies, um effizientere Abfragen zu erzeugen. Die folgende Abfrage veranschaulicht die Optimierung:

var query = context.Entities.Where(e => e.String1.Substring(0, e.String2.Length) == null);

Der generierte SQL-Code lautet wie folgt (die SUBSTRING-Funktion muss nicht ausgewertet werden, da sie nur NULL ist, wenn eines ihrer Argumente NULL lautet):

SELECT [e].[Id], [e].[Int], [e].[NullableInt], [e].[String1], [e].[String2]
FROM [Entities] AS [e]
WHERE [e].[String1] IS NULL OR [e].[String2] IS NULL

Die Optimierung kann auch für benutzerdefinierte Funktionen verwendet werden. Weitere Informationen finden Sie unter Zuordnung von benutzerdefinierten Funktionen.

Schreiben von leistungsstarken Abfragen

  • Das Vergleichen von Non-Nullable-Spalten ist einfacher und schneller als das Vergleichen von Nullwerte zulassenden Spalten. Markieren Sie Spalten, wenn möglich, immer als Non-Nullable.

  • Die Überprüfung auf Gleichheit (==) ist einfacher und schneller als die Überprüfung auf Ungleichheit (!=), da die Abfrage nicht zwischen null- und false-Ergebnissen unterscheiden muss. Verwenden Sie nach Möglichkeit den Gleichheitsvergleich. Den Vergleich == einfach zu negieren ist jedoch identisch mit != und führt nicht zu einer Leistungsverbesserung.

  • In einigen Fällen ist es möglich, einen komplexen Vergleich zu vereinfachen, indem null-Werte explizit aus einer Spalte herausgefiltert werden, z. B. wenn keine null-Werte vorhanden oder diese im Ergebnis nicht relevant sind. Betrachten Sie das folgende Beispiel:

var query1 = context.Entities.Where(e => e.String1 != e.String2 || e.String1.Length == e.String2.Length);
var query2 = context.Entities.Where(
    e => e.String1 != null && e.String2 != null && (e.String1 != e.String2 || e.String1.Length == e.String2.Length));

Diese Abfragen erzeugen den folgenden SQL-Code:

SELECT [e].[Id], [e].[Int], [e].[NullableInt], [e].[String1], [e].[String2]
FROM [Entities] AS [e]
WHERE ((([e].[String1] <> [e].[String2]) OR ([e].[String1] IS NULL OR [e].[String2] IS NULL)) AND ([e].[String1] IS NOT NULL OR [e].[String2] IS NOT NULL)) OR ((CAST(LEN([e].[String1]) AS int) = CAST(LEN([e].[String2]) AS int)) OR ([e].[String1] IS NULL AND [e].[String2] IS NULL))

SELECT [e].[Id], [e].[Int], [e].[NullableInt], [e].[String1], [e].[String2]
FROM [Entities] AS [e]
WHERE ([e].[String1] IS NOT NULL AND [e].[String2] IS NOT NULL) AND (([e].[String1] <> [e].[String2]) OR (CAST(LEN([e].[String1]) AS int) = CAST(LEN([e].[String2]) AS int)))

In der zweiten Abfrage werden null-Ergebnisse explizit aus der Spalte String1 herausgefiltert. EF Core kann die Spalte String1 beim Vergleich gefahrlos als Non-Nullable behandeln, was zu einer einfacheren Abfrage führt.

Verwenden der relationalen NULL-Semantik

Es ist möglich, die Kompensierung von NULL-Vergleichen zu deaktivieren und direkt eine relationale NULL-Semantik zu verwenden. Dazu muss die Methode UseRelationalNulls(true) für OptionsBuilder innerhalb Methode OnConfiguring aufgerufen werden:

new SqlServerDbContextOptionsBuilder(optionsBuilder).UseRelationalNulls();

Warnung

Wenn Sie die relationale NULL-Semantik verwenden, haben Ihre LINQ-Abfragen nicht mehr dieselbe Bedeutung wie in C#, und es kann zu unerwarteten Ergebnissen kommen. Gehen Sie umsichtig vor, wenn Sie diesen Modus verwenden.