如何减少SQLite内存消耗?

时间:2013-03-06 18:41:43

标签: c performance sqlite memory-consumption

我正在寻找在我的应用程序中减少SQLite3内存消耗的方法。

在每次执行时,它都会创建具有以下模式的表:

(main TEXT NOT NULL PRIMARY KEY UNIQUE,count INTEGER DEFAULT 0)

之后,数据库每秒充满50k操作。只写。

当项目已经存在时,它会使用更新查询更新“count”(我认为这称为UPSERT)。这些是我的疑问:

INSERT OR IGNORE INTO table (main) VALUES (@SEQ);
UPDATE tables SET count=count+1 WHERE main = @SEQ;

这样,每个事务有500万次操作,我可以非常快速地写入数据库。

我并不关心这个问题的磁盘空间,但我有一个非常约束的RAM空间。因此,我不能浪费太多记忆。

使用sqlite3_user_memory()通知其执行期间内存消耗增长到近3GB。如果我通过sqlite3_soft_heap_limit64()将其限制为2GB,那么当达到2GB时,数据库操作性能几乎会降至零。

我必须将缓存大小提高到1M(默认页面大小)才能达到理想的性能。

我可以做些什么来减少内存消耗?

4 个答案:

答案 0 :(得分:11)

似乎高内存消耗可能是由于太多操作集中在一个大事务中。尝试像每1M操作一样提交较小的事务可能会有所帮助。每个事务5M操作将消耗许多内存。

但是,我们会平衡操作速度和内存使用情况。

由于较小的交易无效,PRAGMA shrink_memory可能是一种选择。

使用sqlite3_status()SQLITE_STATUS_MEMORY_USED来跟踪动态内存分配,并找到该点。

答案 1 :(得分:4)

假设一个事务中的所有操作都分布在整个表中,以便需要访问表的所有页面,工作集的大小为:

  • 表格数据约为1 GB,加上
  • main列上的索引大约为1 GB,加上
  • 约1 GB用于交易中所有表格页面的原始数据(可能全部都是)。

您可以尝试通过将count列移动到单独的表中来减少每个操作更改的数据量:

CREATE TABLE main_lookup(main TEXT NOT NULL UNIQUE, rowid INTEGER PRIMARY KEY);
CREATE TABLE counters(rowid INTEGER PRIMARY KEY, count INTEGER DEFAULT 0);

然后,对于每个操作:

SELECT rowid FROM main_lookup WHERE main = @SEQ;
if not exists:
    INSERT INTO main_lookup(main) VALUES(@SEQ);
    --read the inserted rowid
    INSERT INTO counters VALUES(@rowid, 0);
UPDATE counters SET count=count+1 WHERE rowid = @rowid;

在C中,插入的rowidsqlite3_last_insert_rowid一起阅读。

单独SELECTINSERT并不比INSERT OR IGNORE慢;在任何一种情况下,SQLite都会做同样的工作。

仅当大多数操作更新已存在的计数器时,此优化才有用。

答案 2 :(得分:3)

我会:

  • 准备陈述(如果你还没有这样做)
  • 降低每次交易的INSERT数量(10秒= 500,000个合适的声音)
  • 如果可以
  • ,请使用PRAGMA locking_mode = EXCLUSIVE;

另外,(我不知道你是否知道)PRAGMA cache_size是在页面中,而不是在MB中。确保以PRAGMA cache_size * PRAGMA page_size或SQLite> = 3.7.10定义目标内存,您也可以PRAGMA cache_size = -kibibytes;。将其设置为1 M(illion)将导致1或2 GB。

我很好奇cache_size如何在INSERT中帮助...

如果PRAGMA temp_store = FILE;有所作为,您也可以尝试进行基准测试。

当然,无论何时没有将数据库写入:

  • PRAGMA shrink_memory;
  • VACUUM;

根据您对数据库的处理方式,这些也可能有所帮助:

  • PRAGMA auto_vacuum = 1|2;
  • PRAGMA secure_delete = ON;

我使用以下编译指示进行了一些测试:

busy_timeout=0;
cache_size=8192;
encoding="UTF-8";
foreign_keys=ON;
journal_mode=WAL;
legacy_file_format=OFF;
synchronous=NORMAL;
temp_store=MEMORY;

测试#1:

INSERT OR IGNORE INTO test (time) VALUES (?);
UPDATE test SET count = count + 1 WHERE time = ?;

每秒峰值~109k更新。

测试#2:

REPLACE INTO test (time, count) VALUES
(?, coalesce((SELECT count FROM test WHERE time = ? LIMIT 1) + 1, 1));

每秒更新〜120k更新。


我也试过PRAGMA temp_store = FILE;,更新速度每秒下降〜1-2k。


对于事务中的7M更新,journal_mode=WAL比所有其他更新慢。


我填充了一个包含35,839,987条记录的数据库,现在我的设置每批65521次更新花费了近4秒 - 但是,它甚至没有达到16 MB的内存消耗。


好的,这是另一个:

  

INTEGER PRIMARY KEY列上的索引(不要这样做)

     

使用INTEGER PRIMARY KEY创建列时,SQLite会使用它   column作为表结构的(索引)键。这是隐藏的   此列上的索引(因为它不显示在SQLite_Master表中)。   不需要在列上添加另一个索引,也永远不会   用过的。此外,它将减慢INSERT,DELETE和UPDATE操作   下来。

您似乎将PK定义为NOT NULL + UNIQUE。 PK是隐含的UNIQUE。

答案 3 :(得分:1)

本着头脑风暴的精神,我会冒险回答。我还没有做过这样的测试:

Improve INSERT-per-second performance of SQLite?

我的假设是文本主键上的索引可能比两个整数列上的几个索引更加RAM密集(您需要模拟散列表)。

编辑:实际上,你甚至不需要主键:

      create table foo( slot integer, myval text, occurrences int);
      create index ix_foo on foo(slot);  // not a unique index

整数主键(或插槽上的非唯一索引)将使您无法快速确定文本值是否已存档。因此,为了满足这一要求,您可以尝试将我建议的内容实施到另一张海报,模拟一个哈希键:

SQLite Optimization for Millions of Entries?

如果文本值存在,则可以使用散列键函数来确定文本值的存储位置。

http://www.cs.princeton.edu/courses/archive/fall08/cos521/hash.pdf http://www.fearme.com/misc/alg/node28.html http://cs.mwsu.edu/~griffin/courses/2133/downloads/Spring11/p677-pearson.pdf