SQLite'nin saniyede INSERT performansını artırın


2975

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_v2SQL 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_bindingsve 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, whiledö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

SELECTPerformansı ö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.


8
İyi bir nokta! Bizim durumumuzda, XML ve CSV metin dosyalarından okunan yaklaşık 1,5 milyon anahtar / değer çifti ile 200 bin kayıt olarak ilgileniyoruz. SO gibi siteleri çalıştıran veritabanlarıyla karşılaştırıldığında küçük - ancak SQLite performansını ayarlamaya yetecek kadar büyük.
Mike Willekes

51
"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." neden XML'de depolamak ve başlangıçta herşeyi yüklemek yerine her şeyi sqlite veritabanında tutmuyorsunuz?
CAFxX

14
Aramamayı denedin 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 .
ahcox

21
Tekrarlanan ölçümler yaptınız mı? 7 yerel işaretçi kaçınmak için 4s "kazanmak" garip, hatta karışık bir optimizer varsayalım.
peterchen

5
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
Keith Thompson

Yanıtlar:


785

Birkaç ipucu:

  1. Eklemeleri / güncellemeleri bir işleme koyun.
  2. SQLite'nin eski sürümleri için - Daha az paranoyak bir günlük modu ( pragma journal_mode) düşünün . Orada NORMAL, ve sonra da, OFFişletim sistemi çökmesi durumunda muhtemelen bozulan veritabanı hakkında çok endişeli değilseniz, ekleme hızını önemli ölçüde artırabilir. Uygulamanız çökerse verilerin iyi olması gerekir. Daha yeni sürümlerde, OFF/MEMORYayarların uygulama düzeyindeki kilitlenmeler için güvenli olmadığını unutmayın.
  3. Sayfa boyutları ile oynamak da bir fark yaratır ( PRAGMA page_size). Daha büyük sayfa boyutlarına sahip olmak, daha büyük sayfalar bellekte tutulduğundan okuma ve yazma işlemlerini biraz daha hızlandırabilir. Veritabanınız için daha fazla bellek kullanılacağını unutmayın.
  4. Endeksleriniz varsa, CREATE INDEXtüm eklerinizi yaptıktan sonra aramayı düşünün . Bu, dizini oluşturmaktan ve eklerinizi yapmaktan önemli ölçüde daha hızlıdır.
  5. SQLite'a eşzamanlı erişiminiz varsa oldukça dikkatli olmalısınız, çünkü yazma işlemi tamamlandığında tüm veritabanı kilitlenir ve birden fazla okuyucu mümkün olmasına rağmen yazma işlemleri kilitlenir. Bu, yeni SQLite sürümlerine bir WAL eklenmesi ile bir miktar geliştirildi.
  6. Yerden tasarruf edin ... daha küçük veritabanları daha hızlı ilerler. Örneğin, anahtar / değer çiftleriniz INTEGER PRIMARY KEYvarsa , anahtarı mümkünse tablodan ima edilen benzersiz satır numarası sütununun yerini alacak şekilde yapmayı deneyin .
  7. Birden çok iş parçacığı kullanıyorsanız, yüklenen sayfaların iş parçacıkları arasında paylaşılmasına izin verecek ve pahalı G / Ç çağrılarını önleyebilecek paylaşılan sayfa önbelleğini kullanmayı deneyebilirsiniz .
  8. Kullanma !feof(file)!

Burada ve burada da benzer sorular sordum .


9
Dokümanlar bir PRAGMA journal_mode bilmiyor NORMAL sqlite.org/pragma.html#pragma_journal_mode
OneWorld

4
Bir süre geçti, önerilerim bir WAL sunulmadan önce eski sürümler için uygulandı. Görünüşe göre DELETE yeni normal ayardır ve şimdi OFF ve MEMORY ayarları da var. KAPALI / HAFIZA'nın veritabanı bütünlüğü pahasına yazma performansını artıracağını ve KAPALI'nın geri alma işlemlerini tamamen devre dışı bıraktığını düşünüyorum.
Snazzer

4
# 7 için, c # system.data.sqlite sarmalayıcısını kullanarak paylaşılan sayfa önbelleğini etkinleştirme örneğiniz var mı?
Aaron Hudon

4
4. çağlar eski anıları geri getirdi - Önceki zamanlarda en az bir vaka, bir grup eklemeden önce bir dizinin bırakılması ve daha sonra eklerin önemli ölçüde hızlandırılması. Dönem için masaya tek erişiminiz olduğunu bildiğiniz bazı ekler için hala modern sistemlerde daha hızlı çalışabilir.
Bill K

Başparmak # 1: İşlemlerde kendim çok iyi şanslar yaşadım.
Enno

146

Bu ekler SQLITE_STATICyerine kullanmayı deneyin SQLITE_TRANSIENT.

SQLITE_TRANSIENT SQLite, döndürmeden önce dize verilerini kopyalamasına neden olur.

SQLITE_STATICverdiğiniz bellek adresinin sorgu yapılana kadar geçerli olacağını söyler (bu döngüde her zaman geçerlidir). Bu, döngü başına birkaç tahsis, kopyalama ve yeniden konumlandırma işlemi kurtaracaktır. Muhtemelen büyük bir gelişme.


109

Kaçının sqlite3_clear_bindings(stmt).

Testteki kod, her seferinde yeterli olması gereken bağları ayarlar.

C API intro SQLite dokümanlardan diyor ki:

Sqlite3_step () öğesini ilk kez çağırmadan önce veya sqlite3_reset () yönteminden hemen sonra , uygulama parametrelere değer eklemek için sqlite3_bind () arabirimlerini çağırabilir . Sqlite3_bind () öğesine yapılan her çağrı , aynı parametre üzerindeki önceki bağlamaları geçersiz kılar

Dokümanlarda sqlite3_clear_bindingssadece ciltleri ayarlamanın yanı sıra onu çağırmanız gerektiğini söyleyen hiçbir şey yoktur .

Daha fazla detay: Avoid_sqlite3_clear_bindings ()


5
Harika doğru: "Sqlite3_reset () birçok sezgi aksine, hazırlanmış bir deyimdeki bağları sıfırlamaz. Tüm ana bilgisayar parametrelerini NULL olarak sıfırlamak için bu yordamı kullanın." - sqlite.org/c3ref/clear_bindings.html
Francis Straccia

63

Toplu uçlarda

Bu yazıdan ve beni buraya getiren Yığın Taşması sorusundan esinlenerek - Bir SQLite veritabanına aynı anda birden çok satır eklemek mümkün müdür? - İlk Git veri havuzumu gönderdim :

https://github.com/rdpoor/CreateOrUpdate

bir dizi ActiveRecord dizisini MySQL , SQLite veya PostgreSQL veritabanlarına toplu olarak yükler . Mevcut kayıtları yoksayma, üzerine yazma veya hata oluşturma seçeneği içerir. Temel ölçütlerim sıralı yazmalara kıyasla 10 kat daha hızlı bir gelişme gösteriyor - YMMV.

Sık sık büyük veri kümelerini içe aktarmam gereken üretim kodunda kullanıyorum ve bundan oldukça memnunum.


4
@Jess: Bağlantıyı takip ederseniz, toplu ekleme sözdizimi anlamına geldiğini göreceksiniz.
Alix Axel

48

INSERT / UPDATE ifadelerinizi yığınlayabilirseniz, toplu içe aktarma en iyi performansı gösterir . 10.000 veya daha fazla bir değer benim için sadece birkaç satır olan bir masada iyi çalıştı, YMMV ...


22
X = 10.000'i ayarlamak istersiniz, böylece x = önbellek [= cache_size * sayfa_boyutu] / ekinizin ortalama boyutu.
Alix Axel

43

Sadece okumaya önem veriyorsanız, biraz daha hızlı (eski verileri okuyabilir) sürümü birden çok iş parçacığından (iş parçacığı başına bağlantı) birden çok bağlantıdan okumaktır.

Önce tablodaki öğeleri bulun:

SELECT COUNT(*) FROM table

sonra sayfalarda okuyun (LIMIT / OFFSET):

SELECT * FROM table ORDER BY _ROWID_ LIMIT <limit> OFFSET <offset>

burada ve iş parçacığı başına şu şekilde hesaplanır:

int limit = (count + n_threads - 1)/n_threads;

her iplik için:

int offset = thread_index * limit

Bizim küçük (200mb) db için bu% 50-75 hız artışı yaptı (Windows 7'de 3.8.0.2 64-bit). Tablolarımız büyük ölçüde normalleştirilmemiştir (1000-1500 sütun, yaklaşık 100.000 veya daha fazla satır).

Çok fazla veya çok az iş parçacığı bunu yapmayacak, kendiniz karşılaştırmalı ve profil oluşturmalısınız.

Ayrıca bizim için, SHAREDCACHE performansı yavaşlattı, bu yüzden PRIVATECACHE'i manuel olarak koydum (çünkü bizim için küresel olarak etkinleştirildi)


29

Ben cache_size daha yüksek bir değere yükselene kadar işlemlerden herhangi bir kazanç elde edemedim PRAGMA cache_size=10000;


Not için pozitif bir değer kullanarak bu cache_sizesetleri önbelleğe sayfa sayısını değil, toplam RAM boyutu. Varsayılan 4kB sayfa boyutuyla bu ayar, açık dosya başına (veya paylaşılan önbellekle çalıştırılıyorsa işlem başına) 40 MB'a kadar veri saklar .
Groo

21

Bu öğreticiyi okuduktan sonra programıma uygulamaya çalıştım.

Adresler içeren 4-5 dosyam var. Her dosyanın yaklaşık 30 milyon kaydı vardır. Önerdiğiniz yapılandırmayı kullanıyorum, ancak saniyedeki INSERT sayısım çok düşük (saniyede ~ 10.000 kayıt).

İşte öneriniz başarısız. Tüm kayıtlar için tek bir işlem ve hata / hata içermeyen tek bir ekleme kullanırsınız. Her kaydı farklı tablolarda birden çok eke böldüğünüzü varsayalım. Kayıt bozulursa ne olur?

ON CONFLICT komutu uygulanmaz, çünkü bir kayıtta 10 öğeniz varsa ve her öğenin farklı bir tabloya eklenmesi gerekiyorsa, öğe 5 bir CONSTRAINT hatası alırsa, önceki 4 ekin de gitmesi gerekir.

İşte geri dönüş burada. Geri alma ile ilgili tek sorun, tüm eklerinizi kaybetmeniz ve üstten başlamanızdır. Bunu nasıl çözebilirsin?

Benim çözümüm birden fazla işlem kullanmaktı . Her 10.000 kayıtta bir işleme başlayıp bitiriyorum (neden bu sayıyı sorma, test ettiğim en hızlı sayı). 10.000 büyüklüğünde bir dizi oluşturdum ve başarılı kayıtları buraya ekledim. Hata oluştuğunda, bir geri alma, bir işlem başlatmak, benim diziden kayıtları eklemek, taahhüt ve kırık kayıttan sonra yeni bir işlem başlatmak.

Bu çözüm, kötü / yinelenen kayıtlar içeren dosyalarla uğraşırken yaşadığım sorunları atlamama yardımcı oldu (neredeyse% 4 kötü kayıtlarım vardı).

Oluşturduğum algoritma, işlemimi 2 saat azaltmama yardımcı oldu. 1 saat 30 metrelik son yükleme işlemi hala yavaş ama başlangıçta aldığı 4 saat ile kıyaslanmıyor. Uçları 10.000 / s'den ~ 14.000 / s'ye hızlandırmayı başardım

Herhangi birinin hızlandırması hakkında başka fikirleri varsa, önerilere açıkım.

GÜNCELLEME :

Yukarıdaki yanıtıma ek olarak, kullandığınız sabit sürücüye bağlı olarak saniyede kesici uçların olduğunu unutmayın. Farklı sabit disklere sahip 3 farklı bilgisayarda test ettim ve zaman zaman büyük farklılıklar yaşadım. PC1 (1 saat 30m), PC2 (6 saat) PC3 (14 saat), bu yüzden neden böyle olacağını merak etmeye başladım.

İki haftalık araştırma ve birden fazla kaynağı kontrol ettikten sonra: Sabit Sürücü, Ram, Önbellek, sabit sürücünüzdeki bazı ayarların G / Ç hızını etkileyebileceğini öğrendim. İstediğiniz çıkış sürücüsündeki özelliklere tıklayarak genel sekmesinde iki seçenek görebilirsiniz. Opt1: Bu sürücüyü sıkıştırın, Opt2: Bu sürücünün dosyalarının dizine eklenmesine izin verin.

Bu iki seçeneği devre dışı bırakarak, tüm 3 bilgisayarın tamamlanması yaklaşık olarak aynı zamanı alır (1 saat 20 dakika 40 dakika). Yavaş eklerle karşılaşırsanız, sabit sürücünüzün bu seçeneklerle yapılandırılıp yapılandırılmadığını kontrol edin. Çözümü bulmaya çalışırken size çok fazla zaman ve baş ağrısı kazandıracak


Aşağıdakileri önereceğim. * İşlem gerçekleştirilmeden önce dizenin değiştirilmemesini sağlamak için bir dize kopyasını önlemek için SQLITE_STATIC ve SQLITE_TRANSIENT kullanın * Stop_times VALUES (NULL,?,?,?,?,?,?,?,? ,?), (NULL,?,?,?,?,?,?,?,?,?,?), (NULL,?,?,?,?,?,?,?,?,?), (NULL ,?,?,?,?,?,?,?,?,?), (NULL,?,?,?,?,?,?,?,?,?) * Mmap sayısını azaltmak için sistem çağrıları.
rouzier

Bunu yaparak 11.51 saniyede 5.582.642 kayıt alabilirim
rouzier


-1

Toplu verileri db'ye eklemek için ContentProvider'ı kullanın. Veritabanına toplu veri eklemek için kullanılan aşağıdaki yöntem. Bu, SQLite'nin saniyede INSERT performansını geliştirmelidir.

private SQLiteDatabase database;
database = dbHelper.getWritableDatabase();

public int bulkInsert(@NonNull Uri uri, @NonNull ContentValues[] values) {

database.beginTransaction();

for (ContentValues value : values)
 db.insert("TABLE_NAME", null, value);

database.setTransactionSuccessful();
database.endTransaction();

}

BulkInsert yöntemini çağırın:

App.getAppContext().getContentResolver().bulkInsert(contentUriTable,
            contentValuesArray);

Bağlantı: https://www.vogella.com/tutorials/AndroidSQLite/article.html Daha fazla bilgi için ContentProvider Bölümünü Kullanma konusuna bakın

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.