Семантика значений 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 значением получается результат 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
Запросы 4 и 5 демонстрируют ситуацию, в которой оба столбца допускают значения 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
, только если некоторые из их аргументов имеют значение 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
и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#, и могут возвращать не те результаты, которые ожидались. Используйте этот режим с осторожностью.