Mengoptimalkan SQLite itu rumit. Kinerja penyisipan massal aplikasi C dapat bervariasi dari 85 sisipan per detik hingga lebih dari 96.000 sisipan per detik!
Latar Belakang: Kami menggunakan SQLite sebagai bagian dari aplikasi desktop. Kami memiliki sejumlah besar data konfigurasi yang disimpan dalam file XML yang diuraikan dan dimuat ke dalam database SQLite untuk diproses lebih lanjut ketika aplikasi diinisialisasi. SQLite sangat ideal untuk situasi ini karena cepat, tidak memerlukan konfigurasi khusus, dan database disimpan dalam disk sebagai file tunggal.
Dasar Pemikiran: Awalnya saya kecewa dengan kinerja yang saya lihat. Ternyata kinerja SQLite dapat sangat bervariasi (baik untuk memasukkan massal dan memilih) tergantung pada bagaimana database dikonfigurasikan dan bagaimana Anda menggunakan API. Itu bukan masalah sepele untuk mencari tahu apa semua opsi dan teknik itu, jadi saya pikir itu bijaksana untuk membuat entri wiki komunitas ini untuk berbagi hasil dengan pembaca Stack Overflow untuk menyelamatkan orang lain dari masalah penyelidikan yang sama.
Eksperimen: Daripada hanya berbicara tentang tips kinerja dalam pengertian umum (yaitu "Gunakan transaksi!" ), Saya pikir yang terbaik adalah menulis beberapa kode C dan benar - benar mengukur dampak dari berbagai opsi. Kita akan mulai dengan beberapa data sederhana:
- File teks dibatasi TAB 28 MB (sekitar 865.000 catatan) dari jadwal transit lengkap untuk kota Toronto
- Mesin uji saya adalah P60 3,60 GHz yang menjalankan Windows XP.
- Kode ini dikompilasi dengan Visual C ++ 2005 sebagai "Release" dengan "Full Optimization" (/ Ox) dan Favor Fast Code (/ Ot).
- Saya menggunakan SQLite "Amalgamation", dikompilasi langsung ke aplikasi pengujian saya. Versi SQLite yang kebetulan saya miliki sedikit lebih tua (3.6.7), tetapi saya menduga hasil ini akan sebanding dengan rilis terbaru (silakan tinggalkan komentar jika Anda berpikir sebaliknya).
Mari kita menulis beberapa kode!
Kode: Program C sederhana yang membaca file teks baris demi baris, membagi string menjadi nilai-nilai dan kemudian memasukkan data ke dalam database SQLite. Dalam versi kode "baseline" ini, database dibuat, tetapi kami tidak akan benar-benar memasukkan data:
/*************************************************************
Baseline code to experiment with SQLite performance.
Input data is a 28 MB TAB-delimited text file of the
complete Toronto Transit System schedule/route info
from http://www.toronto.ca/open/datasets/ttc-routes/
**************************************************************/
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <string.h>
#include "sqlite3.h"
#define INPUTDATA "C:\\TTC_schedule_scheduleitem_10-27-2009.txt"
#define DATABASE "c:\\TTC_schedule_scheduleitem_10-27-2009.sqlite"
#define TABLE "CREATE TABLE IF NOT EXISTS TTC (id INTEGER PRIMARY KEY, Route_ID TEXT, Branch_Code TEXT, Version INTEGER, Stop INTEGER, Vehicle_Index INTEGER, Day Integer, Time TEXT)"
#define BUFFER_SIZE 256
int main(int argc, char **argv) {
sqlite3 * db;
sqlite3_stmt * stmt;
char * sErrMsg = 0;
char * tail = 0;
int nRetCode;
int n = 0;
clock_t cStartClock;
FILE * pFile;
char sInputBuf [BUFFER_SIZE] = "\0";
char * sRT = 0; /* Route */
char * sBR = 0; /* Branch */
char * sVR = 0; /* Version */
char * sST = 0; /* Stop Number */
char * sVI = 0; /* Vehicle */
char * sDT = 0; /* Date */
char * sTM = 0; /* Time */
char sSQL [BUFFER_SIZE] = "\0";
/*********************************************/
/* Open the Database and create the Schema */
sqlite3_open(DATABASE, &db);
sqlite3_exec(db, TABLE, NULL, NULL, &sErrMsg);
/*********************************************/
/* Open input file and import into Database*/
cStartClock = clock();
pFile = fopen (INPUTDATA,"r");
while (!feof(pFile)) {
fgets (sInputBuf, BUFFER_SIZE, pFile);
sRT = strtok (sInputBuf, "\t"); /* Get Route */
sBR = strtok (NULL, "\t"); /* Get Branch */
sVR = strtok (NULL, "\t"); /* Get Version */
sST = strtok (NULL, "\t"); /* Get Stop Number */
sVI = strtok (NULL, "\t"); /* Get Vehicle */
sDT = strtok (NULL, "\t"); /* Get Date */
sTM = strtok (NULL, "\t"); /* Get Time */
/* ACTUAL INSERT WILL GO HERE */
n++;
}
fclose (pFile);
printf("Imported %d records in %4.2f seconds\n", n, (clock() - cStartClock) / (double)CLOCKS_PER_SEC);
sqlite3_close(db);
return 0;
}
Kontrol"
Menjalankan kode apa adanya tidak benar-benar melakukan operasi basis data apa pun, tetapi akan memberi kita gambaran tentang seberapa cepat I / O file C mentah dan operasi pemrosesan string.
Mengimpor 864913 catatan dalam 0,94 detik
Bagus! Kita dapat melakukan 920.000 sisipan per detik, asalkan kita tidak benar-benar melakukan sisipan :-)
"Skenario Kasus Terburuk"
Kita akan membuat string SQL menggunakan nilai yang dibaca dari file dan memanggil operasi SQL menggunakan sqlite3_exec:
sprintf(sSQL, "INSERT INTO TTC VALUES (NULL, '%s', '%s', '%s', '%s', '%s', '%s', '%s')", sRT, sBR, sVR, sST, sVI, sDT, sTM);
sqlite3_exec(db, sSQL, NULL, NULL, &sErrMsg);
Ini akan menjadi lambat karena SQL akan dikompilasi ke dalam kode VDBE untuk setiap sisipan dan setiap sisipan akan terjadi dalam transaksi sendiri. Seberapa lambat?
Impor 864913 catatan dalam 9933,61 detik
Astaga! 2 jam dan 45 menit! Itu hanya 85 sisipan per detik.
Menggunakan Transaksi
Secara default, SQLite akan mengevaluasi setiap pernyataan INSERT / UPDATE dalam transaksi unik. Jika melakukan banyak menyisipkan, disarankan untuk membungkus operasi Anda dalam suatu transaksi:
sqlite3_exec(db, "BEGIN TRANSACTION", NULL, NULL, &sErrMsg);
pFile = fopen (INPUTDATA,"r");
while (!feof(pFile)) {
...
}
fclose (pFile);
sqlite3_exec(db, "END TRANSACTION", NULL, NULL, &sErrMsg);
Impor 864913 catatan dalam 38,03 detik
Itu lebih baik. Cukup dengan membungkus semua sisipan kami dalam satu transaksi meningkatkan kinerja kami menjadi 23.000 sisipan per detik.
Menggunakan Pernyataan Disiapkan
Menggunakan transaksi adalah peningkatan besar, tetapi mengkompilasi ulang pernyataan SQL untuk setiap sisipan tidak masuk akal jika kita menggunakan SQL yang sama berulang-ulang. Mari kita gunakan sqlite3_prepare_v2
untuk mengkompilasi pernyataan SQL kita sekali dan kemudian ikat parameter kita ke pernyataan itu menggunakan sqlite3_bind_text
:
/* Open input file and import into the database */
cStartClock = clock();
sprintf(sSQL, "INSERT INTO TTC VALUES (NULL, @RT, @BR, @VR, @ST, @VI, @DT, @TM)");
sqlite3_prepare_v2(db, sSQL, BUFFER_SIZE, &stmt, &tail);
sqlite3_exec(db, "BEGIN TRANSACTION", NULL, NULL, &sErrMsg);
pFile = fopen (INPUTDATA,"r");
while (!feof(pFile)) {
fgets (sInputBuf, BUFFER_SIZE, pFile);
sRT = strtok (sInputBuf, "\t"); /* Get Route */
sBR = strtok (NULL, "\t"); /* Get Branch */
sVR = strtok (NULL, "\t"); /* Get Version */
sST = strtok (NULL, "\t"); /* Get Stop Number */
sVI = strtok (NULL, "\t"); /* Get Vehicle */
sDT = strtok (NULL, "\t"); /* Get Date */
sTM = strtok (NULL, "\t"); /* Get Time */
sqlite3_bind_text(stmt, 1, sRT, -1, SQLITE_TRANSIENT);
sqlite3_bind_text(stmt, 2, sBR, -1, SQLITE_TRANSIENT);
sqlite3_bind_text(stmt, 3, sVR, -1, SQLITE_TRANSIENT);
sqlite3_bind_text(stmt, 4, sST, -1, SQLITE_TRANSIENT);
sqlite3_bind_text(stmt, 5, sVI, -1, SQLITE_TRANSIENT);
sqlite3_bind_text(stmt, 6, sDT, -1, SQLITE_TRANSIENT);
sqlite3_bind_text(stmt, 7, sTM, -1, SQLITE_TRANSIENT);
sqlite3_step(stmt);
sqlite3_clear_bindings(stmt);
sqlite3_reset(stmt);
n++;
}
fclose (pFile);
sqlite3_exec(db, "END TRANSACTION", NULL, NULL, &sErrMsg);
printf("Imported %d records in %4.2f seconds\n", n, (clock() - cStartClock) / (double)CLOCKS_PER_SEC);
sqlite3_finalize(stmt);
sqlite3_close(db);
return 0;
Mengimpor 864913 catatan dalam 16,27 detik
Bagus! Ada sedikit lebih banyak kode (jangan lupa untuk menelepon sqlite3_clear_bindings
dan sqlite3_reset
), tetapi kami telah lebih dari dua kali lipat kinerja kami menjadi 53.000 sisipan per detik.
PRAGMA sinkron = MATI
Secara default, SQLite akan berhenti setelah mengeluarkan perintah tulis tingkat OS. Ini menjamin bahwa data ditulis ke disk. Dengan menetapkan synchronous = OFF
, kami menginstruksikan SQLite untuk menyerahkan data ke OS untuk ditulis dan kemudian melanjutkan. Ada kemungkinan file database menjadi rusak jika komputer mengalami kerusakan (atau kegagalan daya) bencana sebelum data dituliskan ke piring:
/* Open the database and create the schema */
sqlite3_open(DATABASE, &db);
sqlite3_exec(db, TABLE, NULL, NULL, &sErrMsg);
sqlite3_exec(db, "PRAGMA synchronous = OFF", NULL, NULL, &sErrMsg);
Mengimpor 864913 catatan dalam 12,41 detik
Peningkatannya sekarang lebih kecil, tapi kami mencapai 69.600 sisipan per detik.
PRAGMA journal_mode = MEMORY
Pertimbangkan menyimpan jurnal rollback dalam memori dengan mengevaluasi PRAGMA journal_mode = MEMORY
. Transaksi Anda akan lebih cepat, tetapi jika Anda kehilangan daya atau program Anda macet selama transaksi, basis data Anda bisa dibiarkan dalam keadaan korup dengan transaksi yang diselesaikan sebagian:
/* Open the database and create the schema */
sqlite3_open(DATABASE, &db);
sqlite3_exec(db, TABLE, NULL, NULL, &sErrMsg);
sqlite3_exec(db, "PRAGMA journal_mode = MEMORY", NULL, NULL, &sErrMsg);
Impor 864913 catatan dalam 13,50 detik
Sedikit lebih lambat dari optimasi sebelumnya di 64.000 sisipan per detik.
PRAGMA sinkron = MATI dan PRAGMA journal_mode = MEMORY
Mari kita gabungkan dua optimasi sebelumnya. Ini sedikit lebih berisiko (jika terjadi kerusakan), tetapi kami hanya mengimpor data (tidak menjalankan bank):
/* Open the database and create the schema */
sqlite3_open(DATABASE, &db);
sqlite3_exec(db, TABLE, NULL, NULL, &sErrMsg);
sqlite3_exec(db, "PRAGMA synchronous = OFF", NULL, NULL, &sErrMsg);
sqlite3_exec(db, "PRAGMA journal_mode = MEMORY", NULL, NULL, &sErrMsg);
Mengimpor 864913 catatan dalam 12,00 detik
Fantastis! Kami dapat melakukan 72.000 sisipan per detik.
Menggunakan Database Dalam Memori
Hanya untuk iseng, mari kita membangun semua optimasi sebelumnya dan mendefinisikan kembali nama database sehingga kami bekerja sepenuhnya dalam RAM:
#define DATABASE ":memory:"
Mengimpor 864913 catatan dalam 10,94 detik
Tidak super praktis untuk menyimpan basis data kami dalam RAM, tetapi mengesankan bahwa kami dapat melakukan 79.000 sisipan per detik.
Refactoring Kode C
Meskipun tidak secara khusus peningkatan SQLite, saya tidak suka char*
operasi penugasan ekstra di while
loop. Mari kita cepat-cepat memperbaiki kode itu untuk meneruskan output strtok()
langsung ke sqlite3_bind_text()
, dan biarkan kompiler mencoba mempercepatnya untuk kita:
pFile = fopen (INPUTDATA,"r");
while (!feof(pFile)) {
fgets (sInputBuf, BUFFER_SIZE, pFile);
sqlite3_bind_text(stmt, 1, strtok (sInputBuf, "\t"), -1, SQLITE_TRANSIENT); /* Get Route */
sqlite3_bind_text(stmt, 2, strtok (NULL, "\t"), -1, SQLITE_TRANSIENT); /* Get Branch */
sqlite3_bind_text(stmt, 3, strtok (NULL, "\t"), -1, SQLITE_TRANSIENT); /* Get Version */
sqlite3_bind_text(stmt, 4, strtok (NULL, "\t"), -1, SQLITE_TRANSIENT); /* Get Stop Number */
sqlite3_bind_text(stmt, 5, strtok (NULL, "\t"), -1, SQLITE_TRANSIENT); /* Get Vehicle */
sqlite3_bind_text(stmt, 6, strtok (NULL, "\t"), -1, SQLITE_TRANSIENT); /* Get Date */
sqlite3_bind_text(stmt, 7, strtok (NULL, "\t"), -1, SQLITE_TRANSIENT); /* Get Time */
sqlite3_step(stmt); /* Execute the SQL Statement */
sqlite3_clear_bindings(stmt); /* Clear bindings */
sqlite3_reset(stmt); /* Reset VDBE */
n++;
}
fclose (pFile);
Catatan: Kami kembali menggunakan file database nyata. Database dalam memori cepat, tetapi belum tentu praktis
Mengimpor 864913 catatan dalam 8,94 detik
Sedikit refactoring ke kode pemrosesan string yang digunakan dalam pengikatan parameter kami telah memungkinkan kami untuk melakukan 96.700 sisipan per detik. Saya pikir aman untuk mengatakan bahwa ini sangat cepat . Ketika kita mulai mengubah variabel lain (yaitu ukuran halaman, pembuatan indeks, dll.) Ini akan menjadi patokan kami.
Ringkasan (sejauh ini)
Saya harap Anda masih bersama saya! Alasan kami memulai jalan ini adalah karena kinerja penyisipan massal sangat bervariasi dengan SQLite, dan tidak selalu jelas perubahan apa yang perlu dilakukan untuk mempercepat operasi kami. Menggunakan kompiler yang sama (dan opsi kompiler), versi SQLite yang sama dan data yang sama kami telah mengoptimalkan kode kami dan penggunaan SQLite kami untuk beralih dari skenario terburuk dari 85 sisipan per detik menjadi lebih dari 96.000 sisipan per detik!
BUAT INDEX lalu INSERT vs. INSERT lalu BUAT INDEX
Sebelum kita mulai mengukur SELECT
kinerja, kita tahu bahwa kita akan membuat indeks. Diusulkan dalam salah satu jawaban di bawah ini bahwa ketika melakukan penyisipan massal, lebih cepat membuat indeks setelah data dimasukkan (sebagai lawan membuat indeks terlebih dahulu kemudian memasukkan data). Mari mencoba:
Buat Indeks lalu Sisipkan Data
sqlite3_exec(db, "CREATE INDEX 'TTC_Stop_Index' ON 'TTC' ('Stop')", NULL, NULL, &sErrMsg);
sqlite3_exec(db, "BEGIN TRANSACTION", NULL, NULL, &sErrMsg);
...
Mengimpor 864913 catatan dalam 18,13 detik
Masukkan Data, lalu Buat Indeks
...
sqlite3_exec(db, "END TRANSACTION", NULL, NULL, &sErrMsg);
sqlite3_exec(db, "CREATE INDEX 'TTC_Stop_Index' ON 'TTC' ('Stop')", NULL, NULL, &sErrMsg);
Mengimpor 864913 catatan dalam 13,66 detik
Seperti yang diharapkan, sisipan massal lebih lambat jika satu kolom diindeks, tetapi itu membuat perbedaan jika indeks dibuat setelah data dimasukkan. Baseline tanpa indeks kami adalah 96.000 sisipan per detik. Membuat indeks terlebih dahulu kemudian memasukkan data memberi kita 47.700 sisipan per detik, sedangkan memasukkan data terlebih dahulu kemudian membuat indeks memberi kita 63.300 sisipan per detik.
Dengan senang hati saya akan mengambil saran untuk skenario lain untuk dicoba ... Dan akan segera mengkompilasi data serupa untuk pertanyaan SELECT.
sqlite3_clear_bindings(stmt);
? Anda menyetel binding setiap kali harus cukup: Sebelum memanggil sqlite3_step () untuk pertama kali atau segera setelah sqlite3_reset (), aplikasi dapat memanggil salah satu antarmuka sqlite3_bind () untuk melampirkan nilai pada parameter. Setiap panggilan ke sqlite3_bind () mengabaikan bindings sebelumnya pada parameter yang sama (lihat: sqlite.org/cintro.html ). Tidak ada dalam dokumen untuk fungsi yang mengatakan Anda harus memanggilnya.
feof()
untuk mengontrol penghentian loop input Anda. Gunakan hasil yang dikembalikan oleh fgets()
. stackoverflow.com/a/15485689/827263