优化SQLite很棘手。C应用程序的批量插入性能可以从每秒85次插入到每秒96,000多次插入不等!
背景:我们使用SQLite作为桌面应用程序的一部分。我们有大量存储在XML文件中的配置数据,这些数据被解析并加载到SQLite数据库中,以便在应用程序初始化时进行进一步处理。SQLite非常适合这种情况,因为它速度快,不需要专门的配置,并且数据库作为单个文件存储在磁盘上。
理由:起初我对我看到的表演感到失望。事实证明,SQLite的性能可能会有很大差异(无论是批量插入还是选择),这取决于数据库的配置方式以及您如何使用API。弄清楚所有选项和技术是什么并不是一件小事,所以我认为创建这个社区wiki条目与Stack Overflow阅读器共享结果是谨慎的,以节省其他人进行相同调查的麻烦。
实验:与其简单地谈论一般意义上的性能技巧(即“使用交易!”),我认为最好写一些C代码和实际测量各种选项的影响。我们将从一些简单的数据开始:
让我们写一些代码!
代码:一个简单的C程序,逐行读取文本文件,将字符串拆分为值,然后将数据插入SQLite数据库。在这个“基线”版本的代码中,数据库被创建,但我们不会实际插入数据:
/*************************************************************Baseline code to experiment with SQLite performance.
Input data is a 28 MB TAB-delimited text file of thecomplete Toronto Transit System schedule/route infofrom 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;}
按原样运行代码实际上不会执行任何数据库操作,但它会让我们了解原始C文件I/O和字符串处理操作的速度。
在0.94中导入864913条记录秒
太好了!我们每秒可以执行920,000次插入,前提是我们实际上没有执行任何插入:-)
我们将使用从文件中读取的值生成SQL字符串,并使用sqlite3_exec调用SQL操作:
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代码,并且每次插入都将发生在自己的事务中。
在9933.61中导入864913条记录秒
哎呀!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);
在38.03中导入864913条记录秒
那更好。只需将我们所有的插入包装在一个事务中,就可以将我们的性能提高到每秒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;
在16.27中导入864913条记录秒
不错!代码有点多(别忘了调用sqlite3_clear_bindings
和sqlite3_reset
),但我们的性能已经翻了一番多,达到了每秒53,000次插入。
默认情况下,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);
在12.41中导入864913条记录秒
改进现在较小,但我们达到了每秒69,600次插入。
考虑通过评估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);
在13.50中导入864913条记录秒
比之前每秒64,000次插入。的优化慢一点
让我们结合前面的两个优化。这有点风险(以防崩溃),但我们只是导入数据(而不是运行银行):
/* 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中导入864913条记录秒
太棒了!我们能做到每秒72,000个插入。
只是为了刺激,让我们建立在所有先前的优化基础上并重新定义数据库文件名,以便我们完全在RAM中工作:
#define DATABASE ":memory:"
在10.94中导入864913条记录秒
将我们的数据库存储在RAM中并不是非常实用,但令人印象深刻的是我们可以执行每秒79,000次插入。
虽然不是特别的SQLite改进,但我不喜欢while
循环中额外的char*
赋值操作。让我们快速重构该代码以将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);
注意:我们回到使用真实的数据库文件。内存数据库很快,但不一定实用
8.94中导入864913条记录秒
对参数绑定中使用的字符串处理代码进行了轻微的重构,使我们能够执行每秒96,700次插入。我认为可以肯定地说这是足够快。当我们开始调整其他变量(即页面大小、索引创建等)时,这将是我们的基准。
我希望你还和我在一起!我们开始走这条路的原因是SQLite的大容量插入性能差异很大,并且需要做哪些更改来加快我们的操作并不总是很明显。使用相同的编译器(和编译器选项)、相同版本的SQLite和相同的数据,我们优化了我们的代码和SQLite的使用,以达到从每秒85次插入的最坏情况到每秒超过96,000次插入!
在我们开始衡量SELECT
性能之前,我们知道我们将创建索引。下面的一个答案中建议,在进行批量插入时,在插入数据后创建索引更快(而不是先创建索引然后插入数据)。让我们尝试:
创建索引然后插入数据
sqlite3_exec(db, "CREATE INDEX 'TTC_Stop_Index' ON 'TTC' ('Stop')", NULL, NULL, &sErrMsg);sqlite3_exec(db, "BEGIN TRANSACTION", NULL, NULL, &sErrMsg);...
在18.13中导入864913条记录秒
插入数据,然后创建索引
...sqlite3_exec(db, "END TRANSACTION", NULL, NULL, &sErrMsg);sqlite3_exec(db, "CREATE INDEX 'TTC_Stop_Index' ON 'TTC' ('Stop')", NULL, NULL, &sErrMsg);
在13.66中导入864913条记录秒
正如预期的那样,如果索引一列,批量插入会更慢,但如果索引是在插入数据之后创建的,则会有所不同。我们的无索引基线是每秒96,000次插入。首先创建索引然后插入数据每秒为我们提供47,700次插入,而先插入数据然后创建索引每秒为我们提供63,300次插入。
我很乐意为其他场景提供建议,并将很快为SELECT查询编译类似的数据。