【问题标题】:Efficient way to ensure unique rows in SQLite3确保 SQLite3 中唯一行的有效方法
【发布时间】:2024-01-22 10:39:02
【问题描述】:

我在我的一个项目中使用SQLite3,我需要确保插入到表中的行在它们的某些列的组合方面是唯一的。在大多数情况下,插入的行在这方面会有所不同,但如果匹配,新行必须更新/替换现有行。

显而易见的解决方案是使用复合主键,并带有冲突子句来处理冲突。因此:

CREATE TABLE Event (Id INTEGER, Fld0 TEXT, Fld1 INTEGER, Fld2 TEXT, Fld3 TEXT, Fld4 TEXT, Fld5 TEXT, Fld6 TEXT);

变成了这样:

CREATE TABLE Event (Id INTEGER, Fld0 TEXT, Fld1 INTEGER, Fld2 TEXT, Fld3 TEXT, Fld4 TEXT, Fld5 TEXT, Fld6 TEXT, PRIMARY KEY (Fld0, Fld2, Fld3) ON CONFLICT REPLACE);

这确实按照我的需要强制执行唯一性约束。不幸的是,这种变化也会导致性能损失,远远超出我的预期。我做了 使用sqlite3 命令行实用程序进行了一些测试,以确保我的其余代码没有错误。测试涉及输入 100,000 行,或者在单个 事务或 100 个事务,每个事务 1,000 行。我得到了以下结果:

                                | 1 * 100,000   | 10 * 10,000   | 100 * 1,000   |
                                |---------------|---------------|---------------|
                                | Time  | CPU   | Time  | CPU   | Time  | CPU   |
                                | (sec) | (%)   | (sec) | (%)   | (sec) | (%)   |
--------------------------------|-------|-------|-------|-------|-------|-------|
No primary key                  | 2.33  | 80    | 3.73  | 50    | 15.1  | 15    |
--------------------------------|-------|-------|-------|-------|-------|-------|
Primary key: Fld3               | 5.19  | 84    | 23.6  | 21    | 226.2 | 3     |
--------------------------------|-------|-------|-------|-------|-------|-------|
Primary key: Fld2, Fld3         | 5.11  | 88    | 24.6  | 22    | 258.8 | 3     |
--------------------------------|-------|-------|-------|-------|-------|-------|
Primary key: Fld0, Fld2, Fld3   | 5.38  | 87    | 23.8  | 23    | 232.3 | 3     |

我的应用程序目前最多执行 1,000 行事务,我对性能下降 15 倍感到惊讶。我预计吞吐量最多会下降 3 倍,CPU 使用率会上升,如 100k 事务案例所示。我猜想维护主键约束所涉及的索引需要大量的同步数据库操作,因此在这种情况下我的硬盘成为瓶颈。

使用WAL mode 确实有一些效果 - 性能提升约 15%。不幸的是,这还不够。 PRAGMA synchronous = NORMAL 好像没有任何效果。

可能可以通过增加事务大小来恢复一些性能,但我宁愿不这样做,因为内存使用量增加以及对响应能力和 可靠性。

每行中的文本字段的长度可变,平均约为 250 个字节。查询性能没有太大关系,但插入性能很重要。我的应用程序代码是用 C 语言编写的,并且(应该)至少可以移植到 Linux 和 Windows。

有没有办法在不增加事务大小的情况下提高插入性能? SQLite 中的某些设置(除了永久强制数据库进入异步操作之外的任何设置)还是在我的应用程序代码中以编程方式?例如,有没有办法在不使用索引的情况下确保行的唯一性?

赏金:

通过使用我自己的答案中描述的散列/索引方法,我设法在一定程度上将性能下降缓和到我的应用程序可能可以接受的程度。 然而,似乎随着表中行数的增加,索引的存在使得插入速度越来越慢。

我对任何可以提高此特定用例的性能的技术或微调设置感兴趣,只要它不涉及破解 SQLite3 代码或以其他方式导致项目变得不可维护。

【问题讨论】:

    标签: sql performance sqlite insert


    【解决方案1】:
    Case When Exists((Select ID From Table Where Fld0 = value0 and Fld2 = value1 and Fld3 = value 2)) Then
        --Insert Statement
    End
    

    我不是 100% 认为插入在 SQLite 中的工作方式与 SQLite 一样,但我认为应该如此。在Where 字段上进行适当的索引应该相当快。然而,这是需要考虑的两笔交易。

    【讨论】:

    • 从我在 SQLite 中可以找到的CASE 是一个表达式,而不是一个语句。我一直无法像这样使用它。你有我可以尝试的实际 SQL sn-p 吗?
    • @thkala 我得调查一下。
    【解决方案2】:

    ON CONFLICT REPLACE 子句将使 SQLite 删除现有行,然后插入新行。这意味着 SQLite 可能会花费一些时间

    • 删除现有行
    • 更新索引
    • 插入新行
    • 更新索引

    这是我的看法,基于 SQLite 文档和阅读其他数据库管理系统。没看源码。

    SQLite 有两种表达唯一性约束的方式:PRIMARY KEYUNIQUE。不过,它们都创建了一个索引。

    现在是真正重要的东西。 . .

    很高兴您进行了测试。大多数开发人员不这样做。但我认为您的测试结果具有严重的误导性。

    在您的情况下,将行插入到没有主键的表中的速度并不重要。没有主键的表无法满足您对数据完整性的基本要求。这意味着您不能依靠您的数据库来为您提供正确的答案。

    如果它不需要给出正确的答案,我可以做得非常非常快。

    要获得一个有意义的时间来插入一个没有键的表,你需要

    • 在插入新数据之前运行代码 以确保您不违反 未声明的主键约束, 并确保您更新现有的 具有正确值的行(而不是 插入),或
    • 在插入之后运行代码 清除重复项的表 (Fld0, Fld2, Fld3),并进行调和 冲突

    当然,这些过程所花费的时间也必须考虑在内。

    FWIW,我通过在 1000 条语句的事务中将 100K SQL 插入语句运行到您的架构中进行了测试,只用了 30 秒。 1000 个插入语句的单个事务,这似乎是您在生产中所期望的,耗时 149 毫秒。

    也许你可以通过插入一个无键的临时表来加快速度,然后从中更新有键的表。

    【讨论】:

    • 实际上我的测试确实有意义:项目的原始要求(以及所有性能测试)是数据库将存储所有行。这在中途发生了变化(不是我的错,我发誓:-)),因此我试图找到一种方法来处理这种变化,而无需从头开始,这就是我的替代解决方案的意思。
    • 如果不能严格替换数据,ON CONFLICT IGNORE 可能是一种增强功能。
    【解决方案3】:

    (我通常不回答自己的问题,但我想记录一些想法/部分解决方案。)

    复合主键的主要问题是处理索引的方式。复合键意味着复合值的索引,在我的例子中意味着索引字符串。虽然比较字符串值并没有那么慢,但索引一个长度为 500 字节的值意味着索引中的 B-tree 节点可以容纳的行/节点指针比索引 64 字节的 B-tree 少得多。位整数值。这意味着随着 B 树的高度增加,每次索引搜索加载更多的数据库页面。

    为了解决这个问题,我修改了我的代码:

    • 它使用WAL mode。性能提升当然值得这么小的改动,因为我没有任何关于数据库文件不是独立的问题。

    • 我使用了 MurmurHash3 哈希函数 - 在用 C 重写并调整它之后 - 从将形成键的字段的值中生成单个 32 位哈希值。我将此哈希存储在一个新的 indexed 列中。由于这是一个整数值,因此索引非常快。这是该表的唯一索引。由于表中最多有 10,000,000 行,因此散列冲突不会成为性能问题——尽管我不能真正认为散列值是UNIQUE,但在一般情况下,索引只会返回一行。

    目前,我已经编写了两个替代方案,目前正在进行测试:

    • DELETE FROM Event WHERE Hash=? AND Fld0=? AND Fld2=? AND Fld3=?,后跟INSERT

    • UPDATE Event SET Fld1=?,... WHERE Hash=? AND Fld0=? AND Fld2=? AND Fld3=?,如果没有更新行,则后跟 INSERT

    我希望第二种选择会更快,但我必须先完成测试。无论如何,似乎通过这些更改,性能下降(与原始无索引表相比)已减少到 5 倍左右,这更易于管理。

    编辑:

    此时我已经确定使用第二个变体,它确实稍微快一些。然而,似乎任何类型的索引都会随着索引表变大而显着降低 SQLite3 的速度。将 DB 页面大小增加到 8192 字节似乎有所帮助,但没有我想要的那么大。

    【讨论】:

      【解决方案4】:

      我使用 sqlite 在运行时插入数百万行,这是我用来提高性能的方法:

      • 使用尽可能少的事务。
      • 使用参数化命令 插入数据(准备 命令一次,只需更改 循环中的参数值)
      • 设置 PRAGMA synchronous 关闭(不确定 它如何与 WAL 一起工作)
      • 增加数据库的页面大小。
      • 增加缓存大小。这是一个重要的设置,因为它会导致 sqlite 将数据实际写入磁盘的次数更少,并且会在内存中运行更多的操作,从而使整个过程更快。
      • 如果需要索引,请在插入行后通过运行必要的 sqlite 命令添加它。在这种情况下,您需要像现在一样确保自己的唯一性。

      如果您尝试这些,请发布您的测试结果。我相信这对每个人来说都会很有趣。

      【讨论】:

      • 按顺序:更少的交易:已经完成,在合理范围内。 参数化语句:还有其他方法吗? :-)。 异步模式:无能为力 - 数据库损坏的风险太大。 更大的页面:已经完成,它确实在一定程度上提高了性能。 更大的缓存:已经过测试,它实际上会减慢速度。无论如何,它只对读取有帮助,对写入没有帮助。 事后索引:没办法 - 整个事情会不断更新。还有什么想法吗?每一点都有帮助......
      • @thkala:奇怪的是增加缓存没有帮助。在我的情况下,我可以看到内存使用量增加并且没有文件活动,而使用的内存少于缓存。在您的情况下,可能会导致 sqlite(可能是索引)在插入记录时忽略缓存。您可以使用进程监视器来检查是否有任何 IO 活动正在进行,而它应该使用缓存?
      • @Giorgi:只要索引页适合缓存,增加它的大小应该没关系:我只做插入,原子性约束已经强制一些fsync()(即磁盘写入)操作每笔交易。无论如何,操作系统缓存已经有助于读取......
      • @thkala:还有一个想法:您可以在内存数据库中使用 sqlite,因此所有操作都将在 RAM 中执行,而不是将所有更改写入磁盘。这里有一些链接:Saving to disk an in-memory databaseSynchronizing sqlite database from memory to file 也看看这个关于 sqlite 性能的问题:*.com/questions/1711631/…
      • @thkala:我看到了,但是在内存数据库中插入了 2k 次,而不是将其刷新到磁盘可以带来更好的性能。至少你可以试一试来衡量它的表现。
      【解决方案5】:

      除了所有其他出色的答案之外,您可以做的一件事是将数据分区到多个表中。

      随着行数的增加,SQLite INSERT 会变得越来越慢,但是如果您可以将一个表拆分为多个,则效果会减弱(例如:​​“names”->“names_a”、“names_b”...以字母x 开头的名称)。以后可以CREATE VIEW "names" AS SELECT * FROM "names_a" UNION SELECT * FROM "names_b" UNION ...

      【讨论】:

      • 如何计算(最优)分区大小(即一个分区中的行数)?
      • 我见过的大多数分区策略(跨多个数据库)在关键空间的方便点进行分区,而不是为了保持分区大小严格平衡。
      最近更新 更多