Query singole e suddivise

Problemi di prestazioni con singole query

Quando si lavora su database relazionali, Entity Framework carica le entità correlate introducendo JOIN in una singola query. Anche se gli JOIN sono piuttosto standard quando si usa SQL, possono creare problemi di prestazioni significativi se usati in modo non corretto. Questa pagina descrive questi problemi di prestazioni e mostra un modo alternativo per caricare le entità correlate che li circondano.

Esplosione cartesiana

Esaminiamo la query LINQ seguente e il relativo equivalente SQL tradotto:

var blogs = ctx.Blogs
    .Include(b => b.Posts)
    .Include(b => b.Contributors)
    .ToList();
SELECT [b].[Id], [b].[Name], [p].[Id], [p].[BlogId], [p].[Title], [c].[Id], [c].[BlogId], [c].[FirstName], [c].[LastName]
FROM [Blogs] AS [b]
LEFT JOIN [Posts] AS [p] ON [b].[Id] = [p].[BlogId]
LEFT JOIN [Contributors] AS [c] ON [b].[Id] = [c].[BlogId]
ORDER BY [b].[Id], [p].[Id]

In questo esempio, poiché sia Posts che Contributors sono spostamenti di raccolta di Blog , si trovano allo stesso livello, i database relazionali restituiscono un prodotto incrociato: ogni riga da Posts viene unita a ogni riga da Contributors. Ciò significa che se un determinato blog ha 10 post e 10 collaboratori, il database restituisce 100 righe per quel singolo blog. Questo fenomeno, talvolta chiamato esplosione cartesiana, può causare enormi quantità di dati per essere trasferiti involontariamente al client, in particolare quando alla query vengono aggiunti joIN di pari livello. Questo può essere un problema di prestazioni importante nelle applicazioni di database.

Si noti che l'esplosione cartesiana non si verifica quando i due JOIN non sono allo stesso livello:

var blogs = ctx.Blogs
    .Include(b => b.Posts)
    .ThenInclude(p => p.Comments)
    .ToList();
SELECT [b].[Id], [b].[Name], [t].[Id], [t].[BlogId], [t].[Title], [t].[Id0], [t].[Content], [t].[PostId]
FROM [Blogs] AS [b]
LEFT JOIN [Posts] AS [p] ON [b].[Id] = [p].[BlogId]
LEFT JOIN [Comment] AS [c] ON [p].[Id] = [c].[PostId]
ORDER BY [b].[Id], [t].[Id]

In questa query, Comments è una struttura di spostamento di raccolta di Post, a differenza Contributors della query precedente, che era una struttura di spostamento raccolta di Blog. In questo caso, viene restituita una singola riga per ogni commento che un blog ha (tramite i suoi post) e un prodotto incrociato non si verifica.

Duplicazione dati

I nomi JOIN possono creare un altro tipo di problema di prestazioni. Esaminiamo la query seguente, che carica solo una singola navigazione raccolta:

var blogs = ctx.Blogs
    .Include(b => b.Posts)
    .ToList();
SELECT [b].[Id], [b].[Name], [b].[HugeColumn], [p].[Id], [p].[BlogId], [p].[Title]
FROM [Blogs] AS [b]
LEFT JOIN [Posts] AS [p] ON [b].[Id] = [p].[BlogId]
ORDER BY [b].[Id]

Esaminando le colonne proiettate, ogni riga restituita da questa query contiene proprietà di entrambe le Blogs tabelle e Posts . Ciò significa che le proprietà del blog vengono duplicate per ogni post presente nel blog. Anche se questo è in genere normale e non causa problemi, se la Blogs tabella presenta una colonna molto grande (ad esempio dati binari o un testo enorme), tale colonna verrebbe duplicata e inviata più volte al client. Ciò può aumentare significativamente il traffico di rete e influire negativamente sulle prestazioni dell'applicazione.

Se non è effettivamente necessaria la colonna enorme, è facile non eseguire query per questa colonna:

var blogs = ctx.Blogs
    .Select(b => new
    {
        b.Id,
        b.Name,
        b.Posts
    })
    .ToList();

Usando una proiezione per scegliere in modo esplicito le colonne desiderate, è possibile omettere colonne grandi e migliorare le prestazioni; Si noti che questa è una buona idea indipendentemente dalla duplicazione dei dati, quindi è consigliabile farlo anche quando non si carica una navigazione nella raccolta. Tuttavia, poiché questo progetto il blog in un tipo anonimo, il blog non viene monitorato da Entity Framework e le modifiche apportate non possono essere salvate come di consueto.

Vale la pena notare che, a differenza dell'esplosione cartesiana, la duplicazione dei dati causata da JOIN in genere non è significativa, perché le dimensioni dei dati duplicati sono trascurabili; si tratta in genere di un problema solo se nella tabella principale sono presenti colonne di grandi dimensioni.

Suddividere le query

Per risolvere i problemi di prestazioni descritti in precedenza, ENTITY consente di specificare che una determinata query LINQ deve essere suddivisa in più query SQL. Invece di JOIN, le query suddivise generano una query SQL aggiuntiva per ogni navigazione raccolta inclusa:

using (var context = new BloggingContext())
{
    var blogs = context.Blogs
        .Include(blog => blog.Posts)
        .AsSplitQuery()
        .ToList();
}

Produrrà il codice SQL seguente:

SELECT [b].[BlogId], [b].[OwnerId], [b].[Rating], [b].[Url]
FROM [Blogs] AS [b]
ORDER BY [b].[BlogId]

SELECT [p].[PostId], [p].[AuthorId], [p].[BlogId], [p].[Content], [p].[Rating], [p].[Title], [b].[BlogId]
FROM [Blogs] AS [b]
INNER JOIN [Posts] AS [p] ON [b].[BlogId] = [p].[BlogId]
ORDER BY [b].[BlogId]

Avviso

Quando si usano query suddivise con Skip/Take, prestare particolare attenzione a rendere completamente univoco l'ordinamento delle query; in caso contrario, potrebbero essere restituiti dati non corretti. Ad esempio, se i risultati vengono ordinati solo per data, ma possono essere presenti più risultati con la stessa data, ognuna delle query suddivise potrebbe ottenere risultati diversi dal database. L'ordinamento per data e ID (o qualsiasi altra proprietà o combinazione di proprietà univoche) rende l'ordinamento completamente univoco ed evita questo problema. Si noti che i database relazionali non applicano alcun ordinamento per impostazione predefinita, anche nella chiave primaria.

Nota

Le entità correlate uno a uno vengono sempre caricate tramite JOIN nella stessa query, perché non ha alcun impatto sulle prestazioni.

Abilitazione di query suddivise a livello globale

È anche possibile configurare le query suddivise come predefinita per il contesto dell'applicazione:

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
    optionsBuilder
        .UseSqlServer(
            @"Server=(localdb)\mssqllocaldb;Database=EFQuerying;Trusted_Connection=True",
            o => o.UseQuerySplittingBehavior(QuerySplittingBehavior.SplitQuery));
}

Quando le query suddivise sono configurate come predefinite, è comunque possibile configurare query specifiche da eseguire come singole query:

using (var context = new SplitQueriesBloggingContext())
{
    var blogs = context.Blogs
        .Include(blog => blog.Posts)
        .AsSingleQuery()
        .ToList();
}

EF Core usa la modalità query singola per impostazione predefinita in assenza di alcuna configurazione. Poiché può causare problemi di prestazioni, EF Core genera un avviso ogni volta che vengono soddisfatte le condizioni seguenti:

  • EF Core rileva che la query carica più raccolte.
  • L'utente non ha configurato la modalità di suddivisione delle query a livello globale.
  • L'utente non ha usato AsSingleQuery/AsSplitQuery l'operatore nella query.

Per disattivare l'avviso, configurare la modalità di suddivisione delle query a livello globale o a livello di query su un valore appropriato.

Caratteristiche delle query suddivise

Sebbene la divisione della query eviti i problemi di prestazioni associati a JOINs e all'esplosione cartesiana, presenta anche alcuni svantaggi:

  • Sebbene la maggior parte dei database garantisca la coerenza dei dati per singole query, non esistono garanzie di questo tipo per più query. Se il database viene aggiornato contemporaneamente durante l'esecuzione delle query, i dati risultanti potrebbero non essere coerenti. È possibile attenuarlo eseguendo il wrapping delle query in una transazione serializzabile o snapshot, anche se ciò può creare problemi di prestazioni propri. Per altre informazioni, vedere la documentazione del database.
  • Ogni query implica attualmente un round trip di rete aggiuntivo per il database. Più round trip di rete possono ridurre le prestazioni, soprattutto quando la latenza per il database è elevata (ad esempio, servizi cloud).
  • Anche se alcuni database consentono di usare i risultati di più query contemporaneamente (SQL Server con MARS, Sqlite), la maggior parte consente di attivare solo una singola query in un determinato punto. Pertanto, tutti i risultati delle query precedenti devono essere memorizzati nel buffer nella memoria dell'applicazione prima di eseguire query successive, con conseguente aumento dei requisiti di memoria.
  • Quando si includono spostamenti di riferimento e spostamenti di raccolte, ognuna delle query suddivise includerà join agli spostamenti di riferimento. Ciò può ridurre le prestazioni, in particolare se sono presenti molti spostamenti di riferimento. Si prega di richiamare #29182 se si tratta di qualcosa che si vuole vedere corretto.

Sfortunatamente, non esiste una strategia per il caricamento di entità correlate che si adatti a tutti gli scenari. Considerare attentamente i vantaggi e gli svantaggi delle query singole e suddivise per selezionarne uno adatto alle proprie esigenze.