SQLite optimizasyonu zor. C uygulamasının toplu ekleme performansı saniyede 85 kesici uçtan saniyede 96.000 kesici uca kadar değişebilir!
Arka Plan: SQLite'ı bir masaüstü uygulamasının parçası olarak kullanıyoruz. XML dosyalarında depolanan ve uygulama başlatıldığında daha ileri işlemler için SQLite veritabanına yüklenen büyük miktarda yapılandırma verimiz var. SQLite bu durum için idealdir, çünkü hızlıdır, özel bir yapılandırma gerektirmez ve veritabanı diskte tek bir dosya olarak saklanır.
Gerekçe: Başlangıçta gördüğüm performanstan hayal kırıklığına uğradım. SQLite'nin performansının, veritabanının nasıl yapılandırıldığına ve API'yi nasıl kullandığınıza bağlı olarak önemli ölçüde değişebileceği (hem toplu ekler hem de seçimler) ortaya çıkıyor. Tüm seçeneklerin ve tekniklerin ne olduğunu anlamak önemsiz bir mesele değildi, bu yüzden sonuçları aynı araştırmaların sorununu kurtarmak için Stack Overflow okuyucularıyla paylaşmak için bu topluluk wiki girişini oluşturmanın ihtiyatlı olduğunu düşündüm.
Deney: Genel anlamda performans ipuçlarından bahsetmektense (yani "İşlem kullanın!" ), Biraz C kodu yazmanın ve aslında çeşitli seçeneklerin etkisini ölçmenin en iyi yol olduğunu düşündüm . Bazı basit verilerle başlayacağız:
- Toronto şehri için toplu taşıma programının 28 MB TAB ile ayrılmış metin dosyası (yaklaşık 865.000 kayıt)
- Test makinem Windows XP çalıştıran 3.60 GHz P4.
- Kod, Visual C ++ 2005 ile "Tam Optimizasyon" (/ Ox) ve Hızlı Kod (/ Ot) ile "Release" olarak derlenir .
- Doğrudan test uygulamamda derlenen SQLite "Amalgamation" kullanıyorum. Sahip olduğum SQLite sürümü biraz daha eski (3.6.7), ancak bu sonuçların en son sürümle karşılaştırılacağından şüpheleniyorum (aksi takdirde lütfen bir yorum bırakın).
Biraz kod yazalım!
Kod: Metin dosyasını satır satır okuyan, dizeyi değerlere böler ve ardından verileri bir SQLite veritabanına ekleyen basit bir C programı. Kodun bu "temel" sürümünde veritabanı oluşturulur, ancak aslında veri eklemeyiz:
/*************************************************************
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"
Kodu olduğu gibi çalıştırmak aslında herhangi bir veritabanı işlemi gerçekleştirmez, ancak ham C dosyası G / Ç ve dize işleme işlemlerinin ne kadar hızlı olduğu hakkında bize bir fikir verecektir.
0.94 saniyede 864913 kayıt içe aktarıldı
Harika! Aslında herhangi bir kesici uç yapmamak koşuluyla saniyede 920.000 kesici uç yapabiliriz :-)
"En Kötü Durum Senaryosu"
Dosyadan okunan değerleri kullanarak SQL dizesini oluşturacağız ve sqlite3_exec kullanarak bu SQL işlemini başlatacağız:
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);
Bu yavaş olacaktır çünkü SQL her ek için VDBE kodunda derlenecek ve her ek kendi işleminde gerçekleşecektir. Ne kadar yavaş?
9933,61 saniyede 864913 kayıt içe aktarıldı
Olmadı! 2 saat 45 dakika! Saniyede sadece 85 kesici uç.
İşlem Kullanma
Varsayılan olarak, SQLite benzersiz bir işlem içindeki her INSERT / UPDATE deyimini değerlendirir. Çok sayıda kesici uç gerçekleştiriyorsanız, işleminizi bir işlemde sarmanız önerilir:
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);
38.03 saniyede 864913 kayıt içe aktarıldı
Bu daha iyi. Tüm kesici uçlarımızı tek bir işlemle sarmak, performansımızı saniyede 23.000 kesici uca yükseltti.
Hazırlanan Bir İfadeyi Kullanma
Bir işlemi kullanmak büyük bir gelişmeydi, ancak aynı SQL'i tekrar tekrar kullanırsak, her ekleme için SQL deyimini yeniden derlemek mantıklı değil. sqlite3_prepare_v2
SQL ifademizi bir kez derlemek ve parametrelerimizi şu ifadeyi kullanarak şu ifadeye bağlamak için kullanalım 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;
16.27 saniyede 864913 kayıt içe aktarıldı
Güzel! Biraz daha fazla kod var (aramayı unutmayın sqlite3_clear_bindings
ve sqlite3_reset
), ancak performansımızı saniyede 53.000 eke iki kattan fazla artırdık .
PRAGMA senkronize = KAPALI
Varsayılan olarak, SQLite işletim sistemi düzeyinde yazma komutu verildikten sonra duraklar. Bu, verilerin diske yazılmasını garanti eder. Ayarına göre synchronous = OFF
, devam sonra yazıp için basitçe el-off OS verilere SQLite talimat verirsiniz. Bilgisayar tabağa yazılmadan önce bilgisayar yıkıcı bir çökme (veya elektrik kesintisi) geçirirse veritabanı dosyasının bozulma olasılığı vardır:
/* 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);
12,41 saniyede 864913 kayıt içe aktarıldı
İyileştirmeler artık daha küçük, ancak saniyede 69.600 adede kadar ekleme yapıyoruz.
PRAGMA journal_mode = BELLEK
Değerlendirerek geri alma günlüğünü bellekte saklamayı düşünün PRAGMA journal_mode = MEMORY
. İşleminiz daha hızlı olacaktır, ancak bir işlem sırasında güç kaybederseniz veya programınız çökerse, veritabanınız kısmen tamamlanmış bir işlemle bozuk bir durumda bırakılabilir:
/* 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);
13.50 saniyede 864913 kayıt içe aktarıldı
Saniyede 64.000 kesici uç ile önceki optimizasyondan biraz daha yavaş .
PRAGMA senkronize = KAPALI ve PRAGMA journal_mode = BELLEK
Önceki iki optimizasyonu birleştirelim. Biraz daha riskli (bir kilitlenme durumunda), ancak sadece veri içe aktarıyoruz (banka çalıştırmıyoruz):
/* 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);
12.00 saniyede 864913 kayıt içe aktarıldı
Fantastik! Saniyede 72.000 kesici uç yapabiliriz .
Bellek İçi Veritabanı Kullanma
Sadece tekmeler için, önceki tüm optimizasyonları temel alalım ve veritabanı dosya adını yeniden tanımlayalım, böylece tamamen RAM'de çalışıyoruz:
#define DATABASE ":memory:"
10.94 saniyede 864913 kayıt içe aktarıldı
Veritabanımızı RAM'de saklamak çok pratik değil, ancak saniyede 79.000 kesici uç yapabilmemiz etkileyici .
Yeniden Düzenleme C Kodu
Özellikle bir SQLite iyileştirmesi olmasa char*
da, while
döngüdeki ekstra atama işlemlerini sevmiyorum . strtok()
Doğrudan çıktıya geçmek için bu kodu hızla yeniden düzenleyelim sqlite3_bind_text()
ve derleyicinin işleri hızlandırmaya çalışmasına izin verin:
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);
Not: Gerçek bir veritabanı dosyası kullanmaya geri döndük. Bellek içi veritabanları hızlıdır ancak pratik olması gerekmez
8.94 saniyede 864913 kayıt içe aktarıldı
Parametre bağlamamızda kullanılan dize işleme koduna hafif bir yeniden düzenleme, saniyede 96,700 kesici uç yapmamıza izin verdi . Bunun çok hızlı olduğunu söylemek güvenli . Diğer değişkenleri değiştirmeye başladığımızda (yani sayfa boyutu, dizin oluşturma vb.) Bu bizim ölçütümüz olacaktır.
Özet (şimdiye kadar)
Umarım hala benimlesin! Bu yolda başlamamızın nedeni, toplu ekleme performansının SQLite ile çok çılgınca değişmesidir ve operasyonumuzu hızlandırmak için hangi değişikliklerin yapılması gerektiği her zaman açık değildir. Aynı derleyiciyi (ve derleyici seçeneklerini), aynı SQLite sürümünü ve aynı verileri kullanarak kodumuzu ve SQLite kullanımımızı saniyede 85 kesici uçtan saniyede 96.000'den fazla kesici uca geçmek için optimize ettik !
INDEX OLUŞTUR sonra INSERT vs. EKLE ardından INDEX OLUŞTUR
SELECT
Performansı ölçmeye başlamadan önce , endeksler oluşturacağımızı biliyoruz. Aşağıdaki yanıtlardan birinde, toplu ekler yaparken, veri eklendikten sonra dizini oluşturmak daha hızlıdır (önce dizini oluşturmaktan sonra verileri eklemek yerine). Hadi deneyelim:
Dizin Oluşturun, sonra Veri Ekle
sqlite3_exec(db, "CREATE INDEX 'TTC_Stop_Index' ON 'TTC' ('Stop')", NULL, NULL, &sErrMsg);
sqlite3_exec(db, "BEGIN TRANSACTION", NULL, NULL, &sErrMsg);
...
18.13 saniyede 864913 kayıt içe aktarıldı
Veri Ekle ve Dizin Oluştur
...
sqlite3_exec(db, "END TRANSACTION", NULL, NULL, &sErrMsg);
sqlite3_exec(db, "CREATE INDEX 'TTC_Stop_Index' ON 'TTC' ('Stop')", NULL, NULL, &sErrMsg);
13.66 saniyede 864913 kayıt içe aktarıldı
Beklendiği gibi, bir sütun dizine eklendiğinde toplu ekler daha yavaştır, ancak veri eklendikten sonra dizin oluşturulursa fark yaratır. Endekssiz taban çizgimiz saniyede 96.000 eklemedir. Önce dizini oluşturmak ve sonra veri eklemek bize saniyede 47.700 ek verirken, önce veri eklemek sonra dizini oluşturmak bize saniyede 63.300 ek verir.
Memnuniyetle denemek için diğer senaryolar için öneriler almak istiyorum ... Ve yakında SELECT sorguları için benzer verileri derlemek olacaktır.
sqlite3_clear_bindings(stmt);
mi? Bağlamaları her zaman yeterli olacak şekilde ayarlarsınız: sqlite3_step () öğesini ilk kez çağırmadan önce veya sqlite3_reset () yönteminden hemen sonra, uygulama parametrelere değer eklemek için sqlite3_bind () arabirimlerinden birini çağırabilir. Sqlite3_bind ( ) öğesine yapılan her çağrı, aynı parametrede önceki bağlamaları geçersiz kılar (bkz: sqlite.org/cintro.html ). Dokümanlar'da bu işlev için onu çağırmanız gerektiğini söyleyen hiçbir şey yoktur .
feof()
Giriş döngünüzün sonlandırılmasını kontrol etmek için kullanmayın . Tarafından döndürülen sonucu kullanın fgets()
. stackoverflow.com/a/15485689/827263