SQLite è un popolare database relazionale che puoi incorporare nella tua applicazione. Con una quantità crescente di dati nel database, è necessario applicare l'ottimizzazione delle prestazioni di SQLite. In questo articolo vengono illustrati gli indici e le relative insidie, l'uso del pianificatore di query, la modalità journal WAL (Write-Ahead-Logging) e l'aumento delle dimensioni della cache. Spiega anche l'importanza di misurare l'impatto delle tue modifiche, utilizzando test automatizzati.
Introduzione
SQLite è un popolare sistema di database relazionali (DB) . A differenza dei suoi fratelli maggiori basati su client-server, come MySQL, SQLite può essere incorporato nella tua applicazione come libreria . SQLite ha un set di funzionalità molto simile e può anche gestire milioni di righe, dato che conosci alcuni suggerimenti e trucchi sull'ottimizzazione delle prestazioni. Come mostreranno le sezioni seguenti, c'è altro da sapere sull'ottimizzazione delle prestazioni di SQLite oltre alla semplice creazione di indici.
Crea indici, ma con cautela
L'idea di base di un indice è di velocizzare la lettura di dati specifici , ovvero SELECT
istruzioni con un WHERE
clausola. Anche gli indici accelerano l'ordinamento dati (ORDER BY
), o JOIN
tavoli. Sfortunatamente, gli indici sono un'arma a doppio taglio, perché consumano ulteriore spazio su disco e rallentano la manipolazione dei dati (INSERT
, UPDATE
, DELETE
).
Il consiglio generale è quello di creare il minor numero di indici possibile, ma il maggior numero necessario . Inoltre, gli indici hanno senso solo per più grandi database, con migliaia o milioni di righe.
Utilizza il pianificatore di query per analizzare le tue query
Il modo in cui gli indici vengono utilizzati internamente da SQLite sono documentati, ma non di facile comprensione. Come ulteriormente elaborato da questo articolo, è una buona idea analizzare una query anteponendola a EXPLAIN QUERY PLAN
. Dai un'occhiata a ogni linea di output, di cui ci sono tre varianti di base:
SEARCH table ...
le linee sono un buon segno:SQLite usa uno dei tuoi indici!SCAN table ... USING INDEX
è un brutto segno,SCAN table ...
è anche peggio!
Cerca di evitare la SCAN table [using index]
voci nell'output di EXPLAIN QUERY PLAN
quando possibile, perché si verificheranno problemi di prestazioni su database più grandi. Usa EXPLAIN QUERY PLAN
per aggiungere o modificare iterativamente i tuoi indici fino a quando non avrai più SCAN table
vengono visualizzate le voci.
Ottimizza le query che coinvolgono IS NOT
Il controllo di IS NOT ...
è costoso perché SQLite dovrà scansionare tutte le righe della tabella, anche se la colonna interessata ha un indice . Gli indici sono utili solo se cerchi valori specifici, ad es. confronti che coinvolgono < (più piccolo), > (maggiore) o = (uguale), ma non fanno domanda per !=(disuguale).
Un piccolo trucco è che puoi sostituire WHERE column != value
con WHERE column > value OR column < value
. Ciò utilizzerà l'indice della colonna e influirà efficacemente su tutte le righe il cui valore non è uguale a value
. Allo stesso modo, un WHERE stringColumn != ''
può essere sostituito da WHERE stringColumn > ''
, perché le stringhe sono ordinabili. Tuttavia, quando applichi questo trucco, assicurati di sapere come SQLite gestisce NULL
confronti. Ad esempio, SQLite valuta NULL > ''
come FALSE
.
Se usi un simile trucco di confronto, c'è un altro avvertimento nel caso in cui la tua query contenga WHERE
e ORDER BY
, ognuno con una colonna diversa:questo renderà la query nuovamente inefficiente. Se possibile, usa lo uguale colonna in WHERE
e ORDER BY
oppure crea un indice di copertura che coinvolge sia il WHERE
e ORDER BY
colonna.
Migliora la velocità di scrittura con il registro Write-Ahead
Il Write-Ahead-Logging (WAL) la modalità journal migliora notevolmente le prestazioni di scrittura/aggiornamento , rispetto al rollback predefinito modalità diario. Tuttavia, come documentato qui, ci sono alcuni avvertimenti . Ad esempio, la modalità WAL non è disponibile su alcuni sistemi operativi. Inoltre, ci sono garanzie di coerenza dei dati ridotte in caso di guasto hardware . Assicurati di leggere le prime pagine per capire cosa stai facendo.
Ho trovato che il comando PRAGMA synchronous = NORMAL
fornisce un'accelerazione 3-4x. Impostazione di journal_mode
a WAL
quindi migliora nuovamente le prestazioni in modo significativo (circa 10 volte o più, a seconda del sistema operativo).
Oltre agli avvertimenti che ho già menzionato, dovresti anche essere consapevole di quanto segue:
- Utilizzando la modalità journal WAL, ci saranno due file aggiuntivi accanto al file di database sul filesystem, che hanno lo stesso nome del database, ma con suffisso “-shm” e “-wal”. Normalmente non devi preoccuparti, ma se dovessi inviare il database a un'altra macchina mentre l'applicazione è in esecuzione, non dimenticare di includere quei due file. SQLite compatta questi due file nel file principale ogni volta che normalmente chiudete tutte le connessioni al database aperte.
- Le prestazioni di inserimento o aggiornamento diminuiranno occasionalmente, ogni volta che la query attiva l'unione del contenuto del file di registro WAL nel file di database principale. Questo si chiama checkpoint , vedi qui.
- Ho trovato quel
PRAGMA
s che cambianojournal_mode
esynchronous
non sembrano essere archiviati in modo persistente nel database. Pertanto, io sempre eseguirli nuovamente ogni volta che apro una nuova connessione al database, invece di eseguirli semplicemente quando creo le tabelle per la prima volta.
Misura tutto
Ogni volta che aggiungi modifiche alle prestazioni, assicurati di misurare l'impatto. I test (unità) automatizzati sono un ottimo approccio a questo scopo. Aiutano a documentare i tuoi risultati per il tuo team e scopriranno comportamenti devianti nel tempo , per esempio. quando aggiorni a una versione SQLite più recente. Esempi di cosa puoi misurare:
- Qual è l'effetto dell'utilizzo del WAL modalità journal tramite il rollback modalità? Qual è l'effetto di altri
PRAGMA
che migliorano le prestazioni (presumibilmente). s? - Una volta aggiunto/modificato/rimosso un indice, quanto più velocemente
SELECT
dichiarazioni diventano? Quanto è più lentoINSERT/DELETE/UPDATE
le dichiarazioni diventano? - Quanto spazio aggiuntivo su disco consumano gli indici?
Per ognuno di questi test, considera di ripeterli con dimensioni di database variabili. Per esempio. eseguirli su un database vuoto e anche su un database che contiene già migliaia (o milioni) di voci. Dovresti anche eseguire i test su diversi dispositivi e sistemi operativi, soprattutto se il tuo ambiente di sviluppo e produzione è sostanzialmente diverso.
Regola la dimensione della cache
SQLite archivia le informazioni temporanee in una cache (nella RAM), ad es. durante la creazione dei risultati di un SELECT
query o durante la manipolazione di dati che non sono ancora stati confermati. Per impostazione predefinita, questa dimensione è di soli 2 MB . Le moderne macchine desktop possono risparmiare molto di più. Esegui PRAGMA cache_size = -kibibytes
per aumentare questo valore (attenzione al meno firmare davanti al valore!). Vedi qui per ulteriori informazioni. Ancora una volta, misura che impatto ha questa impostazione sulle prestazioni!
Utilizza REPLACE INTO per creare o aggiornare una riga
Questo potrebbe non essere tanto un ritocco delle prestazioni in quanto è un piccolo trucco accurato. Supponiamo di dover aggiornare una riga nella tabella t
o crea una riga se non esiste ancora. Anziché utilizzare due query (SELECT
seguito da INSERT
o UPDATE
), usa il REPLACE INTO
(documenti ufficiali).
Affinché funzioni, aggiungi una colonna fittizia aggiuntiva (ad es. replacer
) alla tabella t
, che ha un UNIQUE
vincolare. La dichiarazione della colonna potrebbe ad es. be ... replacer INTEGER UNIQUE ...
che fa parte del tuo CREATE TABLE
dichiarazione. Quindi usa una query come
REPLACE INTO t (col1, col2, ..., replacer) VALUES (?,?,...,1)
Code language: SQL (Structured Query Language) (sql)
Quando questa query viene eseguita per la prima volta, eseguirà semplicemente un INSERT
. Quando viene eseguito la seconda volta, UNIQUE
vincolo del replacer
si attiverà la colonna e il comportamento di risoluzione dei conflitti causerà l'eliminazione della vecchia riga, creandone una nuova automaticamente. Potresti anche trovare utile il relativo comando UPSERT.
Conclusione
Una volta che il numero di righe nel database aumenta, le modifiche alle prestazioni diventano una necessità. Gli indici sono la soluzione più comune. Scambiano una maggiore complessità temporale con una ridotta complessità spaziale, migliorando la velocità di lettura e influenzando negativamente le prestazioni di modifica dei dati. Ho dimostrato che devi prestare molta attenzione quando confronti la disuguaglianza in SELECT
istruzioni, perché SQLite non può utilizzare gli indici per questo tipo di confronti. In genere consiglio di utilizzare il pianificatore di query che spiega cosa succede internamente per ogni query SQL. Ogni volta che modifichi qualcosa, misura l'impatto!