提高SQLite的每秒插入性能?
优化SQLite是棘手的。 C应用程序的批量插入性能可以从每秒85次插入到每秒超过96,000次插入!
背景:我们正在使用SQLite作为桌面应用程序的一部分。 我们有大量的configuration数据存储在XML文件中,这些数据被parsing并加载到SQLite数据库中,以便在初始化应用程序时进一步处理。 SQLite对于这种情况非常理想,因为它速度很快,不需要专门的configuration,数据库作为单个文件存储在磁盘上。
理由: 最初,我对我所看到的performance感到失望。 事实certificate,SQLite的性能可能会有很大的不同(对于批量插入和select),这取决于数据库的configuration方式以及如何使用API。 找出所有的select和技巧并不是一件简单的事情,所以我认为创build这个社区维基条目与Stack Overflow读者分享结果是很谨慎的,以便为其他人省去相同调查的麻烦。
实验:我不是简单地谈论一般意义上的性能提示(即“使用事务!” ),而是最好编写一些C代码并实际测量各种选项的影响。 我们将从一些简单的数据开始:
- 多伦多市全部交通时间表的28 MB TAB分隔文本文件(约865,000条logging)
- 我的testing机器是运行Windows XP的3.60 GHz P4。
- 该代码与Visual C ++ 2005一起编译为“发布”,具有“完全优化”(/ Ox)和优先快速代码(/ Ot)。
- 我正在使用SQLite“Amalgamation”,直接编译到我的testing应用程序。 SQLite版本我碰巧有一点老(3.6.7),但我怀疑这些结果将是相当于最新版本(请留下评论,如果你不这么认为)。
我们来写一些代码!
代码:一个简单的C程序,逐行读取文本文件,将string拆分为值,然后将数据插入到SQLite数据库中。 在这个“基准”版本的代码中,数据库被创build,但是我们不会实际插入数据:
/************************************************************* 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和string处理操作的速度。
在0.94秒内导入了864913条logging
大! 如果我们实际上没有做任何插入操作,我们可以每秒做92万个插入:-)
“最坏情况”
我们将使用从文件读取的值生成SQLstring,并使用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条logging
哎呀! 2小时45分钟! 那每秒只有85个插入。
使用交易
默认情况下,SQLite将评估唯一事务中的每个INSERT / UPDATE语句。 如果执行大量的插入操作,build议将操作包装在事务中:
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条logging
好多了 简单地将所有插入内容包装在单个事务中,将性能提高到每秒23,000次插入。
使用预备声明
使用事务是一个巨大的改进,但是如果我们使用相同的SQL over-over,那么重新编译每个插入的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条logging
太好了! 还有一点点的代码(不要忘记调用sqlite3_clear_bindings
和sqlite3_reset
),但是我们的性能提高了一倍以上,达到每秒53,000次插入。
PRAGMA同步=closures
默认情况下,SQLite将在发出OS级写入命令后暂停。 这保证了数据被写入磁盘。 通过设置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条logging
现在的改进小一点,但是我们每秒可以达到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条logging
比之前的每秒64,000个插入优化慢一点。
PRAGMA synchronous = 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条logging
太棒了! 我们能够做到每秒72,000次插入。
使用内存数据库
只是踢,让我们build立在以前的所有优化和重新定义数据库文件名,所以我们完全在RAM中工作:
#define DATABASE ":memory:"
在10.94秒内导入864913条logging
将数据库存储在RAM中并不是非常实用,但是令人印象深刻的是,我们可以每秒执行79,000次插入。
重构C代码
虽然不是特别的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条logging
对参数绑定中使用的string处理代码进行了轻微的重构,使我们能够每秒执行96,700次插入。 我认为可以说这个速度很快 。 当我们开始调整其他variables(即页面大小,索引创build等)时,这将是我们的基准。
总结(迄今为止)
我希望你仍然和我在一起! 我们之所以走上这条路,是因为大容量插入的性能与SQLite差异很大,而且加速我们的操作需要做什么改变并不总是很明显。 使用相同的编译器(和编译器选项),相同版本的SQLite和相同的数据,我们已经优化了我们的代码和SQLite的使用, 从每秒85次插入到每秒超过96,000次插入的最坏情况!
CREATE INDEX,然后INSERT和INSERT,然后CREATE INDEX
在开始测量SELECT
性能之前,我们知道我们将创build索引。 在下面的答案之一中提出,在进行批量插入时,插入数据后创build索引(而不是先创build索引然后插入数据)会更快。 咱们试试吧:
创build索引然后插入数据
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条logging
插入数据然后创build索引
... 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条logging
正如预期的那样,如果一列被索引,批量插入会变慢,但是如果在插入数据之后创build索引,批量插入会有所帮助。 我们的无索引基准是每秒96,000个插入。 首先创build索引,然后插入数据给我们每秒47,700插入,而插入数据,然后创build索引每秒63,300插入。
我很乐意采取其他scheme的build议尝试…并将尽快编译SELECT查询类似的数据。
几个提示:
- 将插入/更新放入事务中。
- 对于旧版本的SQLite – 考虑一个不偏执的日记模式(
pragma journal_mode
日记模式)。 有正常,然后有OFF
,这可以显着提高插入速度,如果你不担心数据库可能会损坏,如果操作系统崩溃。 如果你的应用程序崩溃的数据应该没问题。 请注意,在较新的版本中,OFF/MEMORY
设置对于应用程序级别的崩溃是不安全的。 - 使用页面大小也有所不同(
PRAGMA page_size
)。 更大的页面大小可以使读取和写入更快一点,因为更大的页面被保存在内存中。 请注意,更多的内存将用于您的数据库。 - 如果你有索引,可以考虑在完成所有插入之后调用
CREATE INDEX
。 这比创build索引然后进行插入要快得多。 - 你必须非常小心,如果你有并发访问SQLite,因为整个数据库在写入完成时被locking,虽然多个读取器是可能的,写入将被locking。 在新的SQLite版本中增加了一个WAL,这已经有所改进。
- 利用节省的空间…小型数据库变得更快。 例如,如果您有键值对,可以尝试将键设置为
INTEGER PRIMARY KEY
,这将replace表中隐含的唯一行号列。 - 如果您使用多个线程,则可以尝试使用共享页面caching ,这将允许在线程之间共享加载的页面,这可以避免昂贵的I / O调用。
- 不要使用
!feof(file)
!
我也在这里和这里问过类似的问题。
尝试对这些插入使用SQLITE_STATIC
而不是SQLITE_TRANSIENT
。
SQLITE_TRANSIENT
将导致SQLite在返回之前复制string数据。
SQLITE_STATIC
告诉它你的内存地址是有效的,直到查询完成(在这个循环中总是这样)。 这将为您节省几个分配,复制和释放每个循环的操作。 可能是一个很大的改进。
避免sqlite3_clear_bindings(stmt);
testing中的代码每次都设置绑定,通过这些就足够了。
来自SQLite文档的C API介绍说
在第一次调用sqlite3_step()之前或在sqlite3_reset()之后立即调用sqlite3_step()之前,应用程序可以调用其中一个sqlite3_bind()接口来将值附加到参数中。 每次调用sqlite3_bind()都会覆盖同一个参数上的先前绑定
(请参阅: sqlite.org/cintro.html )。 文档里没有这个function ,除了简单地设置绑定之外,你必须调用它。
更多详细信息: http : //www.hoogli.com/blogs/micro/index.html#Avoid_sqlite3_clear_bindings()
在批量插入
受这篇文章和Stack Overflow问题的启发, 是否可以在SQLite数据库中一次插入多行? – 我已经发布了我的第一个Git仓库:
https://github.com/rdpoor/CreateOrUpdate
它将一组ActiveRecords批量加载到MySQL ,SQLite或PostgreSQL数据库中。 它包含一个选项,可以忽略现有logging,覆盖它们或引发错误。 我的基本基准testing显示,与连续写入相比,速度提高了10倍 – YMMV。
我在生产代码中使用它,我经常需要导入大型数据集,而且我非常满意。
批量导入似乎执行得最好,如果你可以大块你的INSERT / UPDATE语句。 在一张只有几行YMMV的桌子上,价值10,000左右的工作对我来说效果很好。
如果你只关心读取,稍快(但可能读取陈旧的数据)版本是从多个线程(每线程连接)的多个连接读取。
首先在表格中find这些项目:
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)分贝,这使得速度提高了50-75%(Windows 7上的3.8.0.2 64位)。 我们的表格非常规范化(1000-1500列,大约100,000或更多行)。
太多或太less的线程都不会这样做,您需要对自己进行基准testing和configuration。
同样对于我们来说,SHAREDCACHE使性能变慢,所以我手动把PRIVATECACHE(因为我们全局启用)
我没有从交易中获得任何收益,直到我将cache_size提高到更高的值,即PRAGMA cache_size=10000;
阅读本教程后,我尝试将其实现到我的程序中。
我有4-5个包含地址的文件。 每个文件有大约3000万条logging。 我使用的是与您build议的相同的configuration,但是我每秒的INSERT数量却很低(每秒约10.000条logging)。
这里是你的build议失败的地方。 您对所有logging使用单个事务,并且单个插入没有错误/失败。 假设您将每条logging拆分为不同表上的多个插入。 如果logging被破坏会发生什么?
ON CONFLICT命令不适用,如果在logging中有10个元素,并且需要将每个元素插入到不同的表中,如果元素5获得CONSTRAINT错误,则以前的所有4个插入都需要执行。
所以这里是回滚的地方。 只有回滚的问题是,你失去了所有的插入,并从顶部开始。 你怎么解决这个问题?
我的解决scheme是使用多个事务。 我每10.000条logging开始和结束交易(不要问为什么这个数字,这是我testing的最快的一个)。 我创build了一个大小为10.000的数组,并在那里插入成功的logging。 发生错误时,我做一个回滚,开始一个事务,从我的数组插入logging,提交,然后在破损的logging后开始一个新的事务。
这个解决scheme帮助我绕过了处理包含不良/重复logging的文件(我几乎有4%的不良logging)的问题。
我创build的algorithm帮助我减less了2个小时的过程。 文件1小时30分钟的最后加载过程仍然缓慢,但与最初的4小时相比并不相同。 我设法将插入速度从10.000 / s加速到〜14.000 / s
如果任何人有任何其他想法如何加快,我愿意提出build议。
更新 :
除了上面我的回答,你应该记住,每秒插入取决于你使用的硬盘驱动器。 我使用不同的硬盘在3台不同的个人电脑上进行了testing,并且在不同的时代有着巨大的差异 PC1(1小时30分),PC2(6小时)PC3(14小时),所以我开始想知道为什么会这样。
经过两周的研究和检查多个资源:硬盘驱动器,RAM,caching,我发现硬盘上的一些设置可以影响I / O速率。 通过在所需输出驱动器上单击属性,可以在常规选项卡中看到两个选项。 Opt1:压缩此驱动器,Opt2:允许此驱动器的文件具有索引的内容。
通过禁用这两个选项,所有3台个人电脑现在需要大约相同的时间完成(1小时和20至40分钟)。 如果遇到插入缓慢的问题,请检查您的硬盘是否configuration了这些选项。 这将节省您大量的时间和头痛,试图find解决scheme
你的问题的答案是,较新的sqlite3已经提高了性能,使用。
这个答案为什么SQLAlchemy插入sqlite比使用sqlite3直接慢25倍? 通过SqlAlchemy Orm作者有0.5k插入0.5秒,我已经看到类似的结果与python-sqlite和SqlAlchemy。 这使我相信sqlite3的性能有所提高
Paul Betts讲述了如何如此快速地制作C# akavache : https : //www.youtube.com/watch?v = j7WnQhwBwqA
也许你可以find一些线索给你。 在这里做简短的总结太长了