提高 SQLite 的每秒 INSERT 性能

Improve INSERT-per-second performance of SQLite

提问人: 提问时间:11/11/2009 最后编辑:21 revs, 12 users 42%Peter Mortensen 更新时间:11/9/2023 访问量:489756

问:

优化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_v2sqlite3_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_bindingssqlite3_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*whilestrtok()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 查询编译类似的数据。

C 性能 SQLite 优化

评论

13赞 Mike Willekes 11/11/2009
好点子!在我们的例子中,我们正在处理大约 150 万个键/值对,这些键/值对从 XML 和 CSV 文本文件读取到 200k 条记录中。与运行SO等站点的数据库相比,它很小,但足够大,以至于调整SQLite性能变得很重要。
56赞 CAFxX 2/21/2012
“我们在XML文件中存储了大量配置数据,这些数据被解析并加载到SQLite数据库中,以便在应用程序初始化时进行进一步处理。” 为什么不首先将所有内容保存在SQLite数据库中,而不是存储在XML中,然后在初始化时加载所有内容?
15赞 ahcox 8/4/2012
你试过不打电话吗?每次设置绑定就足够了:在首次调用 sqlite3_step() 之前或紧接在 sqlite3_reset() 之后,应用程序可以调用其中一个 sqlite3_bind() 接口将值附加到参数。每次调用 sqlite3_bind() 都会覆盖同一参数上的先前绑定(请参阅:sqlite.org/cintro.html)。该函数的文档中没有任何内容说您必须调用它。sqlite3_clear_bindings(stmt);
27赞 peterchen 11/26/2012
您是否进行了重复测量?避免 7 个本地指针的 4s “胜利”很奇怪,即使假设一个混淆的优化器也是如此。
12赞 Keith Thompson 11/4/2015
不要用于控制输入循环的终止。使用 返回的结果。stackoverflow.com/a/15485689/827263feof()fgets()

答:

62赞 2 revs, 2 users 67%Leon #1

如果可以对 INSERT/UPDATE 语句进行分块,则批量导入似乎性能最佳。10,000 左右的值在只有几行的桌子上对我来说效果很好,YMMV......

评论

24赞 Alix Axel 10/13/2013
您需要调整 x = 10,000,以便 x = 缓存 [= cache_size * page_size] / 插入的平均大小。
870赞 8 revs, 6 users 65%Snazzer #2

几个提示:

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

我也在这里这里问过类似的问题。

评论

13赞 OneWorld 1/31/2014
文档不知道 PRAGMA journal_mode正常 sqlite.org/pragma.html#pragma_journal_mode
8赞 Snazzer 1/31/2014
已经有一段时间了,在引入 WAL 之前,我的建议适用于旧版本。看起来 DELETE 是新的正常设置,现在还有 OFF 和 MEMORY 设置。我认为 OFF/MEMORY 会以牺牲数据库完整性为代价来提高写入性能,而 OFF 会完全禁用回滚。
6赞 Aaron Hudon 8/14/2015
对于#7,您是否有有关如何使用C# System.Data.SQLITE包装器启用共享页面缓存的示例?
6赞 Bill K 8/25/2016
#4 带回了很久以前的记忆——在以前至少有一个案例,在一组添加之前删除索引并在之后重新创建索引会显着加快插入速度。在现代系统上,对于某些添加,您知道在此期间您可以单独访问该表,可能仍然会更快地工作。
0赞 Basj 5/6/2020
@Snazzer也许你说的是?sqlite.org/pragma.html#pragma_synchronoussynchronous = NORMAL
73赞 4 revs, 3 users 58%fearless_fool #3

在批量刀片上

受到这篇文章的启发,以及将我引向这里的 Stack Overflow 问题——是否可以在 SQLite 数据库中一次插入多行? -- 我已经发布了我的第一个 Git 存储库:

https://github.com/rdpoor/CreateOrUpdate

将 ActiveRecords 数组批量加载到 MySQL、SQLite 或 PostgreSQL 数据库中。它包括一个选项,用于忽略现有记录、覆盖它们或引发错误。我的基本基准测试显示,与顺序写入相比,速度提高了 10 倍 -- YMMV。

我在生产代码中使用它,我经常需要导入大型数据集,我对它非常满意。

评论

4赞 Alix Axel 10/15/2013
@Jess:如果你点击链接,你会看到他的意思是批量插入语法。
1赞 fearless_fool 1/28/2021
@afaulconbridge:可能也一样:我怀疑你会获得类似的加速 - 但更安全,更容易 - 只需将你的插入物包装在单个交易中。
0赞 Leroy Scandal 1/23/2023
我同意这些猫,你有 28MB 的数据?我有 130 TB 的二进制数据,
130赞 3 revs, 3 users 68%ahcox #4

避免sqlite3_clear_bindings(stmt)。

测试中的代码每次都设置绑定,通过这些绑定应该就足够了。

SQLite文档中的C API介绍说:

在首次调用 sqlite3_step() 之前或立即调用 sqlite3_reset() 之后,应用程序可以调用 sqlite3_bind() 接口将值附加到参数。每 调用 sqlite3_bind() 会覆盖同一参数上的先前绑定

文档中没有任何内容sqlite3_clear_bindings除了简单地设置绑定之外,还必须调用它。

更多详情:Avoid_sqlite3_clear_bindings()

评论

8赞 Francis Straccia 11/2/2016
非常正确:“与许多人的直觉相反,sqlite3_reset()不会重置准备好的语句的绑定。使用此例程将所有主机参数重置为 NULL sqlite.org/c3ref/clear_bindings.html
53赞 4 revs, 4 users 78%malkia #5

如果您只关心读取,那么更快(但可能会读取过时的数据)版本是从多个线程(每个线程的连接)读取多个连接。

首先在表中找到项目:

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(因为它是全局为我们启用的)

175赞 3 revs, 3 users 45%Alexander Farber #6

尝试对这些插入物使用 SQLITE_STATIC 而不是 SQLITE_TRANSIENT

SQLITE_TRANSIENT将导致SQLite在返回之前复制字符串数据。

SQLITE_STATIC告诉它,在执行查询之前,您提供给它的内存地址将有效(在此循环中始终如此)。这将为每个循环节省多个分配、复制和解除分配操作。可能是一个很大的改进。

36赞 2 revs, 2 users 67%Cristian Ciupitu #7

我无法从交易中获得任何收益,直到我将cache_size提高到更高的价值,即PRAGMA cache_size=10000;

评论

2赞 vgru 1/23/2020
请注意,使用正值 设置要缓存的页数,而不是总 RAM 大小。使用默认页面大小为 4kB 时,此设置将为每个打开的文件(或每个进程,如果使用共享缓存运行)最多保存 40MB 的数据。cache_size
28赞 5 revs, 2 users 91%Jimmy_A #8

阅读本教程后,我尝试将其实现到我的程序中。

我有 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 分钟)。如果遇到插入速度慢的情况,请检查您的硬盘驱动器是否配置了这些选项。它将为您节省大量时间和麻烦,试图找到解决方案

评论

1赞 rouzier 2/7/2019
我将提出以下建议。* 使用 SQLITE_STATIC 与 SQLITE_TRANSIENT 避免字符串复制 您必须确保在执行交易之前不会更改字符串 * 使用批量插入 INSERT INTO stop_times值 (NULL, ?, ?, ?, ?, ?, ?, ?, ?, ?), (NULL, ?, ?, ?, ?, ?, ?, ?, ?, ?), (NULL, ?, ?, ?, ?, ?, ?, ?, ?, ?),(NULL, ?, ?, ?, ?, ?, ?, ?, ?, ?), (NULL, ?, ?, ?, ?, ?, ?, ?, ?, ?) * mmap 文件以减少系统调用的次数。
2赞 rouzier 2/7/2019
这样做,我能够在 11.51 秒内导入 5,582,642 条记录
18赞 5 revs, 4 users 62%doesnt_matter #9

您的问题的答案是较新的 SQLite 3 提高了性能,请使用它。

这个答案 为什么使用 sqlite 的 SQLAlchemy 插入比直接使用 sqlite3 慢 25 倍? 作者:SqlAlchemy Orm 作者在 100 秒内插入了 0.5k,我在 python-sqlite 和 SqlAlchemy 中看到了类似的结果。这让我相信 SQLite 3 的性能有所提高。

-2赞 vishnuc156 #10

使用 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”部分了解更多详细信息

评论

0赞 Mooing Duck 9/19/2023
这个问题是关于 C++ sqlite 的,不是特定于 Android 的
0赞 Mooing Duck 9/19/2023
大多数 ContentProviders 没有正确实现,而且它们实际上并不比仅仅调用循环:(快bulkInsertinsert
2赞 stonux #11

像@Jimmy_A那样将任务拆分为多个事务是要走的路。否则,您可能会因怪物事务和繁重的 COMMIT 任务而使 RAM 饱和。

为了进一步调整性能,您还可以在硬盘驱动器上启用回写缓存,前提是您使用某种电池备份系统(笔记本电脑、UPS、带电池的 RAID 控制器......

2赞 Arundale Ramanathan #12

在我的情况下,使用 s 的速度增加了一倍,因为在内部它与此处建议的批处理 INSERTS 相同。PRAGMA journal_mode = WALINSERT

就我而言,我需要将数据导入索引,而不仅仅是表。SQLite具有WITHOUT ROWID的出色功能,它允许将表和索引组合在一起。默认情况下,SQLite中的表也是B树,任何索引都存储在单独的B树页面中。使用 WITHOUT ROWID 时,表和索引仅使用一个 B 树。

我还使用了技术,它应该防止SQLite以牺牲数据库大小为代价来提高空间利用率,但它似乎对性能没有任何明显的影响。PRAGMA auto_vacuum = 0

虽然我的情况与OP的要求略有不同,但使用WAL的第一个建议应该对他的情况有所影响。