Modellazione per le prestazioni
In molti casi, il modo in cui si modella può avere un impatto profondo sulle prestazioni dell'applicazione; mentre un modello normalizzato e "corretto" è in genere un buon punto di partenza, nelle applicazioni reali alcuni compromessi pragmatici possono andare a lungo per ottenere buone prestazioni. Poiché è piuttosto difficile modificare il modello quando un'applicazione è in esecuzione nell'ambiente di produzione, vale la pena tenere presente le prestazioni durante la creazione del modello iniziale.
Denormalizzazione e memorizzazione nella cache
La denormalizzazione è la procedura per aggiungere dati ridondanti allo schema, in genere per eliminare i join durante l'esecuzione di query. Ad esempio, per un modello con blog e post, in cui ogni post ha una classificazione, potrebbe essere necessario visualizzare frequentemente la classificazione media del blog. L'approccio semplice a questo raggruppa i post in base al blog e calcola la media come parte della query; ma questo richiede un join costoso tra le due tabelle. Denormalizzazione aggiunge la media calcolata di tutti i post a una nuova colonna del blog, in modo che sia immediatamente accessibile, senza partecipare o calcolare.
Le informazioni di memorizzazione nella cache precedenti possono essere visualizzate come una forma di memorizzazione nella cache : le informazioni aggregate dei post vengono memorizzate nella cache nel blog e, come con qualsiasi memorizzazione nella cache, il problema è come mantenere aggiornato il valore memorizzato nella cache con i dati memorizzati nella cache. In molti casi, è ok che i dati memorizzati nella cache si ritardino per un po'; Ad esempio, nell'esempio precedente, è in genere ragionevole che la valutazione media del blog non sia completamente aggiornata in un determinato punto. Se questo è il caso, è possibile ricalcolato ogni ora e poi; in caso contrario, è necessario configurare un sistema più elaborato per mantenere aggiornati i valori memorizzati nella cache.
Di seguito vengono descritte alcune tecniche per la denormalizzazione e la memorizzazione nella cache in EF Core e si riferisce alle sezioni pertinenti nella documentazione.
Colonne calcolate archiviate
Se i dati da memorizzare nella cache sono un prodotto di altre colonne nella stessa tabella, una colonna calcolata archiviata può essere una soluzione perfetta. Ad esempio, un Customer
può avere FirstName
colonne e LastName
, ma potrebbe essere necessario cercare in base al nome completo del cliente. Una colonna calcolata archiviata viene gestita automaticamente dal database, che lo ricalcola ogni volta che la riga viene modificata, ed è anche possibile definire un indice su di esso per velocizzare le query.
Aggiornare le colonne della cache quando gli input cambiano
Se la colonna memorizzata nella cache deve fare riferimento agli input dall'esterno della riga della tabella, non è possibile utilizzare colonne calcolate. Tuttavia, è comunque possibile ricalcolare la colonna ogni volta che cambia l'input; Ad esempio, è possibile ricalcolare la valutazione media del blog ogni volta che un post viene modificato, aggiunto o rimosso. Assicurarsi di identificare le condizioni esatte quando è necessario ricalcolo. In caso contrario, il valore memorizzato nella cache non verrà sincronizzato.
Un modo per eseguire questa operazione consiste nell'eseguire manualmente l'aggiornamento tramite l'API EF Core normale. SaveChanges
Gli eventi o gli intercettori possono essere usati per controllare automaticamente se i post vengono aggiornati e per eseguire il ricalcolo in questo modo. Si noti che questo comporta in genere altri round trip del database, perché è necessario inviare comandi aggiuntivi.
Per applicazioni più sensibili alle prestazioni, è possibile definire trigger di database per eseguire automaticamente il ricalcolo nel database. In questo modo si salvano i round trip aggiuntivi del database, si verifica automaticamente all'interno della stessa transazione dell'aggiornamento principale e può essere più semplice da configurare. Entity Framework non fornisce api specifiche per la creazione o la gestione di trigger, ma è perfettamente corretto creare una migrazione vuota e aggiungere la definizione del trigger tramite SQL non elaborato.
Viste materializzate/indicizzate
Le viste materializzate (o indicizzate) sono simili a quelle normali, ad eccezione del fatto che i dati vengono archiviati su disco ("materializzati"), anziché calcolati ogni volta che viene eseguita una query sulla vista. Tali viste sono concettualmente simili alle colonne calcolate archiviate, perché memorizzano nella cache i risultati di calcoli potenzialmente costosi; tuttavia memorizzano nella cache un set di risultati di un'intera query anziché una singola colonna. Le viste materializzate possono essere sottoposte a query esattamente come qualsiasi normale tabella e, poiché vengono memorizzate nella cache su disco, tali query vengono eseguite in modo molto rapido e economico senza dover eseguire costantemente i calcoli costosi della query che definisce la vista.
Il supporto specifico per le viste materializzate varia in base ai database. In alcuni database ,ad esempio PostgreSQL, le viste materializzate devono essere aggiornate manualmente per poter sincronizzare i valori con le tabelle sottostanti. Questa operazione viene in genere eseguita tramite un timer, nei casi in cui un ritardo di dati è accettabile o tramite un trigger o una chiamata di stored procedure in condizioni specifiche. Le viste indicizzate di SQL Server, d'altra parte, vengono aggiornate automaticamente man mano che vengono modificate le tabelle sottostanti. In questo modo, la vista mostra sempre i dati più recenti, a costo di aggiornamenti più lenti. Inoltre, le viste indice di SQL Server hanno varie restrizioni sul supporto; per altre informazioni, vedere la documentazione .
EF attualmente non fornisce alcuna API specifica per la creazione o la gestione di viste, materializzate/indicizzate o in altro modo; ma è perfettamente corretto creare una migrazione vuota e aggiungere la definizione di vista tramite SQL non elaborato.
Mapping dell'ereditarietà
È consigliabile leggere la pagina dedicata sull'ereditarietà prima di continuare con questa sezione.
EF Core supporta attualmente tre tecniche per il mapping di un modello di ereditarietà a un database relazionale:
- Tabella per gerarchia (TPH), in cui viene eseguito il mapping di un'intera gerarchia .NET di classi a una singola tabella di database.
- Tabella per tipo (TPT), in cui viene eseguito il mapping di ogni tipo nella gerarchia .NET a una tabella diversa nel database.
- Table-per-concrete-type (TPC), in cui ogni tipo concreto nella gerarchia .NET viene mappato a una tabella diversa nel database, in cui ogni tabella contiene colonne per tutte le proprietà del tipo corrispondente.
La scelta della tecnica di mapping dell'ereditarietà può avere un notevole impatto sulle prestazioni dell'applicazione. È consigliabile misurare attentamente prima di impegnarsi in una scelta.
Intuitivamente, TPT potrebbe sembrare come la tecnica "più pulita" ; una tabella separata per ogni tipo .NET rende lo schema del database simile alla gerarchia dei tipi .NET. Inoltre, poiché TPH deve rappresentare l'intera gerarchia in una singola tabella, tutte le righe hanno tutte le colonne indipendentemente dal tipo effettivamente contenuto nella riga e le colonne non correlate sono sempre vuote e inutilizzate. Oltre a sembrare una tecnica di mapping "non pulita", molti ritengono che queste colonne vuote occupano spazio notevole nel database e potrebbero anche compromettere le prestazioni.
Suggerimento
Se il sistema di database lo supporta (e.g. SQL Server), prendere in considerazione l'uso di "colonne di tipo sparse" per le colonne TPH che verranno popolate raramente.
Tuttavia, la misurazione mostra che TPT è nella maggior parte dei casi la tecnica di mapping inferiore dal punto di vista delle prestazioni; dove tutti i dati in TPH provengono da una singola tabella, le query TPT devono unire più tabelle e i join sono una delle principali origini dei problemi di prestazioni nei database relazionali. I database tendono anche a gestire correttamente le colonne vuote e le funzionalità come le colonne di tipo sparse di SQL Server possono ridurre ulteriormente questo sovraccarico.
Il TPC presenta caratteristiche di prestazioni simili a TPH, ma è leggermente più lento quando si selezionano entità di tutti i tipi, in quanto questo comporta diverse tabelle. Tuttavia, TPC è davvero eccellente durante l'esecuzione di query per le entità di un singolo tipo foglia: la query usa solo una singola tabella e non richiede alcun filtro.
Per un esempio concreto, vedere questo benchmark che configura un modello semplice con una gerarchia di 7 tipi. Vengono eseguite il seeding di 5000 righe per ogni tipo, in totale 35000 righe e il benchmark carica semplicemente tutte le righe dal database:
metodo | Media | Error | StdDev | Generazione 0 | Gen1 | Allocato |
---|---|---|---|---|---|---|
TPH | 149,0 ms | 3,38 ms | 9,80 ms | 4000.0000 | 1000.0000 | 40 MB |
TPT | 312,9 ms | 6.17 ms | 10,81 ms | 9000.0000 | 3000.0000 | 75 MB |
TPC | 158,2 ms | 3,24 ms | 8,88 ms | 5000.0000 | 2000.0000 | 46 MB |
Come si può notare, TPH e TPC sono notevolmente più efficienti di TPT per questo scenario. Si noti che i risultati effettivi dipendono sempre dalla query specifica eseguita e dal numero di tabelle nella gerarchia, in modo che altre query possano mostrare un divario di prestazioni diverso; si consiglia di usare questo codice di benchmark come modello per testare altre query.