Semantyka zapytań o wartości null

Wprowadzenie

SQL baz danych działają na 3-wartościowej logice ( , , ) podczas wykonywania porównań, w przeciwieństwie do logiki logicznych truefalse języka null C#. Podczas tłumaczenia zapytań LINQ na SQL EF Core kompensować różnicę przez wprowadzenie dodatkowych kontroli wartości null dla niektórych elementów zapytania. Aby to zilustrować, zdefiniujmy następującą jednostkę:

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; }
}

i wyedytuj kilka zapytań:

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);

Pierwsze dwa zapytania dają proste porównania. W pierwszym zapytaniu obie kolumny nie mogą dopuszczać wartości null, więc sprawdzanie wartości null nie jest wymagane. W drugim zapytaniu wartość może zawierać wartość , ale nie może mieć wartości null; w porównaniu z wartościami innymi niż null daje wynik, który zostałby odfiltrowany NullableIntnull według IdnullnullWHERE operacji. Dlatego nie są wymagane żadne dodatkowe terminy.

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]

Trzecie zapytanie wprowadza sprawdzanie wartości null. Kiedy NullableInt to porównanie daje , który nullId <> NullableIntnull zostałby odfiltrowany według WHERE operacji. Jednak z perspektywy logiki logicznych ten przypadek powinien zostać zwrócony jako część wyniku. W EF Core dodaje niezbędne sprawdzanie, aby to zapewnić.

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

Zapytania cztery i pięć pokazują wzorzec, gdy obie kolumny mogą dopuszczać wartość null. Warto zauważyć, że operacja generuje bardziej <> skomplikowane (i potencjalnie wolniejsze) zapytanie niż == operacja.

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)

Traktowanie wartości dopuszczanych do wartości null w funkcjach

Wiele funkcji w SQL może zwrócić wynik tylko wtedy, null gdy niektóre z ich argumentów to null . EF Core z tego rozwiązania, aby tworzyć bardziej wydajne zapytania. Poniższe zapytanie ilustruje optymalizację:

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

Wygenerowana SQL jest następująca (nie musimy oceniać funkcji, ponieważ będzie ona mieć wartość null tylko wtedy, gdy jeden z argumentów tej funkcji SUBSTRING ma wartość null):

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

Optymalizacja może być również używana dla funkcji zdefiniowanych przez użytkownika. Aby uzyskać więcej informacji, zobacz stronę mapowania funkcji zdefiniowanych przez użytkownika.

Pisanie zapytań o performantach

  • Porównywanie kolumn, które nie dopuszczają wartości null, jest prostsze i szybsze niż porównywanie kolumn dopuszczających wartość null. Jeśli to możliwe, należy rozważyć oznaczenie kolumn jako nienadajnych do wartości null.

  • Sprawdzanie równości ( ) jest prostsze i szybsze niż sprawdzanie braku równości ( ), ponieważ zapytanie nie musi rozróżniać ==!= wyników i nullfalse . Używaj porównania równości zawsze, gdy jest to możliwe. Jednak po prostu negacja porównania jest w praktyce taka sama jak , więc nie ==!= spowoduje poprawy wydajności.

  • W niektórych przypadkach można uprościć złożone porównanie przez jawne odfiltrowanie wartości z kolumny — na przykład gdy nie ma żadnych wartości lub te wartości nie są istotne nullnull w wyniku. Rozpatrzmy następujący przykład:

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));

Te zapytania dają następujące SQL:

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)))

W drugim zapytaniu null wyniki są jawnie filtrowane z String1 kolumny. EF Core można bezpiecznie traktować kolumnę jako nienadajną do wartości null podczas String1 porównywania, co spowoduje prostsze zapytanie.

Korzystanie z semantyki relacyjnej wartości null

Można wyłączyć kompensaty porównania wartości null i bezpośrednio korzystać z relacyjnej semantyki wartości null. Można to zrobić, wywołując UseRelationalNulls(true) metodę w konstruktorze opcji wewnątrz OnConfiguring metody :

new SqlServerDbContextOptionsBuilder(optionsBuilder).UseRelationalNulls();

Ostrzeżenie

W przypadku korzystania z semantyki relacyjnej wartości null zapytania LINQ nie mają już tego samego znaczenia, co w języku C#, i mogą zwracać inne wyniki niż oczekiwano. Podczas korzystania z tego trybu należy zachować ostrożność.