【问题标题】:How to reduce SQLite memory consumption?如何减少 SQLite 内存消耗?
【发布时间】:2023-03-18 02:32:01
【问题描述】:

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

在每次执行时,它都会创建一个具有以下架构的表:

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

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

当一个项目已经存在时,它会使用更新查询更新“计数”(我认为这称为 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(默认页面大小)才能达到理想的性能。

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

【问题讨论】:

  • 桌子有多大?
  • @CL。 35M 行,每个主条目是一个包含 30 多个字符的字符串。

标签: c performance sqlite memory-consumption


【解决方案1】:

假设一个事务中的所有操作都分布在整个表中,使得表的所有页都需要被访问,那么工作集的大小为:

  • 大约 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 中,插入的rowid 被读取为sqlite3_last_insert_rowid

做一个单独的SELECTINSERT 并不比INSERT OR IGNORE 慢; SQLite 在这两种情况下都做同样的工作。

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

【讨论】:

  • 没用。它变得更慢,内存消耗更高。
【解决方案2】:

看来内存消耗高可能是因为太多的操作集中在一个大事务上。尝试提交较小的事务(如每 1M 操作)可能会有所帮助。每个事务 5M 操作消耗太多内存。

但是,我们会平衡运行速度和内存使用量。

如果较小的交易不是一个选项,PRAGMA shrink_memory 可能是一个选择。

使用sqlite3_status()SQLITE_STATUS_MEMORY_USED 跟踪动态内存分配并定位瓶颈。

【讨论】:

  • 我已经尝试将事务减少到 1M。它仍然消耗相同的 RAM,但变得更慢。
  • 有一个PRAGMA shrink_memory 应该建立数据库连接以释放尽可能多的内存。
  • 您的评论让我意识到我使用的是旧版本的 sqlite3。我有 3.7.9,不支持 PRAGMA shrink_memory 或 sqlite3_db_release_memory() 。谢谢你 =) 使用 PRAGMA shrink_memory + sqlite3_db_memory_release(),我看到调用这个函数时内存消耗下降得非常快。我认为这只会释放所有缓存和堆内存,因为插入性能也会下降,直到缓存再次被填满并且内存消耗恢复到更接近以前的水平。它并不完美,但这可能是一个有用的解决方案 =)
  • 运行sqlite3_status();时出现语法错误
【解决方案3】:

本着头脑风暴的精神,我将冒险回答。我没有像这个家伙做过任何测试:

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

【讨论】:

    【解决方案4】:

    我愿意:

    • 准备语句(如果您还没有这样做)
    • 降低每笔交易的 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(百万)将导致 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));
    

    每秒更新约 12 万次。


    我还尝试了PRAGMA temp_store = FILE;,但更新速度下降了大约每秒 1-2k。


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


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


    好的,这是另一个:

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

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

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

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2013-06-13
      • 1970-01-01
      • 2019-03-02
      • 2016-05-24
      相关资源
      最近更新 更多