ปรับปรุงประสิทธิภาพ INSERT ต่อวินาทีของ SQLite


2975

การเพิ่มประสิทธิภาพของ SQLite นั้นยุ่งยาก ประสิทธิภาพการแทรกจำนวนมากของแอพพลิเคชั่น C สามารถเปลี่ยนจาก 85 เม็ดต่อวินาทีไปเป็นมากกว่า 96,000 เม็ดต่อวินาที!

พื้นหลัง:เรากำลังใช้ SQLite เป็นส่วนหนึ่งของแอปพลิเคชันเดสก์ท็อป เรามีข้อมูลการกำหนดค่าจำนวนมากที่เก็บไว้ในไฟล์ XML ที่มีการแยกวิเคราะห์และโหลดลงในฐานข้อมูล SQLite เพื่อการประมวลผลเพิ่มเติมเมื่อเริ่มต้นแอปพลิเคชัน SQLite นั้นเหมาะสำหรับสถานการณ์นี้เพราะมันรวดเร็วไม่ต้องมีการกำหนดค่าพิเศษและฐานข้อมูลจะถูกเก็บไว้ในดิสก์เป็นไฟล์เดียว

เหตุผล: ตอนแรกฉันรู้สึกผิดหวังกับการแสดงที่ฉันได้เห็น ปรากฎว่าประสิทธิภาพของ SQLite อาจแตกต่างกันอย่างมีนัยสำคัญ (ทั้งสำหรับการแทรกจำนวนมากและการเลือก) ขึ้นอยู่กับวิธีการกำหนดค่าฐานข้อมูลและวิธีการที่คุณใช้ API ไม่ใช่เรื่องง่ายที่จะคิดออกว่าตัวเลือกและเทคนิคทั้งหมดเป็นอย่างไรดังนั้นฉันจึงคิดว่าควรสร้างรายการวิกิชุมชนนี้เพื่อแบ่งปันผลลัพธ์กับผู้อ่าน Stack Overflow เพื่อช่วยคนอื่น ๆ ในการตรวจสอบปัญหาเดียวกัน

การทดลอง:แทนที่จะพูดถึงเคล็ดลับประสิทธิภาพในแง่ทั่วไป (เช่น"ใช้ธุรกรรม!" ) ฉันคิดว่ามันดีที่สุดในการเขียนรหัส C และวัดผลกระทบของตัวเลือกต่างๆ เราจะเริ่มด้วยข้อมูลง่ายๆ:

  • ไฟล์ข้อความที่คั่นด้วยแท็บ TAB ขนาด 28 MB (ประมาณ 865,000 บันทึก) ของตารางการขนส่งที่สมบูรณ์สำหรับเมืองโตรอนโต
  • เครื่องทดสอบของฉันคือ 3.60 GHz P4 ที่ใช้ Windows XP
  • รหัสถูกคอมไพล์ด้วยVisual C ++ 2005 เป็น "Release" กับ "การเพิ่มประสิทธิภาพแบบเต็ม" (/ Ox) และ Favour Fast Code (/ Ot)
  • ฉันใช้ SQLite "การรวมกัน" รวบรวมโดยตรงในแอปพลิเคชันทดสอบของฉัน เวอร์ชัน SQLite ที่ฉันเกิดขึ้นนั้นค่อนข้างเก่า (3.6.7) แต่ฉันคิดว่าผลลัพธ์เหล่านี้จะเทียบได้กับรีลีสล่าสุด (โปรดแสดงความคิดเห็นหากคุณคิดเป็นอย่างอื่น)

ลองเขียนโค้ดกัน!

รหัส:โปรแกรม C แบบง่ายที่อ่านไฟล์ข้อความแบบทีละบรรทัดแยกสตริงออกเป็นค่าแล้วแทรกข้อมูลลงในฐานข้อมูล SQLite ในรหัส "รุ่นพื้นฐาน" นี้ฐานข้อมูลจะถูกสร้างขึ้น แต่เราจะไม่แทรกข้อมูล:

/*************************************************************
    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;
}

การควบคุม"

การรันโค้ดตาม - ไม่ได้ดำเนินการกับฐานข้อมูลใด ๆ จริง ๆ แต่มันจะทำให้เรามีความคิดว่า I / O ไฟล์ C ดิบและการดำเนินการประมวลผลสตริงรวดเร็วแค่ไหน

นำเข้า 864913 บันทึกใน 0.94 วินาที

ที่ดี! เราสามารถทำเม็ดมีดได้ 920,000 เม็ดต่อวินาทีโดยที่เราไม่ได้ทำการแทรก :-)


"สถานการณ์เลวร้ายที่สุด"

เราจะสร้างสตริง SQL โดยใช้ค่าที่อ่านจากไฟล์และเรียกใช้การดำเนินการ SQL โดยใช้ 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);

สิ่งนี้กำลังจะช้าเพราะ SQL จะถูกรวบรวมเป็นรหัส VDBE สำหรับทุกส่วนแทรกและส่วนแทรกทุกอันจะเกิดขึ้นในการทำธุรกรรมของตัวเอง ช้าแค่ไหน?

นำเข้า 864913 บันทึกใน 9933.61 วินาที

อ๊ะ! 2 ชั่วโมงและ 45 นาที! นั่นคือเม็ดมีดเพียง85 เม็ดต่อวินาที

ใช้การทำธุรกรรม

โดยค่าเริ่มต้น SQLite จะประเมินทุกคำสั่ง INSERT / UPDATE ภายในธุรกรรมที่ไม่ซ้ำกัน หากทำการแทรกจำนวนมากแนะนำให้ห่อการทำงานของคุณในการทำธุรกรรม:

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

นำเข้า 864913 บันทึกใน 38.03 วินาที

มันดีกว่า. เพียงแค่ตัดเม็ดมีดทั้งหมดของเราในการทำรายการเดียวปรับปรุงประสิทธิภาพของเราเป็น23,000 เม็ดต่อวินาที

ใช้งบเตรียม

การใช้ทรานแซคชันเป็นการปรับปรุงครั้งใหญ่ แต่การคอมไพล์คำสั่ง SQL ใหม่สำหรับการแทรกทุกครั้งนั้นไม่สมเหตุสมผลถ้าเราใช้ SQL แบบเดิมซ้ำไปซ้ำมา ลองใช้sqlite3_prepare_v2เพื่อรวบรวมคำสั่ง SQL ของเราหนึ่งครั้งแล้วผูกพารามิเตอร์ของเรากับคำสั่งนั้นโดยใช้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;

นำเข้า 864913 บันทึกใน 16.27 วินาที

ดี! มีโค้ดอีกเล็กน้อย (อย่าลืมโทรsqlite3_clear_bindingsและsqlite3_reset) แต่เราเพิ่มประสิทธิภาพของเราให้เป็นสองเท่าเป็น53,000 เม็ดต่อวินาที

PRAGMA ซิงโครนัส = OFF

โดยค่าเริ่มต้น SQLite จะหยุดชั่วคราวหลังจากออกคำสั่งการเขียนระดับระบบปฏิบัติการ สิ่งนี้รับประกันว่าข้อมูลถูกเขียนลงดิสก์ ด้วยการตั้งค่าsynchronous = OFFเรากำลังสั่งให้ SQLite ส่งข้อมูลไปยังระบบปฏิบัติการเพื่อเขียนและดำเนินการต่อ มีโอกาสที่ไฟล์ฐานข้อมูลอาจเสียหายหากคอมพิวเตอร์ประสบกับความเสียหายรุนแรง (หรือไฟฟ้าขัดข้อง) ก่อนที่ข้อมูลจะถูกเขียนลงบนแผ่นเสียง:

/* 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);

นำเข้า 864913 บันทึกใน 12.41 วินาที

ตอนนี้การปรับปรุงเล็กลง แต่เราเพิ่มเม็ดมีดได้มากถึง69,600 ต่อวินาที

PRAGMA journal_mode = MEMORY

PRAGMA journal_mode = MEMORYพิจารณาจัดเก็บวารสารย้อนกลับในหน่วยความจำโดยการประเมิน ธุรกรรมของคุณจะเร็วขึ้น แต่ถ้าคุณสูญเสียพลังงานหรือโปรแกรมของคุณล้มเหลวในระหว่างการทำธุรกรรมฐานข้อมูลของคุณคุณอาจจะอยู่ในสถานะที่เสียหายด้วยการทำธุรกรรมบางส่วนเสร็จสมบูรณ์:

/* 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);

นำเข้า 864913 บันทึกใน 13.50 วินาที

ช้ากว่าการปรับให้เหมาะสมก่อนหน้าเล็กน้อยที่64,000 เม็ดต่อวินาที

PRAGMA ซิงโครนัส = OFF และ PRAGMA journal_mode = MEMORY

มารวมการเพิ่มประสิทธิภาพสองรายการก่อนหน้าเข้าด้วยกัน มันมีความเสี่ยงมากขึ้นเล็กน้อย (ในกรณีที่เกิดความผิดพลาด) แต่เราเพิ่งนำเข้าข้อมูล (ไม่ใช่ธนาคาร):

/* 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);

นำเข้า 864913 บันทึกใน 12.00 วินาที

Fantastic! เราสามารถทำเม็ดมีด 72,000 เม็ดต่อวินาที

การใช้ฐานข้อมูลในหน่วยความจำ

เพื่อความสนุกลองสร้างการเพิ่มประสิทธิภาพก่อนหน้านี้ทั้งหมดและกำหนดชื่อไฟล์ฐานข้อมูลอีกครั้งเพื่อให้เราทำงานใน RAM ทั้งหมด:

#define DATABASE ":memory:"

นำเข้า 864913 บันทึกใน 10.94 วินาที

การเก็บฐานข้อมูลของเราใน RAM เป็นเรื่องที่ไม่เป็นประโยชน์ แต่มันน่าประทับใจที่เราสามารถทำการแทรกได้ 79,000 ต่อวินาที

Refactoring C Code

แม้ว่าจะไม่ใช่การปรับปรุง SQLite โดยเฉพาะ แต่ฉันไม่ชอบการchar*ดำเนินการมอบหมายพิเศษในwhileลูป ให้เราปรับโครงสร้างโค้ดนั้นอย่างรวดเร็วเพื่อส่งผลลัพธ์ออกstrtok()ไปยังโดยตรงsqlite3_bind_text()และให้คอมไพเลอร์พยายามเร่งความเร็วให้กับเรา:

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

หมายเหตุ: เรากลับไปใช้ไฟล์ฐานข้อมูลจริง ฐานข้อมูลในหน่วยความจำรวดเร็ว แต่ไม่จำเป็นต้องใช้งานจริง

นำเข้า 864913 บันทึกใน 8.94 วินาที

การปรับเปลี่ยนเล็กน้อยสำหรับโค้ดการประมวลผลสตริงที่ใช้ในการเชื่อมพารามิเตอร์ของเราทำให้เราสามารถทำการแทรกได้ 96,700 ครั้งต่อวินาที ฉันคิดว่ามันปลอดภัยที่จะบอกว่านี่เร็วมาก เมื่อเราเริ่มปรับแต่งตัวแปรอื่น ๆ (เช่นขนาดหน้าการสร้างดัชนีเป็นต้น) สิ่งนี้จะเป็นเกณฑ์มาตรฐานของเรา


สรุป (จนถึงปัจจุบัน)

ฉันหวังว่าคุณจะยังอยู่กับฉัน! เหตุผลที่เราเริ่มต้นบนถนนสายนี้คือประสิทธิภาพการแทรกจำนวนมากแตกต่างกันอย่างมากกับ SQLite และไม่ชัดเจนเสมอไปว่าการเปลี่ยนแปลงใดที่ต้องดำเนินการเพื่อเร่งการดำเนินงานของเราให้เร็วขึ้น การใช้คอมไพเลอร์ตัวเดียวกัน (และตัวเลือกคอมไพเลอร์), เวอร์ชั่นเดียวกันของ SQLite และข้อมูลเดียวกันกับที่เราปรับปรุงโค้ดของเราและการใช้งาน SQLite ของเราเพื่อเปลี่ยนจากสถานการณ์ที่เลวร้ายที่สุดที่ 85 เม็ดต่อวินาทีเป็น 96,000 เม็ดต่อวินาที!


สร้าง INDEX จากนั้นใส่ INSERT และ INSERT จากนั้นสร้าง INDEX

ก่อนที่เราจะเริ่มวัดSELECTประสิทธิภาพเรารู้ว่าเราจะสร้างดัชนี คำแนะนำในหนึ่งในคำตอบด้านล่างนี้เมื่อทำการแทรกจำนวนมากจะเร็วกว่าในการสร้างดัชนีหลังจากที่ข้อมูลถูกแทรก (ซึ่งต่างจากการสร้างดัชนีก่อนจากนั้นจึงแทรกข้อมูล) มาลองกัน:

สร้างดัชนีแล้วใส่ข้อมูล

sqlite3_exec(db, "CREATE  INDEX 'TTC_Stop_Index' ON 'TTC' ('Stop')", NULL, NULL, &sErrMsg);
sqlite3_exec(db, "BEGIN TRANSACTION", NULL, NULL, &sErrMsg);
...

นำเข้า 864913 บันทึกใน 18.13 วินาที

แทรกข้อมูลจากนั้นสร้างดัชนี

...
sqlite3_exec(db, "END TRANSACTION", NULL, NULL, &sErrMsg);
sqlite3_exec(db, "CREATE  INDEX 'TTC_Stop_Index' ON 'TTC' ('Stop')", NULL, NULL, &sErrMsg);

นำเข้า 864913 บันทึกใน 13.66 วินาที

ตามที่คาดไว้การแทรกจำนวนมากจะช้ากว่าหากมีการจัดทำดัชนีหนึ่งคอลัมน์ แต่จะสร้างความแตกต่างได้หากสร้างดัชนีหลังจากแทรกข้อมูลแล้ว พื้นฐานที่ไม่มีดัชนีของเราคือ 96,000 เม็ดต่อวินาที การสร้างดัชนีก่อนจากนั้นการแทรกข้อมูลจะให้ 47,700 เม็ดต่อวินาทีในขณะที่การแทรกข้อมูลก่อนจากนั้นการสร้างดัชนีจะให้เม็ดมีด 63,300 เม็ดต่อวินาที


ฉันยินดีรับข้อเสนอแนะสำหรับสถานการณ์อื่น ๆ ที่จะลอง ... และจะรวบรวมข้อมูลที่คล้ายกันสำหรับคำค้นหา SELECT ในไม่ช้า


8
จุดดี! ในกรณีของเราเรากำลังเผชิญกับคู่คีย์ / ค่าประมาณ 1.5 ล้านคู่ที่อ่านจากไฟล์ข้อความ XML และ CSV เป็นจำนวน 200k เล็กเมื่อเปรียบเทียบกับฐานข้อมูลที่เรียกใช้ไซต์เช่นนั้น - แต่ใหญ่พอที่การปรับประสิทธิภาพของ SQLite จะมีความสำคัญ
Mike Willekes

51
"เรามีข้อมูลการกำหนดค่าจำนวนมากที่เก็บไว้ในไฟล์ XML ที่ถูกวิเคราะห์และโหลดลงในฐานข้อมูล SQLite เพื่อการประมวลผลเพิ่มเติมเมื่อเริ่มต้นแอปพลิเคชัน" ทำไมคุณไม่เก็บทุกอย่างไว้ในฐานข้อมูล sqlite ตั้งแต่แรกแทนที่จะเก็บไว้ใน XML แล้วโหลดทุกอย่างในเวลาเริ่มต้น
CAFxX

14
คุณเคยลองโทรแล้วsqlite3_clear_bindings(stmt);หรือยัง? คุณตั้งค่าการรวมทุกครั้งซึ่งควรจะเพียงพอ: ก่อนที่จะเรียก sqlite3_step () เป็นครั้งแรกหรือทันทีหลังจาก sqlite3_reset () แอปพลิเคชันสามารถเรียกใช้หนึ่งในอินเตอร์เฟส sqlite3_bind () เพื่อแนบค่ากับพารามิเตอร์ การเรียกไปที่ sqlite3_bind () แต่ละครั้งจะแทนที่การโยงก่อนหน้านี้ในพารามิเตอร์เดียวกัน (ดู: sqlite.org/cintro.html ) ไม่มีสิ่งใดในเอกสารสำหรับฟังก์ชั่นที่บอกว่าคุณต้องเรียกมันว่า
ahcox

21
คุณทำการวัดซ้ำแล้วซ้ำอีกหรือไม่? 4s "ชนะ" สำหรับการหลีกเลี่ยง 7 พอยน์เตอร์พอยน์เตอร์นั้นเป็นเรื่องแปลกแม้จะใช้เครื่องมือเพิ่มประสิทธิภาพที่สับสนก็ตาม
peterchen

5
อย่าใช้feof()เพื่อควบคุมการยกเลิกลูปอินพุทของคุณ fgets()ใช้ผลที่ส่งกลับโดย stackoverflow.com/a/15485689/827263
Keith Thompson

คำตอบ:


785

เคล็ดลับหลายประการ:

  1. ใส่ส่วนแทรก / อัพเดตในธุรกรรม
  2. สำหรับ SQLite เวอร์ชันเก่า - พิจารณาโหมดเจอร์นัลหวาดระแวงน้อยกว่า ( pragma journal_mode) มีNORMALแล้วมีOFFซึ่งสามารถเพิ่มความเร็วในการแทรกอย่างมีนัยสำคัญถ้าคุณไม่กังวลเกินไปเกี่ยวกับฐานข้อมูลที่อาจได้รับความเสียหายหากระบบปฏิบัติการล่ม หากแอปพลิเคชันของคุณขัดข้องข้อมูลควรใช้ได้ โปรดทราบว่าในรุ่นที่ใหม่กว่าการOFF/MEMORYตั้งค่าไม่ปลอดภัยสำหรับการล่มในระดับแอปพลิเคชัน
  3. เล่นกับขนาดหน้าสร้างความแตกต่างเช่นกัน ( PRAGMA page_size) การมีขนาดหน้ากระดาษที่ใหญ่กว่าสามารถทำให้อ่านและเขียนได้เร็วขึ้นเนื่องจากเพจที่มีขนาดใหญ่ขึ้นจะถูกเก็บไว้ในหน่วยความจำ โปรดทราบว่าหน่วยความจำเพิ่มเติมจะถูกใช้สำหรับฐานข้อมูลของคุณ
  4. หากคุณมีดัชนีให้ลองโทรCREATE INDEXหลังจากทำการแทรกทั้งหมดของคุณ สิ่งนี้เร็วกว่าการสร้างดัชนีอย่างมากจากนั้นจึงทำการแทรกของคุณ
  5. คุณต้องระมัดระวังค่อนข้างมากถ้าคุณมีการเข้าถึง SQLite พร้อมกันเนื่องจากฐานข้อมูลทั้งหมดถูกล็อคเมื่อการเขียนเสร็จสิ้นและแม้ว่าผู้อ่านหลายคนจะเป็นไปได้การเขียนจะถูกล็อค สิ่งนี้ได้รับการปรับปรุงบ้างด้วยการเพิ่ม WAL ใน SQLite เวอร์ชันที่ใหม่กว่า
  6. ใช้ประโยชน์จากการประหยัดพื้นที่ ... ฐานข้อมูลขนาดเล็กลงได้เร็วขึ้น ตัวอย่างเช่นหากคุณมีคู่ของค่าคีย์ให้ลองทำกุญแจให้เป็นINTEGER PRIMARY KEYไปได้ซึ่งจะแทนที่คอลัมน์หมายเลขแถวที่ไม่ซ้ำโดยนัยในตาราง
  7. หากคุณใช้หลายเธรดคุณสามารถลองใช้แคชเพจที่ใช้ร่วมกันซึ่งจะอนุญาตให้เพจที่โหลดโหลดร่วมกันระหว่างเธรดซึ่งสามารถหลีกเลี่ยงการโทร I / O ราคาแพงได้
  8. อย่าใช้!feof(file)!

ฉันยังเคยถามคำถามที่คล้ายกันที่นี่และที่นี่


9
เอกสารไม่รู้จัก PRAGMA journal_mode NORMAL sqlite.org/pragma.html#pragma_journal_mode
OneWorld

4
ไม่นานมานี้คำแนะนำของฉันถูกนำไปใช้กับเวอร์ชั่นที่เก่ากว่าก่อนที่จะมีการเปิดตัว WAL ดูเหมือนว่า DELETE เป็นการตั้งค่าปกติใหม่และตอนนี้ก็มีการตั้งค่าปิดและ MEMORY เช่นกัน ฉันสมมติว่า OFF / MEMORY จะปรับปรุงประสิทธิภาพการเขียนด้วยค่าใช้จ่ายของความสมบูรณ์ของฐานข้อมูลและ OFF จะปิดใช้งานการย้อนกลับอย่างสมบูรณ์
Snazzer

4
สำหรับ # 7 คุณมีตัวอย่างเกี่ยวกับวิธีเปิดใช้งานแคชหน้าที่แชร์โดยใช้ c # system.data.sqlite wrapper หรือไม่?
Aaron Hudon

4
# 4 นำความทรงจำเก่า ๆ กลับมาทุกวัย - มีอย่างน้อยหนึ่งกรณีกลับมาก่อน - เวลาที่วางดัชนีก่อนที่กลุ่มของการเพิ่มและการสร้างใหม่อีกครั้งหลังจากนั้นแทรกเร่งอย่างมีนัยสำคัญ อาจยังทำงานได้เร็วขึ้นในระบบที่ทันสมัยสำหรับบางส่วนที่คุณรู้ว่าคุณสามารถเข้าถึงตารางในช่วงเวลานั้น
Bill K

ยกนิ้วให้กับ # 1: ฉันโชคดีมากกับการทำธุรกรรมด้วยตัวเอง
Enno

146

ลองใช้SQLITE_STATICแทนSQLITE_TRANSIENTส่วนแทรกเหล่านั้น

SQLITE_TRANSIENT จะทำให้ SQLite คัดลอกข้อมูลสตริงก่อนส่งคืน

SQLITE_STATICบอกว่าที่อยู่หน่วยความจำที่คุณให้ไว้จะถูกต้องจนกว่าจะมีการดำเนินการค้นหา (ซึ่งในลูปนี้จะเป็นกรณีนี้เสมอ) สิ่งนี้จะช่วยให้คุณประหยัดดำเนินการจัดสรรคัดลอกและยกเลิกการจัดสรรต่อลูป อาจเป็นการปรับปรุงครั้งใหญ่


109

sqlite3_clear_bindings(stmt)หลีกเลี่ยงการ

รหัสในการทดสอบตั้งค่าการโยงทุกครั้งซึ่งควรจะเพียงพอ

บทนำ C APIจากเอกสารข้อมูล SQLite พูดว่า:

ก่อนที่จะเรียกsqlite3_step ()เป็นครั้งแรกหรือทันทีหลังจากsqlite3_reset ()แอปพลิเคชันสามารถเรียกใช้ อินเตอร์เฟส sqlite3_bind ()เพื่อแนบค่ากับพารามิเตอร์ การเรียกใช้sqlite3_bind ()แต่ละครั้งจะแทนที่การโยงก่อนหน้านี้ในพารามิเตอร์เดียวกัน

ไม่มีสิ่งใดในเอกสารที่จะsqlite3_clear_bindingsบอกว่าคุณต้องเรียกมันเพิ่มเติมจากการตั้งค่าการผูก

รายละเอียดเพิ่มเติม: Avoid_sqlite3_clear_bindings ()


5
ขวาสุดยอด: "ตรงกันข้ามกับปรีชาของหลาย ๆ sqlite3_reset () ไม่รีเซ็ตการผูกบนคำสั่งที่เตรียมไว้ใช้รูทีนนี้เพื่อรีเซ็ตพารามิเตอร์โฮสต์ทั้งหมดเป็น NULL" - sqlite.org/c3ref/clear_bindings.html
Francis Straccia

63

บนเม็ดมีดจำนวนมาก

แรงบันดาลใจจากการโพสต์นี้และจากคำถาม Stack Overflow ที่นำฉันมาที่นี่ - เป็นไปได้หรือไม่ที่จะแทรกหลายแถวในฐานข้อมูล SQLite? - ฉันโพสต์ที่เก็บGitแรกของฉัน:

https://github.com/rdpoor/CreateOrUpdate

ซึ่งเป็นกลุ่มโหลดอาร์เรย์ของ ActiveRecords ลงMySQL , SQLite หรือPostgreSQLฐานข้อมูล มันมีตัวเลือกเพื่อละเว้นระเบียนที่มีอยู่เขียนทับพวกเขาหรือเพิ่มข้อผิดพลาด มาตรฐานพื้นฐานของฉันแสดงการปรับปรุงความเร็ว 10 เท่าเมื่อเทียบกับการเขียนตามลำดับ - YMMV

ฉันใช้มันในรหัสการผลิตซึ่งบ่อยครั้งที่ฉันต้องนำเข้าชุดข้อมูลขนาดใหญ่และฉันก็ค่อนข้างพอใจกับมัน


4
@Jess: ถ้าคุณไปตามลิงค์คุณจะเห็นว่าเขาหมายถึงไวยากรณ์แทรกแบทช์
Alix Axel

48

การนำเข้าจำนวนมากดูเหมือนจะทำงานได้ดีที่สุดหากคุณสามารถแยกงบINSERT / UPDATE ของคุณ มูลค่า 10,000 หรือมากกว่านั้นได้ผลดีสำหรับฉันบนโต๊ะที่มีเพียงไม่กี่แถว YMMV ...


22
คุณต้องการปรับค่า x = 10,000 เพื่อให้ x = cache [= cache_size * page_size] / ขนาดเฉลี่ยของการแทรกของคุณ
Alix Axel

43

หากคุณสนใจเกี่ยวกับการอ่านรุ่นที่ค่อนข้างเร็วกว่า (แต่อาจอ่านข้อมูลเก่า) คือการอ่านจากการเชื่อมต่อที่หลากหลายจากหลายเธรด (การเชื่อมต่อต่อเธรด)

ขั้นแรกให้ค้นหาสิ่งของในตาราง:

SELECT COUNT(*) FROM table

อ่านแล้วในหน้า (LIMIT / OFFSET):

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

โดยที่และคำนวณต่อเธรดดังนี้:

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

สำหรับแต่ละหัวข้อ:

int offset = thread_index * limit

สำหรับขนาดเล็ก (200mb) db ของเราทำให้ความเร็ว 50-75% (3.8.0.2 64- บิตบน Windows 7) ตารางของเรามีขนาดที่ไม่เป็นมาตรฐาน (1,000-1500 คอลัมน์, ประมาณ 100,000 แถวหรือมากกว่า)

เธรดมากเกินไปหรือน้อยเกินไปจะไม่ทำคุณต้องทำการเบนช์มาร์กและโพรไฟล์ด้วยตัวเอง

สำหรับเราแล้ว SHAREDCACHE ทำให้ประสิทธิภาพช้าลงดังนั้นฉันจึงวาง PRIVATECACHE เอง (เพราะเปิดใช้งานทั่วโลกสำหรับเรา)


29

ฉันไม่ได้รับผลกำไรใด ๆ จากการทำธุรกรรมจนกว่าฉันจะยกระดับ cache_size ให้สูงกว่าเช่น PRAGMA cache_size=10000;


โปรดทราบว่าการใช้ค่าบวกสำหรับcache_sizeตั้งค่าจำนวนหน้าที่จะแคชไม่ใช่ขนาด RAM ทั้งหมด ด้วยขนาดหน้าเริ่มต้นที่ 4kB การตั้งค่านี้จะเก็บข้อมูลได้สูงสุด 40MB ต่อไฟล์ที่เปิด (หรือต่อกระบวนการหากทำงานด้วยแคชที่ใช้ร่วมกัน )
Groo

21

หลังจากอ่านบทช่วยสอนนี้ฉันพยายามนำไปใช้กับโปรแกรมของฉัน

ฉันมีไฟล์ 4-5 ไฟล์ที่มีที่อยู่ แต่ละไฟล์มีประมาณ 30 ล้านบันทึก ฉันใช้การตั้งค่าแบบเดียวกันกับที่คุณกำลังแนะนำ แต่จำนวน INSERTs ต่อวินาทีของฉันต่ำมาก (ประมาณ 10.000 บันทึกต่อวินาที)

ที่นี่ข้อเสนอแนะของคุณล้มเหลว คุณใช้ธุรกรรมเดียวสำหรับระเบียนทั้งหมดและการแทรกครั้งเดียวโดยไม่มีข้อผิดพลาด / ล้มเหลว สมมติว่าคุณกำลังแยกแต่ละระเบียนออกเป็นหลายส่วนแทรกบนตารางที่แตกต่างกัน จะเกิดอะไรขึ้นถ้าบันทึกเสียหาย?

คำสั่ง ON CONFLICT ไม่ได้ใช้ถ้าคุณมีองค์ประกอบ 10 รายการในเรกคอร์ดและคุณต้องการแต่ละองค์ประกอบที่แทรกลงในตารางที่แตกต่างกันหากองค์ประกอบ 5 ได้รับข้อผิดพลาด CONSTRAINT ข้อผิดพลาดแทรก 4 ก่อนหน้านี้ทั้งหมดต้องไปด้วย

ดังนั้นนี่คือที่มาของการย้อนกลับ ปัญหาเดียวของการย้อนกลับคือคุณสูญเสียส่วนแทรกทั้งหมดและเริ่มจากด้านบน คุณจะแก้ปัญหานี้อย่างไร

โซลูชันของฉันคือใช้หลายธุรกรรม ฉันเริ่มต้นและสิ้นสุดการทำธุรกรรมทุก 10,000 รายการ (อย่าถามว่าทำไมหมายเลขนั้นมันเป็นรายการที่เร็วที่สุดที่ฉันทดสอบ) ฉันสร้างอาร์เรย์ที่มีขนาด 10.000 และใส่บันทึกที่ประสบความสำเร็จที่นั่น เมื่อข้อผิดพลาดเกิดขึ้นฉันจะย้อนกลับเริ่มทำธุรกรรมแทรกระเบียนจากอาร์เรย์ของฉันกระทำและจากนั้นเริ่มทำธุรกรรมใหม่หลังจากบันทึกที่ขาด

วิธีการแก้ปัญหานี้ช่วยให้ฉันหลีกเลี่ยงปัญหาที่ฉันมีเมื่อจัดการกับไฟล์ที่มีระเบียนไม่ถูกต้อง / ซ้ำกัน (ฉันมีบันทึกไม่ดีเกือบ 4%)

อัลกอริทึมที่ฉันสร้างขึ้นช่วยฉันลดขั้นตอนลง 2 ชั่วโมง กระบวนการโหลดครั้งสุดท้ายของไฟล์ 1 ชม. 30 ม. ซึ่งยังคงช้า แต่ไม่เปรียบเทียบกับ 4hrs ที่ใช้ในตอนแรก ฉันจัดการเพื่อเพิ่มความเร็วในการแทรกจาก 10.000 / s เป็น ~ 14.000 / s

หากใครมีแนวคิดอื่น ๆ เกี่ยวกับวิธีเพิ่มความเร็วฉันเปิดรับข้อเสนอแนะ

อัปเดต :

นอกเหนือจากคำตอบของฉันข้างต้นคุณควรจำไว้ว่าเม็ดมีดต่อวินาทีขึ้นอยู่กับฮาร์ดไดรฟ์ที่คุณใช้ด้วย ฉันทดสอบบนพีซี 3 เครื่องด้วยฮาร์ดไดรฟ์ที่แตกต่างกันและได้รับความแตกต่างอย่างมากในเวลา PC1 (1 ชั่วโมง 30 ม.), PC2 (6 ชั่วโมง) PC3 (14 ชั่วโมง) ดังนั้นฉันเริ่มสงสัยว่าทำไมมันถึงเป็นเช่นนั้น

หลังจากสองสัปดาห์ของการวิจัยและตรวจสอบทรัพยากรหลายอย่าง: ฮาร์ดไดรฟ์, RAM, แคชฉันพบว่าการตั้งค่าบางอย่างในฮาร์ดไดรฟ์ของคุณอาจส่งผลกระทบต่ออัตรา I / O ด้วยการคลิกคุณสมบัติบนไดรฟ์ผลลัพธ์ที่คุณต้องการคุณจะเห็นสองตัวเลือกในแท็บทั่วไป Opt1: บีบอัดไดรฟ์นี้ Opt2: อนุญาตให้ไฟล์ของไดรฟ์นี้มีการจัดทำดัชนีเนื้อหา

ด้วยการปิดการใช้งานตัวเลือกเหล่านี้ทั้งสองพีซีทั้ง 3 เครื่องใช้เวลาประมาณเดียวกันในการเสร็จสิ้น (1 ชั่วโมงและ 20 ถึง 40 นาที) หากคุณพบกับเม็ดมีดช้าให้ตรวจสอบว่าฮาร์ดไดรฟ์ของคุณได้รับการกำหนดค่าด้วยตัวเลือกเหล่านี้ มันจะช่วยคุณประหยัดเวลาและปวดหัวในการพยายามหาวิธีแก้ปัญหา


ฉันจะแนะนำต่อไปนี้ * ใช้ SQLITE_STATIC กับ SQLITE_TRANSIENT เพื่อหลีกเลี่ยงการคัดลอกสตริงคุณต้องแน่ใจว่าสตริงจะไม่ถูกเปลี่ยนแปลงก่อนที่จะดำเนินการธุรกรรม * ใช้การแทรกจำนวนมาก INSERT INTO stop_times ค่า (NULL,?,?,?,?,?,?,? ,?), (NULL,?,?,?,?,?,?,?,?), (NULL,?,?,?,?,?,?,?,?,?)? (NULL ไฟล์, mm, เพื่อลดจำนวนของไฟล์, จำนวน,,,,,,,,,,,,,,,,,,,,,,, syscalls
rouzier

การทำเช่นนั้นฉันสามารถนำเข้า 5,582,642 บันทึกใน 11.51 วินาที
rouzier

11

คำตอบสำหรับคำถามของคุณคือ SQLite 3 รุ่นใหม่มีการปรับปรุงประสิทธิภาพให้ใช้

คำตอบนี้ทำไม SQLAlchemy จึงแทรกด้วย sqlite ช้ากว่าการใช้ sqlite3 ถึง 25 เท่า โดย SqlAlchemy Orm ผู้แต่งมีเม็ดมีด 100k ใน 0.5 วินาทีและฉันได้เห็นผลลัพธ์ที่คล้ายกันกับ python-sqlite และ SqlAlchemy ซึ่งทำให้ฉันเชื่อว่าประสิทธิภาพได้รับการปรับปรุงด้วย SQLite 3


-1

ใช้ ContentProvider สำหรับการแทรกข้อมูลจำนวนมากใน db วิธีการด้านล่างใช้สำหรับการแทรกข้อมูลจำนวนมากในฐานข้อมูล สิ่งนี้จะปรับปรุงประสิทธิภาพของ INSERT ต่อวินาทีของ SQLite

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:

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

ลิงก์: https://www.vogella.com/tutorials/AndroidSQLite/article.html ตรวจสอบการใช้มาตรา ContentProvider สำหรับรายละเอียดเพิ่มเติม

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.