关于C#:提高sqlite的每秒插入性能?

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
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);

Imported 864913 records in 38.03
seconds

那就更好了。只需在一个事务中包装所有插入,就可以将性能提高到每秒23000个插入。

使用准备好的语句

使用事务是一个巨大的改进,但是如果我们一遍又一遍地使用相同的SQL,为每个插入重新编译SQL语句就没有意义。让我们使用sqlite3_prepare_v2编译一次SQL语句,然后使用sqlite3_bind_text将参数绑定到该语句:

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

好极了!有更多的代码(不要忘记调用sqlite3_clear_bindingssqlite3_reset),但是我们的性能提高了一倍多,达到每秒53000个插入。

pragma synchronous=关闭

默认情况下,SQLite将在发出操作系统级写入命令后暂停。这样可以保证数据被写入磁盘。通过设置synchronous = OFF,我们指示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=内存

考虑通过评估PRAGMA journal_mode = MEMORY将回滚日志存储在内存中。您的事务将更快,但如果您在事务过程中断电或程序崩溃,数据库可能会处于损坏状态,并且事务部分完成:

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

让我们结合前面的两个优化。这有点危险(如果是


几个提示:

  • 在事务中放置插入/更新。
  • 对于旧版本的sqlite,考虑一种不那么偏执的日志模式(pragma journal_mode)。有NORMAL,然后有OFF,如果您不太担心操作系统崩溃时数据库可能会损坏,那么可以显著提高插入速度。如果应用程序崩溃,那么数据应该是正常的。请注意,在较新版本中,OFF/MEMORY设置对于应用程序级崩溃不安全。
  • 使用页面大小也会有所不同(PRAGMA page_size)。拥有较大的页面大小可以使读写速度更快,因为较大的页面保存在内存中。请注意,您的数据库将使用更多的内存。
  • 如果您有索引,那么在完成所有插入之后,可以考虑调用CREATE INDEX。这比创建索引然后插入要快得多。
  • 如果您同时访问sqlite,则必须非常小心,因为在完成写入操作时,整个数据库将被锁定,并且尽管可能有多个读卡器,但写入操作将被锁定。在新的sqlite版本中添加了一个wal,这在一定程度上得到了改进。
  • 利用节省空间…更小的数据库更快。例如,如果您有键值对,请尝试将该键设置为INTEGER PRIMARY KEY(如果可能),这将替换表中隐含的唯一行号列。
  • 如果您使用多个线程,可以尝试使用共享页缓存,这将允许在线程之间共享加载的页,从而避免昂贵的I/O调用。
  • 不要使用!feof(file)
  • 我也在这里和这里问过类似的问题。


    尝试使用SQLITE_STATIC而不是SQLITE_TRANSIENT来插入这些插件。

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

    SQLITE_STATIC告诉它,在执行查询之前,您提供的内存地址是有效的(在这个循环中总是这样)。这将为每个循环保存几个分配、复制和解除分配操作。可能有很大的改进。


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


    我不能从交易中获得任何收益,除非我将缓存大小提高到更高的值,即PRAGMA cache_size=10000;


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

    我有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部分进行检查