Semantica Null query

Introduzione

I database SQL operano sulla logica a 3 valori (true, false, null) quando si eseguono confronti, anziché la logica booleana di C#. Quando si convertono query LINQ in SQL, EF Core tenta di compensare la differenza introducendo controlli Null aggiuntivi per alcuni elementi della query. Per illustrare questo concetto, definire l'entità seguente:

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

ed eseguire diverse query:

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

Le prime due query producono confronti semplici. Nella prima query entrambe le colonne non sono nullable, quindi non sono necessari controlli Null. Nella seconda query NullableInt potrebbe contenere null, ma Id non è nullable. Il confronto null con i null risultati non Null è quindi filtrato in base WHERE all'operazione. Quindi non sono necessari termini aggiuntivi.

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]

La terza query introduce un controllo Null. Quando NullableInt il null confronto Id <> NullableInt restituisce null, che verrebbe filtrato in WHERE base all'operazione. Tuttavia, dal punto di vista della logica booleana questo caso deve essere restituito come parte del risultato. Di conseguenza, EF Core aggiunge il controllo necessario per assicurarsi che.

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

Le query quattro e cinque mostrano il criterio quando entrambe le colonne sono nullable. Vale la pena notare che l'operazione <> produce query più complesse (e potenzialmente più lente) rispetto all'operazione == .

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)

Trattamento dei valori nullable nelle funzioni

Molte funzioni in SQL possono restituire un null risultato solo se alcuni dei relativi argomenti sono null. EF Core sfrutta questa funzionalità per produrre query più efficienti. La query seguente illustra l'ottimizzazione:

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

Il codice SQL generato è il seguente (non è necessario valutare la SUBSTRING funzione perché sarà null solo quando uno degli argomenti è 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

L'ottimizzazione può essere usata anche per le funzioni definite dall'utente. Per altri dettagli, vedere la pagina di mapping delle funzioni definite dall'utente.

Scrittura di query con prestazioni elevate

  • Il confronto di colonne non nullable è più semplice e veloce rispetto al confronto di colonne nullable. Prendere in considerazione la possibilità di contrassegnare le colonne come non nullable quando possibile.

  • Il controllo dell'uguaglianza (==) è più semplice e veloce rispetto al controllo della mancata uguaglianza (!=), perché la query non deve distinguere tra null e false risultato. Usare il confronto di uguaglianza quando possibile. Tuttavia, la semplice negazione == del confronto è in effetti uguale !=a , quindi non comporta un miglioramento delle prestazioni.

  • In alcuni casi, è possibile semplificare un confronto complesso filtrando null in modo esplicito i valori di una colonna, ad esempio quando non null sono presenti valori o questi valori non sono rilevanti nel risultato. Si consideri l'esempio seguente:

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

Queste query producono il codice SQL seguente:

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

Nella seconda query i null risultati vengono filtrati in modo esplicito dalla String1 colonna. EF Core può considerare in modo sicuro la colonna come non nullable durante il String1 confronto, ottenendo una query più semplice.

Uso della semantica null relazionale

È possibile disabilitare la compensazione del confronto null e usare direttamente la semantica null relazionale. A tale scopo, chiamare UseRelationalNulls(true) il metodo nel generatore di opzioni all'interno OnConfiguring del metodo :

new SqlServerDbContextOptionsBuilder(optionsBuilder).UseRelationalNulls();

Avviso

Quando si usa la semantica null relazionale, le query LINQ non hanno più lo stesso significato di quelle in C# e possono restituire risultati diversi rispetto al previsto. Prestare attenzione quando si usa questa modalità.