Sqlserver
 sql >> Database >  >> RDS >> Sqlserver

Il codice Entity Framework è lento quando si usa Include() molte volte

tl;dr Più Includes s far saltare in aria il set di risultati SQL. Presto diventa più economico caricare i dati tramite più chiamate al database invece di eseguire una mega istruzione. Prova a trovare la migliore combinazione di Includes e Load dichiarazioni.

sembra che ci sia una penalizzazione delle prestazioni quando si utilizza Includi

È un eufemismo! Più Includes s esplode rapidamente il risultato della query SQL sia in larghezza che in lunghezza. Perché?

Fattore di crescita di Includes s

(Questa parte si applica a Entity Framework classico, v6 e precedenti)

Diciamo che abbiamo

  • entità radice Root
  • entità padre Root.Parent
  • Entità secondarie Root.Children1 e Root.Children2
  • un'istruzione LINQ Root.Include("Parent").Include("Children1").Include("Children2")

Questo crea un'istruzione SQL che ha la struttura seguente:

SELECT *, <PseudoColumns>
FROM Root
JOIN Parent
JOIN Children1

UNION

SELECT *, <PseudoColumns>
FROM Root
JOIN Parent
JOIN Children2

Questi <PseudoColumns> sono costituiti da espressioni come CAST(NULL AS int) AS [C2], e servono per avere la stessa quantità di colonne in tutti i UNION -ed query. La prima parte aggiunge pseudo colonne per Child2 , la seconda parte aggiunge pseudo colonne per Child1 .

Questo è ciò che significa per la dimensione del set di risultati SQL:

  • Numero di colonne nel SELECT clausola è la somma di tutte le colonne nelle quattro tabelle
  • Il numero di righe è la somma dei record nelle raccolte secondarie incluse

Poiché il numero totale di punti dati è columns * rows , ogni ulteriore Includes aumenta esponenzialmente il numero totale di punti dati nel set di risultati. Lascia che lo dimostri prendendo Root di nuovo, ora con un ulteriore Children3 collezione. Se tutte le tabelle hanno 5 colonne e 100 righe, otteniamo:

Un Includes (Root + 1 raccolta figlio):10 colonne * 100 righe =1000 punti dati.
Due Includes s (Root + 2 raccolte secondarie):15 colonne * 200 righe =3000 punti dati.
Tre Includes s (Root + 3 raccolte secondarie):20 colonne * 300 righe =6000 punti dati.

Con 12 Includes questo ammonterebbe a 78000 punti dati!

Al contrario, se ottieni tutti i record per ogni tabella separatamente invece di 12 Includes , hai 13 * 5 * 100 punti dati:6500, meno del 10%!

Ora questi numeri sono alquanto esagerati in quanto molti di questi punti dati saranno null , quindi non contribuiscono molto alla dimensione effettiva del set di risultati inviato al client. Ma la dimensione della query e l'attività per Query Optimizer vengono sicuramente influenzate negativamente dal numero crescente di Includes s.

Equilibrio

Quindi usando Includes è un delicato equilibrio tra il costo delle chiamate al database e il volume dei dati. È difficile dare una regola pratica, ma ormai puoi immaginare che il volume di dati generalmente supera rapidamente il costo delle chiamate extra se sono presenti più di ~3 Includes per le raccolte figlie (ma un po' di più per le Includes principali , che ampliano solo il set di risultati).

Alternativa

L'alternativa a Includes consiste nel caricare i dati in query separate:

context.Configuration.LazyLoadingEnabled = false;
var rootId = 1;
context.Children1.Where(c => c.RootId == rootId).Load();
context.Children2.Where(c => c.RootId == rootId).Load();
return context.Roots.Find(rootId);

Questo carica tutti i dati richiesti nella cache del contesto. Durante questo processo, EF esegue correzione della relazione in base al quale popola automaticamente le proprietà di navigazione (Root.Children ecc.) da entità caricate. Il risultato finale è identico all'istruzione con Includes s, fatta eccezione per un'importante differenza:le raccolte figlie non sono contrassegnate come caricate nel gestore dello stato dell'entità, quindi EF tenterà di attivare il caricamento lento se si accede ad esse. Ecco perché è importante disattivare il caricamento lento.

In realtà, dovrai capire quale combinazione di Includes e Load le dichiarazioni funzionano meglio per te.

Altri aspetti da considerare

Ogni Includes aumenta anche la complessità delle query, quindi Query Optimizer del database dovrà impegnarsi sempre di più per trovare il miglior piano di query. Ad un certo punto questo potrebbe non riuscire più. Inoltre, quando mancano alcuni indici vitali (in particolare su chiavi esterne), le prestazioni potrebbero risentirne aggiungendo Includes s, anche con il miglior piano di query.

Nucleo di Entity Framework

Esplosione cartesiana

Per qualche motivo, il comportamento descritto sopra, query UNIONed, è stato abbandonato a partire da EF core 3. Ora crea una query con join. Quando la query è a forma di "stella", ciò porta all'esplosione cartesiana (nel set di risultati SQL). Riesco a trovare solo una nota che annuncia questo cambiamento radicale, ma non dice perché.

Query divise

Per contrastare questa esplosione cartesiana, Entity Framework core 5 ha introdotto il concetto di query divise che consente di caricare i dati correlati in più query. Impedisce la creazione di un set di risultati SQL enorme e moltiplicato. Inoltre, a causa della minore complessità della query, può ridurre il tempo necessario per recuperare i dati anche con più roundtrip. Tuttavia, può portare a dati incoerenti quando si verificano aggiornamenti simultanei.

Più relazioni 1:n fuori dalla radice della query.