提问人: 提问时间:11/11/2009 最后编辑:21 revs, 12 users 42%Peter Mortensen 更新时间:11/9/2023 访问量:489756
提高 SQLite 的每秒 INSERT 性能
Improve INSERT-per-second performance of SQLite
问:
优化SQLite是很棘手的。C 应用程序的批量插入性能可以从每秒 85 个插入到每秒超过 96,000 个插入不等!
背景:我们正在使用SQLite作为桌面应用程序的一部分。我们在XML文件中存储了大量的配置数据,这些数据被解析并加载到SQLite数据库中,以便在应用程序初始化时进行进一步处理。SQLite 非常适合这种情况,因为它速度快,不需要专门的配置,并且数据库作为单个文件存储在磁盘上。
理由:最初我对我所看到的表现感到失望。事实证明,SQLite的性能可能会有很大差异(无论是批量插入还是选择),具体取决于数据库的配置方式以及使用API的方式。弄清楚所有的选项和技术都不是一件小事,所以我认为创建这个社区 wiki 条目与 Stack Overflow 读者分享结果是谨慎的,这样可以省去其他人进行相同调查的麻烦。
实验:与其简单地谈论一般意义上的性能技巧(即“使用事务!”),我认为最好编写一些 C 代码并实际衡量各种选项的影响。我们将从一些简单的数据开始:
- 一个 28 MB 的制表符分隔文本文件(约 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 和字符串处理操作的速度。
导入的 864913 记录 0.94 秒
伟大!我们每秒可以进行 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 代码,并且每次插入都会在自己的事务中发生。有多慢?
导入的 864913 记录在 9933.61 秒
哎呀!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);
导入的 864913 38.03 中的记录 秒
那就更好了。只需将所有插入件包装在单个事务中,即可将我们的性能提高到每秒 23,000 次插入。
使用预准备语句
使用事务是一个巨大的改进,但是如果我们一遍又一遍地使用相同的 SQL,那么为每次插入重新编译 SQL 语句是没有意义的。让我们使用 SQL 语句编译一次,然后使用以下命令将参数绑定到该语句:sqlite3_prepare_v2
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;
导入的 864913 记录 in 16.27 秒
好!代码多了一点(别忘了调用和),但我们的性能提高了一倍多,达到每秒 53,000 次插入。sqlite3_clear_bindings
sqlite3_reset
PRAGMA 同步 = OFF
默认情况下,SQLite 将在发出操作系统级写入命令后暂停。这样可以保证将数据写入磁盘。通过设置 ,我们指示SQLite简单地将数据移交给操作系统进行写入,然后继续。如果计算机在将数据写入盘片之前发生灾难性崩溃(或电源故障),则数据库文件可能会损坏:synchronous = OFF
/* 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);
导入的 864913 记录 in 13.50 秒
比之前的优化速度慢一点,每秒 64,000 次插入。
PRAGMA 同步 = OFF,PRAGMA journal_mode = 内存
让我们结合前两个优化。风险更大一些(如果发生崩溃),但我们只是导入数据(而不是运行银行):
/* 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 次插入。
使用 In-Memory 数据库
只是为了踢球,让我们在之前的所有优化的基础上,重新定义数据库文件名,以便我们完全在RAM中工作:
#define DATABASE ":memory:"
导入的 864913 记录 在 10.94 秒
将数据库存储在 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);
导入的 864913 记录 在 13.66 秒
正如预期的那样,如果对一列编制索引,则大容量插入速度会较慢,但如果在插入数据后创建索引,则确实会有所不同。我们的无索引基线是每秒 96,000 次插入。首先创建索引,然后插入数据,每秒插入 47,700 次,而先插入数据,然后创建索引,每秒插入 63,300 次。
我很乐意接受其他方案的建议来尝试......并将很快为 SELECT 查询编译类似的数据。
答:
如果可以对 INSERT/UPDATE 语句进行分块,则批量导入似乎性能最佳。10,000 左右的值在只有几行的桌子上对我来说效果很好,YMMV......
评论
几个提示:
- 在事务中放置插入/更新。
- 对于旧版本的 SQLite - 考虑一个不那么偏执的日记模式()。有 ,然后是 ,如果您不太担心操作系统崩溃时数据库可能会损坏,它可以显着提高插入速度。如果您的应用程序崩溃,数据应该没问题。请注意,在较新的版本中,这些设置对于应用程序级崩溃是不安全的。
pragma journal_mode
NORMAL
OFF
OFF/MEMORY
- 调整页面大小也会有所作为()。较大的页面大小可以使读取和写入速度更快,因为较大的页面保存在内存中。请注意,数据库将使用更多内存。
PRAGMA page_size
- 如果您有索引,请考虑在完成所有插入后调用。这比创建索引然后进行插入要快得多。
CREATE INDEX
- 如果您可以并发访问 SQLite,则必须非常小心,因为当写入完成时,整个数据库将被锁定,尽管可以有多个读取器,但写入将被锁定。通过在较新的SQLite版本中添加WAL,这在一定程度上得到了改善。
- 利用节省空间的优势...数据库越小,运行速度越快。例如,如果您有键值对,请尝试将键设为键,这将替换表中隐含的唯一行号列。
INTEGER PRIMARY KEY
- 如果您使用多个线程,您可以尝试使用共享页面缓存,这将允许在线程之间共享加载的页面,从而避免昂贵的 I/O 调用。
- 不要使用 !
feof(file)!
评论
在批量刀片上
受到这篇文章的启发,以及将我引向这里的 Stack Overflow 问题——是否可以在 SQLite 数据库中一次插入多行? -- 我已经发布了我的第一个 Git 存储库:
https://github.com/rdpoor/CreateOrUpdate
将 ActiveRecords 数组批量加载到 MySQL、SQLite 或 PostgreSQL 数据库中。它包括一个选项,用于忽略现有记录、覆盖它们或引发错误。我的基本基准测试显示,与顺序写入相比,速度提高了 10 倍 -- YMMV。
我在生产代码中使用它,我经常需要导入大型数据集,我对它非常满意。
评论
避免sqlite3_clear_bindings(stmt)。
测试中的代码每次都设置绑定,通过这些绑定应该就足够了。
SQLite文档中的C API介绍说:
在首次调用 sqlite3_step() 之前或立即调用 sqlite3_reset() 之后,应用程序可以调用 sqlite3_bind() 接口将值附加到参数。每 调用 sqlite3_bind() 会覆盖同一参数上的先前绑定
文档中没有任何内容sqlite3_clear_bindings
除了简单地设置绑定之外,还必须调用它。
更多详情:Avoid_sqlite3_clear_bindings()
评论
如果您只关心读取,那么更快(但可能会读取过时的数据)版本是从多个线程(每个线程的连接)读取多个连接。
首先在表中找到项目:
SELECT COUNT(*) FROM table
然后以页面为单位读取(LIMIT/OFFSET):
SELECT * FROM table ORDER BY _ROWID_ LIMIT <limit> OFFSET <offset>
其中 和 是按线程计算的,如下所示:<limit>
<offset>
int limit = (count + n_threads - 1)/n_threads;
对于每个线程:
int offset = thread_index * limit
对于我们的小 (200mb) db,这提高了 50-75% 的速度(Windows 7 上的 3.8.0.2 64 位)。我们的表是高度非规范化的(1000-1500 列,大约 100,000 行或更多)。
线程太多或太少都做不到,您需要对自己进行基准测试和分析。
同样对我们来说,SHAREDCACHE 使性能变慢,所以我手动放置了 PRIVATECACHE(因为它是全局为我们启用的)
尝试对这些插入物使用 SQLITE_STATIC
而不是 SQLITE_TRANSIENT
。
SQLITE_TRANSIENT
将导致SQLite在返回之前复制字符串数据。
SQLITE_STATIC
告诉它,在执行查询之前,您提供给它的内存地址将有效(在此循环中始终如此)。这将为每个循环节省多个分配、复制和解除分配操作。可能是一个很大的改进。
我无法从交易中获得任何收益,直到我将cache_size提高到更高的价值,即PRAGMA cache_size=10000;
评论
阅读本教程后,我尝试将其实现到我的程序中。
我有 4-5 个包含地址的文件。每个文件大约有 3000 万条记录。我使用的是您建议的相同配置,但我每秒的 INSERT 数量非常低(每秒 ~10.000 条记录)。
这是您的建议失败的地方。您对所有记录使用单个事务,对没有错误/失败的单个插入使用。假设您正在将每条记录拆分为不同表上的多个插入。如果记录被打破会怎样?
ON CONFLICT 命令不适用,因为如果记录中有 10 个元素,并且需要将每个元素插入到不同的表中,如果元素 5 出现 CONSTRAINT 错误,则所有前 4 次插入也需要删除。
所以这就是回滚的地方。回滚的唯一问题是您丢失了所有插入并从顶部开始。你怎么能解决这个问题?
我的解决方案是使用多个事务。我每 10.000 条记录开始和结束一笔交易(不要问为什么这个数字,这是我测试过的最快的一个)。我创建了一个大小为 10.000 的数组,并在其中插入成功的记录。当错误发生时,我执行回始一个事务,从我的数组中插入记录,提交,然后在损坏的记录之后开始一个新的事务。
这个解决方案帮助我绕过了在处理包含错误/重复记录的文件时遇到的问题(我有近 4% 的错误记录)。
我创建的算法帮助我将流程缩短了 2 小时。文件的最终加载过程 1 小时 30 分钟,这仍然很慢,但与最初花费的 4 小时相比不尽相同。我设法将插入速度从 10.000/s 加速到 ~14.000/s
如果有人对如何加快速度有任何其他想法,我愿意接受建议。
更新:
除了我上面的回答之外,您还应该记住,每秒插入次数也取决于您使用的硬盘驱动器。我在 3 台具有不同硬盘驱动器的不同 PC 上对其进行了测试,并得到了巨大的时间差异。PC1 (1小时30分钟), PC2 (6小时) PC3 (14小时), 所以我开始想知道为什么会这样。
经过两周的研究和检查多种资源:硬盘、内存、缓存,我发现硬盘上的某些设置会影响 I/O 速率。通过单击所需输出驱动器上的属性,您可以在常规选项卡中看到两个选项。 选项 1:压缩此驱动器,选项 2:允许此驱动器的文件对内容进行索引。
通过禁用这两个选项,所有 3 台 PC 现在需要大致相同的时间才能完成(1 小时 20 到 40 分钟)。如果遇到插入速度慢的情况,请检查您的硬盘驱动器是否配置了这些选项。它将为您节省大量时间和麻烦,试图找到解决方案
评论
您的问题的答案是较新的 SQLite 3 提高了性能,请使用它。
这个答案 为什么使用 sqlite 的 SQLAlchemy 插入比直接使用 sqlite3 慢 25 倍? 作者:SqlAlchemy Orm 作者在 100 秒内插入了 0.5k,我在 python-sqlite 和 SqlAlchemy 中看到了类似的结果。这让我相信 SQLite 3 的性能有所提高。
使用 ContentProvider 在 db 中插入批量数据。 以下方法用于将批量数据插入数据库。这应该可以提高SQLite的每秒插入性能。
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 方法:
App.getAppContext().getContentResolver().bulkInsert(contentUriTable,
contentValuesArray);
链接:https://www.vogella.com/tutorials/AndroidSQLite/article.html 查看“使用 ContentProvider”部分了解更多详细信息
评论
bulkInsert
insert
像@Jimmy_A那样将任务拆分为多个事务是要走的路。否则,您可能会因怪物事务和繁重的 COMMIT 任务而使 RAM 饱和。
为了进一步调整性能,您还可以在硬盘驱动器上启用回写缓存,前提是您使用某种电池备份系统(笔记本电脑、UPS、带电池的 RAID 控制器......
在我的情况下,使用 s 的速度增加了一倍,因为在内部它与此处建议的批处理 INSERTS 相同。PRAGMA journal_mode = WAL
INSERT
就我而言,我需要将数据导入索引,而不仅仅是表。SQLite具有WITHOUT ROWID的出色功能,它允许将表和索引组合在一起。默认情况下,SQLite中的表也是B树,任何索引都存储在单独的B树页面中。使用 WITHOUT ROWID 时,表和索引仅使用一个 B 树。
我还使用了技术,它应该防止SQLite以牺牲数据库大小为代价来提高空间利用率,但它似乎对性能没有任何明显的影响。PRAGMA auto_vacuum = 0
虽然我的情况与OP的要求略有不同,但使用WAL的第一个建议应该对他的情况有所影响。
评论
sqlite3_clear_bindings(stmt);
feof()
fgets()