Compartilhar via


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 resultados null e false. 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 valor null 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.