Как уменьшить потребление памяти SQLite?

Я ищу способы уменьшить потребление памяти SQLite3 в моем приложении.

При каждом выполнении он создает таблицу со следующей схемой:

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

После этого база данных заполняется 50 тыс. Операций в секунду. Пиши только.

Когда элемент уже существует, он обновляет "count" с помощью запроса на обновление (я думаю, что это называется UPSERT). Это мои запросы:

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

Таким образом, с 5 миллионами операций на транзакцию я могу очень быстро записывать в БД.

Я действительно не забочусь о дисковом пространстве для этой проблемы, но у меня есть очень ограниченное пространство ОЗУ. Таким образом, я не могу тратить слишком много памяти.

С sqlite3_user_memory() сообщает, что его потребление памяти возрастает почти до 3 ГБ во время выполнения. Если я ограничу его до 2 ГБ с помощью sqlite3_soft_heap_limit64(), производительность операций с базой данных упадет почти до нуля, когда достигнет 2 ГБ.

Мне пришлось увеличить размер кэша до 1 МБ (размер страницы по умолчанию), чтобы достичь желаемой производительности.

Что я могу сделать, чтобы уменьшить потребление памяти?

4 ответа

Решение

Я мог бы:

  • подготовьте заявления (если вы еще этого не делаете)
  • уменьшите количество INSERT на транзакцию (10 сек = 500000 звуков)
  • использование PRAGMA locking_mode = EXCLUSIVE; если ты можешь

Кроме того, (я не уверен, если вы знаете) PRAGMA cache_size в страницах, а не в мегабайтах. Убедитесь, что вы определили свою целевую память как PRAGMA cache_size * PRAGMA page_size или в SQLite >= 3.7.10 вы также можете сделать PRAGMA cache_size = -kibibytes;, Установка его на 1 М (иллюзия) приведет к 1 или 2 ГБ.

Мне любопытно как cache_size помогает во ВСТАВКАХ, хотя...

Вы также можете попробовать и сравнительный тест, если 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 = ?;

Пик ~109к обновлений в секунду.

Тест № 2:

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

Пик на ~120 тыс. Обновлений в секунду.


Я тоже пробовал PRAGMA temp_store = FILE; и обновления сбрасываются на ~1-2к в секунду.


Для 7M обновлений в транзакции, journal_mode=WAL медленнее, чем все остальные.


Я заполнил базу данных 35 839 987 записями, и теперь моя установка занимает почти 4 секунды на каждый пакет из 65521 обновлений, однако он даже не достигает 16 МБ потребления памяти.


Хорошо, вот еще один:

Индексы на столбцах INTEGER PRIMARY KEY (не делайте этого)

Когда вы создаете столбец с INTEGER PRIMARY KEY, SQLite использует этот столбец в качестве ключа для (индексации) структуры таблицы. Это скрытый индекс (так как он не отображается в таблице SQLite_Master) для этого столбца. Добавление другого индекса для столбца не требуется и никогда не будет использоваться. Кроме того, это замедлит операции INSERT, DELETE и UPDATE.

Вы, кажется, определяете свой ПК как НЕ NULL + UNIQUE. ПК уникален неявно.

Кажется, что высокое потребление памяти может быть вызвано тем фактом, что слишком много операций сосредоточено в одной большой транзакции. Может помочь попытка совершить транзакцию меньшего размера, например, за 1М операций. 5 миллионов операций на транзакцию будут занимать много памяти.

Однако мы бы сбалансировали скорость работы и использование памяти.

Так как меньшая транзакция не помогает, PRAGMA shrink_memory может быть выбор.

С помощью sqlite3_status() с SQLITE_STATUS_MEMORY_USED отследить динамическое распределение памяти и найти точку.

Предполагая, что все операции в одной транзакции распределены по всей таблице, так что все страницы таблицы должны быть доступны, размер рабочего набора составляет:

  • около 1 ГБ для данных таблицы, плюс
  • около 1 ГБ для индекса на main колонка, плюс
  • около 1 ГБ для исходных данных всех страниц таблицы, измененных в транзакции (вероятно, все они).

Вы можете попытаться уменьшить объем данных, который изменяется для каждой операции, перемещая 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 читается с http://www.sqlite.org/c3ref/last_insert_rowid.html.

Делать отдельно SELECT а также INSERT не медленнее, чем INSERT OR IGNORE; SQLite делает ту же работу в любом случае.

Эта оптимизация полезна, только если большинство операций обновляют уже существующий счетчик.

В духе мозгового штурма я рискну ответить. Я не делал никаких испытаний, как этот парень:

Улучшить производительность SQLite INSERT-в-секунду?

Моя гипотеза заключается в том, что индекс в текстовом первичном ключе может быть более интенсивным в ОЗУ, чем пара индексов в двух целочисленных столбцах (что необходимо для имитации хэш-таблицы).

РЕДАКТИРОВАТЬ: На самом деле, вам даже не нужен первичный ключ для этого:

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

Целочисленный первичный ключ (или неуникальный индекс на слоте) не даст вам быстрого способа определить, было ли ваше текстовое значение уже в файле. Таким образом, чтобы удовлетворить это требование, вы можете попытаться реализовать то, что я предложил, для другого автора, имитируя хешированный ключ:

Оптимизация SQLite для миллионов записей?

Функция хэш-ключа позволит вам определить, где будет храниться текстовое значение, если оно существует.

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

Другие вопросы по тегам