优化SQLite很棘手。 C应用程序的批量插入性能可以从每秒85次插入到每秒超过96,000次插入!
背景: 我们使用SQLite作为桌面应用程序的一部分。我们有大量的配置数据存储在XML文件中,这些数据被解析并加载到SQLite数据库中,以便在初始化应用程序时进一步处理。 SQLite非常适合这种情况,因为它速度快,不需要专门配置,数据库作为单个文件存储在磁盘上。
理由: 最初我对我看到的表现感到失望。 事实证明,SQLite的性能可能会有很大差异(对于批量插入和选择),具体取决于数据库的配置方式以及您使用API的方式。弄清楚所有选项和技术是什么并不是一件小事,所以我认为创建这个社区wiki条目与Stack Overflow读者分享结果是谨慎的,以便为其他人节省相同调查的麻烦。
本实验: 而不是简单地谈论一般意义上的性能提示(即 “使用交易!”),我认为最好写一些C代码和 实际测量 各种选择的影响。我们将从一些简单的数据开始:
- 一个28 MB的TAB分隔文本文件(大约865,000条记录) 多伦多市完整的运输时间表
- 我的测试机器是运行Windows XP的3.60 GHz P4。
- 代码是用。编译的 Visual C ++ 2005年作为“完全优化”(/ Ox)和赞成快速代码(/ Ot)的“发布”。
- 我正在使用SQLite“Amalgamation”,直接编译到我的测试应用程序中。我碰巧遇到的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;
}
“控制”
按原样运行代码实际上并不执行任何数据库操作,但它会让我们了解原始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次插入。
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);
在12.41中导入了864913条记录 秒
这些改进现在变小了,但我们很乐意 每秒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);
在13.50中导入了864913条记录 秒
比之前的优化要慢一点 每秒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);
在12.00中导入了864913条记录 秒
太棒了!我们能够做到 每秒72,000次插入。
使用内存数据库
只是为了解决问题,让我们在之前的所有优化基础上重新定义数据库文件名,以便我们完全在RAM中工作:
#define DATABASE ":memory:"
在10.94中导入了864913条记录 秒
将我们的数据库存储在RAM中并不是非常实用,但令人印象深刻的是我们可以执行 每秒79,000次插入。
重构C代码
虽然不是特别的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);
注意:我们回到使用真正的数据库文件。内存数据库很快,但不一定实用
在8.94中导入了864913条记录 秒
对我们的参数绑定中使用的字符串处理代码进行了轻微的重构,这使我们能够执行 每秒96,700次插入。 我认为可以说这是安全的 很快。当我们开始调整其他变量(即页面大小,索引创建等)时,这将是我们的基准。
摘要(到目前为止)
我希望你还和我在一起! 我们开始走这条道路的原因是,SQLite的批量插入性能变化如此之大,并且并不总是很明显需要做出哪些改变来加速我们的操作。使用相同的编译器(和编译器选项),相同版本的SQLite和相同的数据我们优化了我们的代码和我们使用的SQLite去 从最糟糕的情况来看,每秒85次插入,每秒超过96,000次插入!
创建INDEX然后INSERT与INSERT然后CREATE INDEX
在我们开始测量之前 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查询编译类似的数据。