确保SQLite3中唯一行的有效方法

时间:2011-03-03 14:20:38

标签: sql performance sqlite insert

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

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

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中的一些设置(除了永久强制DB进入异步操作之外的任何东西,或者是在我的应用程序代码中以编程方式)?例如,有没有办法确保行唯一性而不使用索引?

BOUNTY:

通过使用我自己的答案中描述的散列/索引方法,我设法将性能降低到一定程度,以至于我的应用程序可能接受它。 但是,似乎随着表中行数的增加,索引的存在会使插入越来越慢。

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

5 个答案:

答案 0 :(得分:15)

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

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

如果您尝试这些,请发布您的测试结果。我相信每个人都会感兴趣。

答案 1 :(得分:8)

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

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

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

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

现在真正重要的东西。 。

你做测试真是太好了。大多数开发人员不这样做。但我认为你的测试结果非常误导。

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

如果没有给出正确的答案,我可以让它真的非常快。

要获得插入没有密钥的表的有意义的时间,您需要

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

当然,这些流程所需的时间也必须考虑在内。

FWIW,我通过在1000个语句的事务中将100K SQL插入语句运行到您的模式中进行了测试,并且只花了30秒。 1000个插入语句的单个事务,似乎是您在生产中所期望的,花了149毫秒。

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

答案 2 :(得分:4)

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

复合主键的主要问题是索引的处理方式。复合键意味着复合值的索引,在我的情况下意味着索引字符串。虽然比较字符串值并不那么慢,但索引长度为500字节的值意味着索引中的B树节点可以比索引64-的B树更少的行/节点指针。位整数值。这意味着每个索引搜索都会加载更多的DB页面,因为B树的高度会增加。

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

  • 它使用WAL mode。性能提升肯定值得这么小的变化,因为我没有任何问题,DB文件不是自包含的。

  • 我使用MurmurHash3哈希函数 - 在用C重新编写它并调整它之后 - 从形成密钥的字段的值产生一个32位哈希值。我将此哈希存储在新的索引列中。由于这是一个整数值,因此索引非常快。这是此表的唯一索引。由于表中最多有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的速度。将数据库页面大小增加到8192字节似乎有所帮助,但并不像我想的那样大幅提升。

答案 3 :(得分:3)

Case When Exists((Select ID From Table Where Fld0 = value0 and Fld2 = value1 and Fld3 = value 2)) Then
    --Insert Statement
End

我不是100%认为插件的工作方式与SQLite相同,但我认为应该如此。这对Where字段的正确索引应该相当快。然而,这是需要考虑的两个交易。

答案 4 :(得分:3)

除了所有其他好的答案之外,您可以做的一件事是将数据分成几个表。

随着行数的增加,SQLite INSERT变得越来越慢,但是如果你可以将一个表拆分成几个那些效果减弱的表(例如:“names” - >“names_a”,“names_b”,......对于以字母x开头的名称。稍后,您可以CREATE VIEW "names" AS SELECT * FROM "names_a" UNION SELECT * FROM "names_b" UNION ...