การเพิ่มประสิทธิภาพของ 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 ในไม่ช้า
sqlite3_clear_bindings(stmt);
หรือยัง? คุณตั้งค่าการรวมทุกครั้งซึ่งควรจะเพียงพอ: ก่อนที่จะเรียก sqlite3_step () เป็นครั้งแรกหรือทันทีหลังจาก sqlite3_reset () แอปพลิเคชันสามารถเรียกใช้หนึ่งในอินเตอร์เฟส sqlite3_bind () เพื่อแนบค่ากับพารามิเตอร์ การเรียกไปที่ sqlite3_bind () แต่ละครั้งจะแทนที่การโยงก่อนหน้านี้ในพารามิเตอร์เดียวกัน (ดู: sqlite.org/cintro.html ) ไม่มีสิ่งใดในเอกสารสำหรับฟังก์ชั่นที่บอกว่าคุณต้องเรียกมันว่า
feof()
เพื่อควบคุมการยกเลิกลูปอินพุทของคุณ fgets()
ใช้ผลที่ส่งกลับโดย stackoverflow.com/a/15485689/827263