Semântica nula de consulta
Introdução
Os bancos de dados SQL operam na lógica de 3 valores (true
, false
, null
) ao executar comparações, em oposição à lógica booliana de C#. Ao traduzir consultas LINQ para SQL, o EF Core tenta compensar a diferença introduzindo verificações nulas adicionais para alguns elementos da consulta.
Para ilustrar isso, vamos definir a seguinte entidade:
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; }
}
e emita várias consultas:
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);
As duas primeiras consultas produzem comparações simples. Na primeira consulta, ambas as colunas são não anuláveis, portanto, não são necessárias verificações nulas. Na segunda consulta, NullableInt
poderia conter null
, mas Id
não é anulável; comparando null
com não nulos gera null
como resultado, que seriam filtrados pela operação WHERE
. Portanto, também não são necessários termos adicionais.
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]
A terceira consulta apresenta uma verificação nula. Quando NullableInt
é null
a comparação Id <> NullableInt
gera null
, que seria filtrada pela operação WHERE
. No entanto, da perspectiva da lógica booliana, esse caso deve ser retornado como parte do resultado. Portanto, o EF Core adiciona a verificação necessária para garantir isso.
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
As consultas quatro e cinco mostram o padrão quando ambas as colunas são anuláveis. Vale a pena observar que a operação <>
produz uma consulta mais complicada (e potencialmente mais lenta) do que a operação ==
.
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)
Tratamento de valores anuláveis em funções
Muitas funções no SQL só poderão retornar um resultado null
se alguns de seus argumentos forem null
. O EF Core aproveita isso para produzir consultas mais eficientes.
A consulta a seguir ilustra a otimização:
var query = context.Entities.Where(e => e.String1.Substring(0, e.String2.Length) == null);
O SQL gerado é o seguinte (não precisamos avaliar a função SUBSTRING
, pois ela só será nula quando qualquer um dos argumentos para ela for nulo.):
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
A otimização também pode ser usada para funções definidas pelo usuário. Consulte a página de mapeamento de função definida pelo usuário para obter mais detalhes.
Como escrever consultas com desempenho
Comparar colunas não anuláveis é mais simples e mais rápido do que comparar colunas anuláveis. Considere marcar colunas como não anuláveis sempre que possível.
A verificação de igualdade (
==
) é mais simples e rápida do que verificar a não igualdade (!=
), pois a consulta não precisa distinguir entre os resultadosnull
efalse
. Use a comparação de igualdade sempre que possível. No entanto, simplesmente negar a comparação==
é efetivamente o mesmo que!=
, portanto, isso não resulta em melhoria de desempenho.Em alguns casos, é possível simplificar uma comparação complexa filtrando valores
null
de uma coluna explicitamente - por exemplo, quando nenhum valornull
está presente ou esses valores não são relevantes no resultado. Considere o seguinte exemplo:
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));
Essas consultas produzem o seguinte 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)))
Na segunda consulta, os resultados null
são filtrados da coluna String1
explicitamente. O EF Core pode tratar a coluna String1
com segurança como não anulável durante a comparação, resultando em uma consulta mais simples.
Usando semântica nula relacional
É possível desabilitar a compensação de comparação nula e usar a semântica nula relacional diretamente. Isso pode ser feito chamando o método UseRelationalNulls(true)
no construtor de opções dentro do método OnConfiguring
:
new SqlServerDbContextOptionsBuilder(optionsBuilder).UseRelationalNulls();
Aviso
Ao usar semântica nula relacional, suas consultas LINQ não têm mais o mesmo significado que no C#e podem produzir resultados diferentes do esperado. Tenha cuidado ao usar esse modo.