Meningkatkan kinerja SQLITE INSERT-per-second

2975

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_v2untuk 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_bindingsdan 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 whileloop. 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 SELECTkinerja, 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.

Mike Willekes
sumber
8
Poin bagus! Dalam kasus kami, kami menangani sekitar 1,5 juta pasangan kunci / nilai yang dibaca dari file teks XML dan CSV menjadi 200 ribu catatan. Kecil jika dibandingkan dengan basis data yang menjalankan situs seperti SO - tetapi cukup besar sehingga menyetel kinerja SQLite menjadi penting.
Mike Willekes
51
"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." mengapa Anda tidak menyimpan semuanya dalam database sqlite di tempat pertama, alih-alih menyimpan dalam XML dan kemudian memuat semuanya pada waktu inisialisasi?
CAFxX
14
Sudahkah Anda mencoba tidak menelepon 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.
ahcox
21
Apakah Anda melakukan pengukuran berulang? 4s "win" untuk menghindari 7 pointer lokal adalah aneh, bahkan dengan asumsi pengoptimal yang bingung.
peterchen
5
Jangan gunakan feof()untuk mengontrol penghentian loop input Anda. Gunakan hasil yang dikembalikan oleh fgets(). stackoverflow.com/a/15485689/827263
Keith Thompson

Jawaban:

785

Beberapa tips:

  1. Masukkan sisipan / pembaruan dalam transaksi.
  2. Untuk versi SQLite yang lebih lama - Pertimbangkan mode jurnal yang kurang paranoid ( pragma journal_mode). Ada NORMAL, dan kemudian ada OFF, yang secara signifikan dapat meningkatkan kecepatan memasukkan jika Anda tidak terlalu khawatir tentang database yang mungkin rusak jika OS lumpuh. Jika aplikasi Anda macet, data harusnya baik-baik saja. Perhatikan bahwa dalam versi yang lebih baru, OFF/MEMORYpengaturan tidak aman untuk tingkat aplikasi crash.
  3. Bermain dengan ukuran halaman juga membuat perbedaan ( PRAGMA page_size). Memiliki ukuran halaman yang lebih besar dapat membuat membaca dan menulis berjalan sedikit lebih cepat karena halaman yang lebih besar disimpan dalam memori. Perhatikan bahwa lebih banyak memori akan digunakan untuk basis data Anda.
  4. Jika Anda memiliki indeks, pertimbangkan menelepon CREATE INDEXsetelah melakukan semua sisipan Anda. Ini secara signifikan lebih cepat daripada membuat indeks dan kemudian melakukan sisipan Anda.
  5. Anda harus sangat berhati-hati jika memiliki akses bersamaan ke SQLite, karena seluruh basis data dikunci saat penulisan selesai, dan meskipun banyak pembaca dimungkinkan, penulisan akan dikunci. Ini telah sedikit meningkat dengan penambahan WAL di versi SQLite yang lebih baru.
  6. Manfaatkan penghematan ruang ... basis data yang lebih kecil lebih cepat. Misalnya, jika Anda memiliki pasangan nilai kunci, coba buat kunci tersebut INTEGER PRIMARY KEYjika mungkin, yang akan menggantikan kolom nomor baris unik yang tersirat dalam tabel.
  7. Jika Anda menggunakan beberapa utas, Anda dapat mencoba menggunakan cache halaman yang dibagikan , yang akan memungkinkan halaman yang dimuat dibagi di antara utas, yang dapat menghindari panggilan I / O yang mahal.
  8. Jangan gunakan !feof(file)!

Saya juga mengajukan pertanyaan serupa di sini dan di sini .

Snazzer
sumber
9
Documents tidak mengenal PRAGMA
OneWorld
4
Sudah lama, saran saya diterapkan untuk versi yang lebih lama sebelum WAL diperkenalkan. Sepertinya DELETE adalah pengaturan normal baru, dan sekarang juga ada pengaturan OFF dan MEMORY. Saya kira OFF / MEMORY akan meningkatkan kinerja penulisan dengan mengorbankan integritas basis data, dan OFF sepenuhnya menonaktifkan rollback.
Snazzer
4
untuk # 7, apakah Anda memiliki contoh tentang cara mengaktifkan cache halaman bersama menggunakan pembungkus c # system.data.sqlite?
Aaron Hudon
4
# 4 membawa kembali kenangan lama - Setidaknya ada satu kasus di masa lalu di mana menjatuhkan indeks sebelum sekelompok menambahkan dan menciptakan kembali setelah itu mempercepat memasukkan secara signifikan. Mungkin masih bekerja lebih cepat pada sistem modern untuk beberapa penambahan di mana Anda tahu Anda memiliki akses tunggal ke tabel untuk periode tersebut.
Bill K
Acungan jempol untuk # 1: Saya sudah sangat beruntung dengan transaksi sendiri.
Enno
146

Coba gunakan SQLITE_STATICalih-alih SQLITE_TRANSIENTuntuk sisipan tersebut.

SQLITE_TRANSIENT akan menyebabkan SQLite menyalin data string sebelum kembali.

SQLITE_STATICmemberitahu itu bahwa alamat memori yang Anda berikan akan valid sampai permintaan dilakukan (yang dalam loop ini selalu demikian). Ini akan menghemat beberapa operasi alokasi, salin, dan alokasikan semua per loop. Mungkin perbaikan besar.

Alexander Farber
sumber
109

Hindari sqlite3_clear_bindings(stmt).

Kode dalam tes ini mengatur binding setiap kali harus cukup.

The C API intro dari docs SQLite mengatakan:

Sebelum memanggil sqlite3_step () untuk pertama kalinya atau segera setelah sqlite3_reset () , aplikasi dapat memanggil antarmuka sqlite3_bind () untuk melampirkan nilai ke parameter. Setiap panggilan ke sqlite3_bind () mengabaikan bindings sebelumnya pada parameter yang sama

Tidak ada dalam dokumen untuk sqlite3_clear_bindingsmengatakan Anda harus memanggilnya selain hanya mengatur binding.

Lebih detail: Avoid_sqlite3_clear_bindings ()

ahcox
sumber
5
Benar sekali: "Bertentangan dengan intuisi banyak, sqlite3_reset () tidak mengatur ulang binding pada pernyataan yang disiapkan. Gunakan rutin ini untuk mengatur ulang semua parameter host ke NULL." - sqlite.org/c3ref/clear_bindings.html
Francis Straccia
63

Pada sisipan massal

Terinspirasi oleh posting ini dan oleh pertanyaan Stack Overflow yang membawa saya ke sini - Apakah mungkin untuk memasukkan beberapa baris sekaligus dalam database SQLite? - Saya telah memposting repositori Git pertama saya :

https://github.com/rdpoor/CreateOrUpdate

yang memuat banyak array ActiveRecords ke dalam database MySQL , SQLite atau PostgreSQL . Ini termasuk opsi untuk mengabaikan catatan yang ada, menimpa mereka atau meningkatkan kesalahan. Tolok ukur dasar saya menunjukkan peningkatan kecepatan 10x dibandingkan dengan penulisan berurutan - YMMV.

Saya menggunakannya dalam kode produksi di mana saya sering perlu mengimpor dataset besar, dan saya cukup senang dengannya.

fearless_fool
sumber
4
@ Jess: Jika Anda mengikuti tautan, Anda akan melihat bahwa maksudnya adalah sintaks penyisipan batch.
Alix Axel
48

Impor massal tampaknya berkinerja terbaik jika Anda dapat memotong pernyataan INSERT / UPDATE Anda . Nilai 10.000 atau lebih telah bekerja dengan baik untuk saya di atas meja dengan hanya beberapa baris, YMMV ...

Leon
sumber
22
Anda ingin menyetel x = 10.000 sehingga x = cache [= cache_size * page_size] / ukuran rata-rata dari insert Anda.
Alix Axel
43

Jika Anda hanya peduli tentang membaca, versi yang agak lebih cepat (tetapi mungkin membaca data basi) adalah membaca dari beberapa koneksi dari beberapa utas (koneksi per-utas).

Pertama-tama temukan item, dalam tabel:

SELECT COUNT(*) FROM table

lalu baca di halaman (LIMIT / OFFSET):

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

di mana dan dihitung per-utas, seperti ini:

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

untuk setiap utas:

int offset = thread_index * limit

Untuk db (200mb) kecil kami, ini membuat kecepatan 50-75% (3.8.0.2 64-bit pada Windows 7). Tabel kami sangat non-normal (1000-1500 kolom, sekitar 100.000 atau lebih baris).

Terlalu banyak atau terlalu sedikit utas tidak akan melakukannya, Anda perlu membuat tolok ukur dan profil sendiri.

Juga bagi kami, SHAREDCACHE membuat kinerja lebih lambat, jadi saya menempatkan PRIVATECACHE secara manual (karena itu diaktifkan secara global untuk kami)

malkia
sumber
29

Saya tidak bisa mendapatkan keuntungan dari transaksi sampai saya menaikkan cache_size ke nilai yang lebih tinggi yaitu PRAGMA cache_size=10000;

anefeletos
sumber
Perhatikan bahwa menggunakan nilai positif untuk cache_sizemenetapkan jumlah halaman yang akan di-cache , bukan ukuran total RAM. Dengan ukuran halaman default 4kB, pengaturan ini akan menampung hingga 40MB data per file terbuka (atau per proses, jika berjalan dengan cache bersama ).
Groo
21

Setelah membaca tutorial ini, saya mencoba mengimplementasikannya ke program saya.

Saya punya 4-5 file yang berisi alamat. Setiap file memiliki sekitar 30 juta catatan. Saya menggunakan konfigurasi yang sama dengan yang Anda sarankan, tetapi jumlah INSERT saya per detiknya sangat rendah (~ 10.000 catatan per detik).

Di sinilah saran Anda gagal. Anda menggunakan satu transaksi untuk semua catatan dan satu sisipan tanpa kesalahan / gagal. Katakanlah Anda membagi setiap rekaman menjadi beberapa sisipan pada tabel yang berbeda. Apa yang terjadi jika catatan rusak?

Perintah ON CONFLICT tidak berlaku, karena jika Anda memiliki 10 elemen dalam catatan dan Anda perlu setiap elemen dimasukkan ke tabel yang berbeda, jika elemen 5 mendapatkan kesalahan CONSTRAINT, maka semua 4 sisipan sebelumnya harus pergi juga.

Jadi di sinilah kemunduran datang. Satu-satunya masalah dengan rollback adalah Anda kehilangan semua sisipan dan mulai dari atas. Bagaimana Anda bisa menyelesaikan ini?

Solusi saya adalah menggunakan beberapa transaksi. Saya memulai dan mengakhiri transaksi setiap 10.000 catatan (Jangan tanya mengapa angka itu, itu yang tercepat yang saya uji). Saya membuat array berukuran 10.000 dan menyisipkan catatan yang berhasil di sana. Ketika kesalahan terjadi, saya melakukan rollback, memulai transaksi, memasukkan catatan dari array saya, melakukan dan kemudian memulai transaksi baru setelah catatan rusak.

Solusi ini membantu saya melewati masalah yang saya miliki ketika berhadapan dengan file yang berisi catatan buruk / duplikat (saya memiliki hampir 4% catatan buruk).

Algoritma yang saya buat membantu saya mengurangi proses saya hingga 2 jam. Proses pemuatan akhir dari file 1 jam 30 m yang masih lambat tetapi tidak dibandingkan dengan 4 jam yang awalnya diperlukan. Saya berhasil mempercepat insert dari 10.000 / s menjadi ~ 14.000 / s

Jika ada yang punya ide lain tentang cara mempercepatnya, saya terbuka untuk saran.

PEMBARUAN :

Selain jawaban saya di atas, Anda harus ingat bahwa memasukkan per detik tergantung pada hard drive yang Anda gunakan juga. Saya mengujinya pada 3 PC yang berbeda dengan hard drive yang berbeda dan mendapat perbedaan besar dalam waktu. PC1 (1 jam 30m), PC2 (6 jam) PC3 (14 jam), jadi saya mulai bertanya-tanya mengapa itu terjadi.

Setelah dua minggu meneliti dan memeriksa berbagai sumber daya: Hard Drive, Ram, Cache, saya mengetahui bahwa beberapa pengaturan pada hard drive Anda dapat memengaruhi tingkat I / O. Dengan mengklik properti pada drive output yang Anda inginkan, Anda dapat melihat dua opsi di tab umum. Opt1: Kompres drive ini, Opt2: Izinkan file drive ini memiliki konten yang diindeks.

Dengan menonaktifkan kedua opsi ini, ketiga PC sekarang membutuhkan waktu yang hampir bersamaan untuk menyelesaikan (1 jam dan 20 hingga 40 menit). Jika Anda menemukan sisipan lambat, periksa apakah hard drive Anda dikonfigurasi dengan opsi ini. Ini akan menghemat banyak waktu dan sakit kepala saat mencoba menemukan solusinya

Jimmy_A
sumber
Saya akan menyarankan yang berikut ini. * Gunakan SQLITE_STATIC vs SQLITE_TRANSIENT untuk menghindari salinan string, Anda harus memastikan string tidak akan berubah sebelum transaksi dijalankan * Gunakan penyisipan massal Sisipkan ke stop_times NILAI (NULL,?,?,?,?,?,?,?,?,?,? ,?), (NULL,?,?,?,?,?,?,?,?,?), (NULL,?,?,?,?,?,?,?,?,?,?,?), (NULL ,?,?,?,?,?,?,?,?,?,?), (NULL,?,?,?,?,?,?,?,?,?, * Mmap file untuk mengurangi jumlah syscalls.
rouzier
Dengan melakukan itu saya dapat mengimpor 5.582.642 catatan dalam 11,51 detik
rouzier
-1

Gunakan ContentProvider untuk memasukkan data massal dalam db. Metode di bawah ini digunakan untuk memasukkan data massal ke dalam basis data. Ini harus meningkatkan kinerja SQLite INSERT-per-detik.

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();

}

Hubungi metode bulkInsert:

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

Tautan: https://www.vogella.com/tutorials/AndroidSQLite/article.html periksa Menggunakan Bagian ContentProvider untuk lebih jelasnya

vishnuc156
sumber