Improve INSERT-per-second performance of SQLite?
优化sqlite很困难。C应用程序的大容量插入性能从每秒85个插入到每秒96000个插入!
背景:我们使用sqlite作为桌面应用程序的一部分。我们在XML文件中存储了大量的配置数据,这些数据被解析并加载到一个sqlite数据库中,以便在应用程序初始化时进行进一步的处理。sqlite非常适合这种情况,因为它速度快,不需要专门的配置,并且数据库作为单个文件存储在磁盘上。
理由:起初我对看到的表演感到失望。结果表明,根据数据库的配置方式和API的使用方式,SQLite的性能可能会有很大的不同(对于大容量插入和选择都是如此)。弄清楚所有选项和技术是什么不是一件小事,所以我认为创建这个社区wiki条目是明智的,以便与堆栈溢出阅读器共享结果,以避免其他人在相同的调查中遇到麻烦。
实验:而不是简单地讨论一般意义上的性能提示(即"使用事务!")我认为最好编写一些C代码,并实际测量各种选项的影响。我们将从一些简单的数据开始:
- 一个28 MB的制表符分隔文本文件(约865000条记录),用于多伦多市的完整运输计划。
- 我的测试机是运行Windows XP的3.60 GHz P4。
- 代码用Visual C++ 2005编译为"释放",具有"完全优化"(/ox),并支持快速代码(/OT)。
- 我使用的是sqlite"合并",它直接编译到我的测试应用程序中。我碰巧拥有的sqlite版本稍旧一些(3.6.7),但我怀疑这些结果会与最新版本相比较(如果您不这么认为,请留下评论)。
让我们写一些代码!
代码:一个简单的C程序,它逐行读取文本文件,将字符串拆分为值,然后将数据插入到一个sqlite数据库中。在代码的这个"基线"版本中,创建了数据库,但我们不会实际插入数据:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 | /************************************************************* 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, (clock() - cStartClock) / (double)CLOCKS_PER_SEC); sqlite3_close(db); return 0; } |
"控制"
按原样运行代码实际上并不执行任何数据库操作,但它会让我们了解原始C文件I/O和字符串处理操作的速度有多快。
Imported 864913 records in 0.94
seconds
伟大的!如果我们实际上不做任何插入,我们可以每秒做92万个插入:—)
"最坏情况"我们将使用从文件中读取的值生成SQL字符串,并使用sqlite3_exec调用该SQL操作:
1 2 | 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代码,并且每个插入都将发生在自己的事务中。多慢?
Imported 864913 records in 9933.61
seconds
伊克斯!2小时45分钟!这只是每秒85次插入。
使用事务默认情况下,sqlite将评估唯一事务中的每个insert/update语句。如果执行大量插入,建议在事务中包装操作:
1 2 3 4 5 6 7 8 9 10 11 |
Imported 864913 records in 38.03
seconds
那就更好了。只需在一个事务中包装所有插入,就可以将性能提高到每秒23000个插入。
使用准备好的语句使用事务是一个巨大的改进,但是如果我们一遍又一遍地使用相同的SQL,为每个插入重新编译SQL语句就没有意义。让我们使用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 | /* 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, (clock() - cStartClock) / (double)CLOCKS_PER_SEC); sqlite3_finalize(stmt); sqlite3_close(db); return 0; |
Imported 864913 records in 16.27
seconds
好极了!有更多的代码(不要忘记调用
默认情况下,SQLite将在发出操作系统级写入命令后暂停。这样可以保证数据被写入磁盘。通过设置
1 2 3 4 | /* 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); |
Imported 864913 records in 12.41
seconds
现在改进幅度较小,但每秒最多可插入69600个。
pragma journal_mode=内存考虑通过评估
1 2 3 4 | /* 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); |
Imported 864913 records in 13.50
seconds
比之前的优化慢一点,每秒64000个插入。
pragma synchronous=off和pragma journal_mode=memory让我们结合前面的两个优化。这有点危险(如果是
几个提示:
我也在这里和这里问过类似的问题。
尝试使用
避免使用sqlite3_clear_bindings(stmt);
测试中的代码每次都会设置足够的绑定。
来自sqlite文档的c api简介说
Prior to calling sqlite3_step() for the first time or immediately
after sqlite3_reset(), the application can invoke one of the
sqlite3_bind() interfaces to attach values to the parameters. Each
call to sqlite3_bind() overrides prior bindings on the same parameter
(请参见:sqlite.org/cintro.html)。在该函数的文档中,除了简单地设置绑定之外,没有任何说明您必须调用它的内容。
更多详细信息:http://www.hoogli.com/blogs/micro/index.html避免使用sqlite3_clear_bindings()。
批量插入
受到这篇文章和导致我出现在这里的堆栈溢出问题的启发——是否可以在一个sqlite数据库中一次插入多行?--我发布了我的第一个Git存储库:
https://github.com/rdpoor/createorupdate
在mysql、sqlite或postgresql数据库中批量加载一组activeRecords。它包括忽略现有记录、覆盖现有记录或引发错误的选项。我的基本基准测试显示,与顺序写入相比,速度提高了10倍——ymmv。
我在生产代码中使用它,我经常需要导入大型数据集,我对此非常满意。
如果可以将insert/update语句分成块,则批量导入的性能似乎最好。在只有几行的表上,10000左右的值对我很有效,ymmv…
如果您只关心读取,那么更快(但可能读取过时的数据)的版本是从多个线程(每个线程的连接)的多个连接读取。
首先查找表中的项目:
1 | SELECT COUNT(*) FROM table |
然后读取页面(限制/偏移)
1 | SELECT * FROM table ORDER BY _ROWID_ LIMIT <limit> OFFSET <offset> |
其中,每个线程计算和,如下所示:
1 | int limit = (count + n_threads - 1)/n_threads; |
对于每个线程:
1 | int offset = thread_index * limit |
对于我们的小型(200MB)数据库,这使速度提高了50-75%(Windows7上为3.8.0.2 64位)。我们的表非常不规范(1000-1500列,大约100000行或更多行)。
太多或太少的线程无法做到这一点,您需要对自己进行基准测试和分析。
同样对我们来说,sharedcache使性能变慢,所以我手动放置privatecache(因为它是为我们全局启用的)
我不能从交易中获得任何收益,除非我将缓存大小提高到更高的值,即
在阅读了本教程之后,我尝试将其实现到我的程序中。
我有4-5个包含地址的文件。每个文件大约有3000万条记录。我使用的配置与您建议的配置相同,但每秒插入的次数非常低(每秒大约10000条记录)。
这就是你的建议失败的地方。您可以对所有记录使用一个事务,并使用一个没有错误/失败的插入。假设您正在将每个记录拆分为不同表上的多个插入。如果唱片坏了怎么办?
on conflict命令不适用,因为如果一个记录中有10个元素,并且需要将每个元素插入到不同的表中,如果元素5出现约束错误,那么前面的4个插入也需要执行。
这里就是回滚的地方。回滚的唯一问题是您丢失了所有的插入并从顶部开始。你怎么能解决这个问题?
我的解决方案是使用多个事务。我每10000条记录就开始和结束一个事务(不要问为什么这个数字,它是我测试的最快的)。我创建了一个大小为10000的数组,并在其中插入成功的记录。当错误发生时,我执行回滚、开始一个事务、从数组中插入记录、提交,然后在断开的记录之后开始一个新事务。
这个解决方案帮助我绕过了在处理包含坏/重复记录的文件时遇到的问题(我有将近4%的坏记录)。
我创建的算法帮助我将进程缩短了2小时。文件1hr 30 m的最终加载过程仍然很慢,但与最初花费的4小时相比没有太大变化。我设法把插入速度从10.000/s提高到约14.000/s。
如果有人对如何加快速度有其他想法,我愿意接受建议。
更新:
除了我上面的答案,您还应该记住,每秒插入的次数取决于您使用的硬盘。我在3台不同的电脑上用不同的硬盘进行了测试,时间上有很大的不同。PC1(1小时30米),PC2(6小时)PC3(14小时),所以我开始想为什么会这样。
经过两周的研究和多个资源检查:硬盘、RAM、缓存,我发现硬盘上的某些设置会影响I/O速率。通过单击所需输出驱动器上的属性,可以在"常规"选项卡中看到两个选项。opt1:压缩此驱动器,opt2:允许对此驱动器的文件进行内容索引。
通过禁用这两个选项,所有3台电脑现在完成的时间大致相同(1小时和20至40分钟)。如果遇到插入速度慢的情况,请检查硬盘是否配置了这些选项。如果你想找到解决方案,它会节省你很多时间和头痛。
您的问题的答案是,新的sqlite3提高了性能,使用它。
这个答案为什么用sqlite插入sqlAlchemy比直接使用sqlite3慢25倍?通过sqlacalchemy orm,作者在0.5秒内插入了100k个插件,我看到了与python sqlite和sqlacalchemy类似的结果。这让我相信,使用sqlite3可以提高性能
使用ContentProvider在数据库中插入大容量数据。以下方法用于将大容量数据插入数据库。这将提高sqlite的每秒插入性能。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | private SQLiteDatabase database; database = dbHelper.getWritableDatabase(); public int bulkInsert(@NonNull Uri uri, @NonNull ContentValues[] values) { database.beginTransaction(); for (ContentValues value : values) db.insert("TABLE_NAME", null, value); database.setTransactionSuccessful(); database.endTransaction(); } |
调用BulkInsert方法:
1 2 | App.getAppContext().getContentResolver().bulkInsert(contentUriTable, contentValuesArray); |
链接:https://www.vogella.com/tutorials/androidsqlite/article.html有关详细信息,请使用ContentProvider部分进行检查