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

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

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

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

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

Когда элемент уже существует, он обновляет «счетчик» с помощью запроса на обновление (кажется, это называется 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 ГБ.

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

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


person Pedro Alves    schedule 06.03.2013    source источник
comment
Насколько большой стол?   -  person CL.    schedule 06.03.2013
comment
@КЛ. 35 миллионов строк, каждая основная запись представляет собой строку из 30+ символов.   -  person Pedro Alves    schedule 06.03.2013


Ответы (4)


Я буду:

  • подготовьте заявления (если вы еще этого не делаете)
  • уменьшить количество INSERT на транзакцию (10 секунд = 500 000 звучит уместно)
  • используйте 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 помогает в 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 = ?;

Пиковое значение ~ 109 тыс. обновлений в секунду.

Тест №2:

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

Пиковое значение составляло ~120 000 обновлений в секунду.


Я также попробовал 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.

Кажется, вы определяете свой PK как NOT NULL + UNIQUE. PK неявно является УНИКАЛЬНЫМ.

person Alix Axel    schedule 12.05.2013

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

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

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

Используйте sqlite3_status() с SQLITE_STATUS_MEMORY_USED, чтобы проследить распределение динамической памяти и найти узкое место.

person Peixu Zhu    schedule 09.03.2013
comment
Я уже пытался уменьшить количество транзакций до 1М. Он по-прежнему потребляет ту же оперативную память, но становится намного медленнее. - person Pedro Alves; 10.03.2013
comment
предполагается, что PRAGMA shrink_memory подключается к базе данных, чтобы освободить как можно больше памяти. - person Peixu Zhu; 10.03.2013
comment
ваш комментарий заставил меня понять, что я использую старую версию sqlite3. У меня была версия 3.7.9, которая не поддерживает PRAGMA shrink_memory или sqlite3_db_release_memory() . Спасибо за это =) Используя PRAGMA shrink_memory + sqlite3_db_memory_release(), я вижу, что потребление памяти очень быстро падает при вызове этой функции. Я думаю, что это просто освобождает весь кеш и память кучи, потому что производительность вставки также падает до тех пор, пока кеш снова не заполнится, а потребление памяти не вернется к тому, что было раньше. Это не идеально, но это может быть полезным решением =) - person Pedro Alves; 10.03.2013

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

  • около 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 читается с помощью sqlite3_last_insert_rowid.

Выполнение отдельных SELECT и INSERT не медленнее, чем INSERT OR IGNORE; SQLite делает то же самое в любом случае.

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

person CL.    schedule 06.03.2013
comment
Не работает. Стало медленнее и потребление памяти выше. - person Pedro Alves; 07.03.2013

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

Повысить производительность INSERT в секунду для SQLite?

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

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

      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

person Tim    schedule 15.03.2013