Семантика запроса NULL

Введение

Базы данных SQL используют 3-значную логику (true, false, null) для выполнения сравнений, в отличие от булевой логики в C#. При переводе запросов LINQ в SQL EF Core пытается компенсировать разницу, введя дополнительные проверки null для некоторых элементов запроса. Чтобы проиллюстрировать это, давайте определим следующую сущность:

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

и выполните несколько запросов:

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

Первые два запроса создают простые сравнения. В первом запросе оба столбца не могут содержать NULL, поэтому проверки NULL не требуются. Во втором запросе NullableInt мог бы содержать null, но Id является обязательным к заполнению; сравнение null с ненулевым значением дает null в результате, который будет отфильтрован в ходе операции WHERE. Поэтому никаких дополнительных условий не требуется.

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]

Третий запрос вводит проверку null. Когда NullableInt равно null, результат сравнения Id <> NullableInt дает null, который будет отфильтрован операцией WHERE. Однако с точки зрения булевой логики этот случай должен включаться в результат. Поэтому EF Core добавляет необходимую проверку, чтобы убедиться в этом.

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

Запросы четыре и пять показывают шаблон, если оба столбца являются пустыми. Следует отметить, что <> операция создает более сложный (и потенциально медленный) запрос, чем == операция.

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)

Обработка значений, допускающих значение NULL, в функциях

Многие функции в SQL могут возвращать результат только в том случае, если некоторые из их аргументов являются null. EF Core использует это для создания более эффективных запросов. Приведенный ниже запрос иллюстрирует оптимизацию:

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

Созданный SQL выглядит следующим образом (нам не нужно оценивать SUBSTRING функцию, так как она будет иметь значение NULL, если один из аргументов для него имеет значение 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

Оптимизацию также можно использовать для определяемых пользователем функций. Дополнительные сведения см. на странице сопоставления определяемых пользователем функций .

Написание эффективных запросов

  • Сравнение столбцов, не допускающих значения NULL, проще и быстрее, чем сравнение столбцов, допускающих значение NULL. По возможности помечайте столбцы как не допускающие значения NULL.

  • Проверка на равенство (==) проще и быстрее, чем проверка на неравенство (!=), так как запросу не нужно различать результат null и результат false. По возможности используйте сравнение равенства. Однако простое отрицание сравнения == практически идентично !=, поэтому это не улучшает производительность.

  • В некоторых случаях можно упростить сложное сравнение, отфильтровав null значения из столбца явным образом , например, если null значения отсутствуют или эти значения не относятся к результату. Рассмотрим следующий пример:

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

Эти запросы создают следующий 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)))

Во втором запросе результаты null в явной форме исключены из столбца String1. EF Core может безопасно обрабатывать String1 столбец как не допускающий значение NULL во время сравнения, что приводит к более простому запросу.

Использование реляционной семантики NULL

Можно отключить компенсацию сравнения NULL и напрямую использовать реляционную семантику NULL. Это можно сделать, вызвав UseRelationalNulls(true) метод в построителе параметров внутри OnConfiguring метода:

new SqlServerDbContextOptionsBuilder(optionsBuilder).UseRelationalNulls();

Предупреждение

При использовании реляционной семантики NULL запросы LINQ больше не имеют того же значения, что и в C#, и могут давать результаты, отличные от ожидаемых. Соблюдайте осторожность при использовании этого режима.