Suggerimenti sulle prestazioni per l'accesso al database e Entity Framework

Suggerimenti sulle prestazioni per l'accesso al database e Entity Framework

Uno dei "trucchi" più comuni in un progetto di sviluppo è dimenticare le prestazioni finché non si verifica un problema. Ho sentito spesso persone citare Knuth dicendo che "l'ottimizzazione prematura è la radice di tutti i mali", suggerendo che in questo momento è troppo presto per pensare all'ottimizzazione delle prestazioni.

Ovviamente l'ottimizzazione e il miglioramento delle prestazioni vengono rimandati, rimandati e rimandati ancora un po'... fino a quando non c'è un test delle prestazioni in pre-produzione e tutto fallisce. (Se sei fortunato, almeno l'hai catturato prima che vada in produzione. Molte volte questo è il primo punto in cui viene individuato il problema).

Credo che sia necessario farlo funzionare prima di farlo funzionare velocemente, ma all'interno di questa affermazione, c'è un'implicazione che "lavorare" e "lavorare velocemente" sono entrambi necessario. Farlo funzionare non è abbastanza. E Knuth viene citato fuori contesto:la citazione completa è "Dovremmo dimenticare le piccole efficienze , diciamo circa il 97% delle volte:l'ottimizzazione prematura è la radice di tutti i mali. ” (enfasi mia). Sono piccole efficienze , non grandi. Dice anche “Nelle discipline ingegneristiche consolidate un miglioramento del 12%, facilmente ottenibile, non è mai considerato marginale e credo che lo stesso punto di vista dovrebbe prevalere nell'ingegneria del software “. 12%!!

Vorrei condividere 3 suggerimenti che ho utilizzato per fare un'enorme differenza per le prestazioni di un'applicazione .NET utilizzando Entity Framework. Ho spesso sentito persone criticare Entity Framework come lento, ma sto fuori dalle infinite discussioni religiose inutili sul fatto che lo sia o meno. Tutto quello che posso dire è che dalla mia esperienza, il collo di bottiglia delle prestazioni non è mai stato colpa di Entity Framework:è da qualche altra parte o dal modo in cui è stato utilizzato Entity Framework.

Indici mancanti

Questo non ha nulla a che fare con Entity Framework:si tratta di una modifica al database, non al codice .NET. Entity Framework genera SQL dietro le quinte e lo invia al database per l'esecuzione, e non ha idea se questo SQL eseguirà una scansione completa della tabella estremamente costosa o se utilizzerà gli indici in modo intelligente per evitare di dover cercare in ogni riga nel database.

Per me, questo è il primo porto di scalo quando qualcuno dice che un'applicazione che accede a un database è lenta. SQL Server dispone di alcuni ottimi strumenti per aiutare in questo:puoi utilizzare SQL Profiler per registrare un file di traccia di tutte le query SQL che colpiscono un database in un periodo di tempo, quindi utilizzare questo file di traccia in Ottimizzazione guidata motore di database per identificare quali indici che secondo il motore farà la differenza più grande per la tua applicazione.

Ho visto incredibili miglioramenti derivare da questa tecnica:i miglioramenti del 97% non sono rari. Ancora una volta, non è proprio un suggerimento di Entity Framework, ma vale la pena controllare.

Il problema "Seleziona N+1"

Quindi, di nuovo, non proprio un problema di Entity Framework... sì, c'è un po' di un tema che emerge qui! Questo è qualcosa che è comune a molti ORM.

Fondamentalmente penso che il problema sia un effetto collaterale del "caricamento pigro". Ad esempio, supponiamo che la tua applicazione interroghi un database sulle auto. Le auto sono rappresentate da un oggetto POCO "Auto", che contiene un elenco di oggetti figlio di tipo POCO "Ruota".

Dalla tua applicazione, potresti interrogare tramite chiave primaria un'auto con targa "ABC 123", che (si spera) restituisce un oggetto come risultato. Quindi chiami il metodo "Ruote" per ottenere informazioni sulle ruote dell'auto.

Se il tuo database è logicamente normalizzato, probabilmente hai fatto almeno due query qui:quella originale per ottenere l'auto e poi un'altra per ottenere informazioni sulle ruote. Se poi chiami una proprietà dall'oggetto "Ruota" che costituisce l'elenco, probabilmente eseguirai un'altra query del database per ottenere tali informazioni.

Questo è in realtà un enorme vantaggio degli ORM:tu come sviluppatore non devi fare lavoro extra per caricare informazioni sugli oggetti figlio e la query si verifica solo quando l'applicazione richiede informazioni su quell'oggetto. È tutto astratto da te e si chiama caricamento pigro.

Non c'è niente di sbagliato o di male nel caricamento pigro. Come ogni strumento, ha un posto e ci sono opportunità di usarlo in modo improprio. Il punto in cui l'ho visto abusato di più è nello scenario in cui uno sviluppatore:

  • restituisce un oggetto da una chiamata a Entity Framework;
  • chiude la sessione (es. connessione al database);
  • cerca nell'oggetto padre un oggetto figlio e ottiene un'eccezione che dice che la sessione è chiusa;

Lo sviluppatore quindi fa una delle due cose:

  • Lo sviluppatore sposta tutta la logica nel metodo in cui la sessione è aperta perché il caricamento lento risolve tutti i suoi problemi. Questo porta a un grande pasticcio di codice. Ad un certo punto, sempre, questo codice viene copiato e incollato, di solito in un ciclo, portando a carichi e carichi di query sul database. Poiché SQL Server è eccezionale, probabilmente ha eseguito tutte queste query in pochi secondi e nessuno se ne accorge fino a quando non viene distribuito in produzione e centinaia di utenti tentano di farlo tutto in una volta e il sito crolla. (Ok, è troppo drammatico:i tuoi eventi di test delle prestazioni lo prenderanno. Perché ovviamente stai facendo i test delle prestazioni prima di andare in produzione, vero? Non è vero ?)
  • Lo sviluppatore migliore si rende conto che spostare tutto il codice in un metodo è una cattiva idea e, anche se il caricamento lento ti consente di farlo, sta abusando della tecnica. Leggono alcuni blog, scoprono questa cosa chiamata caricamento ansioso e scrivono codice come questo:
var car = (from c in context.Cars.Include("Wheel")
            where c.RegistrationPlate == "ABC 123"
            select c).FirstOrDefault<Car>();

Entity Framework è abbastanza intelligente da riconoscere cosa sta succedendo qui:invece di eseguire una query stupida sulla tabella Car, si unisce alla tabella Wheel e invia una query per ottenere tutto ciò di cui ha bisogno per Car e Wheels.

Quindi questo va bene, ma nella mia carriera quasi tutte le applicazioni hanno una relazione molto più complessa tra oggetto ed entità di database rispetto a un semplice genitore e figlio. Questo porta a catene di query molto più complesse.

Una tecnica che ho utilizzato con successo è quella di creare una vista del database che includa tutto il necessario per il metodo di business dell'applicazione. Mi piace usare le viste perché mi dà un controllo molto più granulare su esattamente quali sono i join tra le tabelle e anche quali campi vengono restituiti dal database. Semplifica anche il codice Entity Framework. Ma il vantaggio più grande è che la vista diventa un'interfaccia – in realtà un contratto – tra il database e il codice. Quindi, se hai un esperto di database che ti dice "Guarda, i tuoi problemi di prestazioni dipendono da come è progettato il tuo database:posso risolverlo, ma se lo faccio probabilmente interromperò la tua applicazione ", sarai in grado di rispondere "Bene, interroghiamo il database attraverso una vista, quindi finché sei in grado di creare una vista con le stesse colonne e output, puoi modificare il database senza influire noi.

Ovviamente se stai utilizzando una vista del database, ciò significa che non sarai in grado di aggiornare gli oggetti utilizzando Entity Framework perché una vista è di sola lettura... il che vanifica lo scopo dell'utilizzo di un ORM. Tuttavia, se qualcuno richiede una correzione per un sito lento, è molto meno invadente creare e indicizzare una vista piuttosto che riprogettare l'applicazione.

Nota:non sto sostenendo questo come una bacchetta magica:è solo una tecnica che a volte ha il suo posto.

AsNoTracking

Questa è un'impostazione di Entity Framework. Se stai utilizzando le visualizzazioni, o sai che la tua chiamata a Entity Framework non dovrà aggiornare il database, puoi ottenere un ulteriore aumento delle prestazioni utilizzando la parola chiave AsNoTracking.

var cars = context.Cars.AsNoTracking().Where(c => c.Color == "Red");

Questo ti darà un aumento delle prestazioni se stai restituendo grandi volumi di dati, ma meno per volumi più piccoli. Il tuo chilometraggio può variare, ma ricorda che devi assicurarti di non aggiornare il contesto per utilizzarlo.

Riepilogo

  • Ignora la saggezza dei post dei newsgroup che dicono "Entity Framework è solo lento, niente che puoi fare";
  • Invece, esegui il profiler di SQL Server sul database e inserisci il file di traccia risultante tramite il Database Engine Tuning Adviser di SQL Server per trovare gli indici che miglioreranno le query più lente;
  • Analizza il codice per identificare il problema "Seleziona N+1":ce n'è quasi sempre uno nel codice da qualche parte. Se vuoi trovarlo, disattiva il caricamento lento ed esegui i test.
  • Se stai restituendo grandi volumi di dati in un elenco di sola lettura, verifica se puoi utilizzare AsNoTracking per ottenere un po' più di prestazioni dalla tua applicazione.