gpt4 book ai didi

c - 提高SQLite每秒INSERT的性能?

转载 作者:行者123 更新时间:2023-11-29 17:54:20 25 4
gpt4 key购买 nike

优化SQLite是棘手的。 C应用程序的大容量插入性能可以从每秒85次插入到每秒超过96,000次插入!

背景:我们正在将SQLite用作桌面应用程序的一部分。我们将大量配置数据存储在XML文件中,这些文件会被解析并加载到SQLite数据库中,以便在初始化应用程序时进行进一步处理。 SQLite非常适合这种情况,因为它速度快,不需要专门的配置,并且数据库作为单个文件存储在磁盘上。

基本原理:最初,我对看到的性能感到失望。事实证明,取决于数据库的配置方式和使用API​​的方式,SQLite的性能可能会发生很大的变化(对于批量插入和选择)。弄清楚所有选项和技术是什么都不是一件容易的事,因此我认为创建此社区Wiki条目与Stack Overflow读者共享结果以节省其他人的麻烦是审慎的做法。

实验:我认为,最好是编写一些C代码并实际衡量各种选择的影响,而不是简单地讨论一般意义上的性能提示(即“使用事务!”)。我们将从一些简单的数据开始:


complete transit schedule for the city of Toronto的28 MB TAB分隔的文本文件(约865,000条记录)
我的测试计算机是运行Windows XP的3.60 GHz P4。
该代码在Visual C++ 2005中被编译为带有“完全优化”(/ Ox)和“优先快速代码”(/ 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;
}




“控制”

按原样运行代码实际上不会执行任何数据库操作,但是它将使我们了解原始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个记录
  秒


kes! 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_bindingssqlite3_reset),但是我们的性能提高了一倍以上,达到每秒53,000次插入。

PRAGMA同步= OFF

默认情况下,SQLite将在发出OS级写命令后暂停。这样可以确保将数据写入磁盘。通过设置 synchronous = OFF,我们指示SQLite只需将数据移交给OS进行写入,然后继续。如果计算机在将数据写入磁盘之前遭受灾难性崩溃(或电源故障),则数据库文件可能会损坏:

/* 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 =内存

考虑通过评估 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次插入!



先创建索引,然后插入VS.插入,然后创建索引

在开始衡量 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查询编译类似的数据。

最佳答案

几点提示:


将插入/更新放入事务中。
对于旧版本的SQLite-考虑使用较少偏执的日记模式(pragma journal_mode)。有NORMAL,然后有OFF,如果您不太担心如果操作系统崩溃,数据库可能会损坏,则可以显着提高插入速度。如果您的应用程序崩溃,数据应该没问题。请注意,在较新的版本中,OFF/MEMORY设置对于应用程序级崩溃不安全。
播放页面大小也会有所不同(PRAGMA page_size)。由于较大的页面保留在内存中,因此具有较大的页面大小可以使读取和写入的速度更快。请注意,更多的内存将用于您的数据库。
如果有索引,请在完成所有插入操作后考虑调用CREATE INDEX。这比创建索引然后进行插入要快得多。
如果您可以同时访问SQLite,则必须非常小心,因为写入完成后整个数据库将被锁定,尽管可能有多个读取器,但写入将被锁定。通过在较新的SQLite版本中添加WAL,对此进行了一些改进。
利用节省空间的优势...较小的数据库运行速度更快。例如,如果您具有键值对,请尝试尽可能将键设置为INTEGER PRIMARY KEY,它将替换表中隐含的唯一行号列。
如果使用多个线程,则可以尝试使用shared page cache,这将允许在线程之间共享已加载的页面,从而避免了昂贵的I / O调用。
Don't use !feof(file)!


我也问过类似的问题herehere

关于c - 提高SQLite每秒INSERT的性能?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/49001220/

25 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com