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

оптимизация SQLite сложно. Производительность объемной вставки приложения C может варьироваться от 85 вставок в секунду до более 96 000 вставок в секунду!

Справочная информация: мы используем SQLite как часть настольного приложения. У нас есть большие объемы данных конфигурации, хранящихся в XML-файлах, которые анализируются и загружаются в базу данных SQLite для дальнейшей обработки при инициализации приложения. SQLite не подходит для этой ситуации, потому что это быстро, это не требует специальной настройки, и база данных хранится на диске в виде одного файла.

обоснование: Первоначально я был разочарован выступлением, которое я видел. оказывается, что производительность SQLite может значительно отличаться (как для массовых вставок, так и для выбора) в зависимости от того, как настроена база данных и как вы используете API. Это был не тривиальный вопрос, чтобы выяснить, какие все варианты и методы были, поэтому я подумал разумно создать эту запись сообщества wiki, чтобы поделиться результатами с читателями переполнения стека, чтобы избавить других от проблем тех же исследований.

Эксперимент: вместо того, чтобы просто говорить о советах по производительности в общем смысле (т. е. "использовать транзакцию!"), я подумал, что лучше написать код C и на самом деле мера влияние различных вариантов. Начнем с простого. данные:

  • текстовый файл с разделителями табуляции 28 МБ (приблизительно 865 000 записей)полный транзитный график для города Торонто
  • моя тестовая машина-это 3.60 GHz P4 под управлением Windows XP.
  • код компилируется с помощью Visual C++ 2005 как "релиз" с "полной оптимизацией" (/Ox) и Favor Fast Code (/Ot).
  • я использую SQLite "Amalgamation", скомпилированный непосредственно в мое тестовое приложение. Этот Версия SQLite у меня немного старше (3.6.7), но я подозреваю, что эти результаты будут сопоставимы с последней версией (пожалуйста, оставьте комментарий, если вы думаете иначе).

давайте напишем код!

Код: простая программа C, которая читает текстовый файл строка за строкой, разбивает строку на значения, а затем вставляет данные в базу данных SQLite. В этой "базовой" версии кода создается база данных, но мы не будем вставлять данные:

/*************************************************************
    Baseline code to experiment with SQLite performance.

    Input data is a 28 MB TAB-delimited text file of the
    complete Toronto Transit System schedule/route info
    from http://www.toronto.ca/open/datasets/ttc-routes/

**************************************************************/
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <string.h>
#include "sqlite3.h"

#define INPUTDATA "C:TTC_schedule_scheduleitem_10-27-2009.txt"
#define DATABASE "c:TTC_schedule_scheduleitem_10-27-2009.sqlite"
#define TABLE "CREATE TABLE IF NOT EXISTS TTC (id INTEGER PRIMARY KEY, Route_ID TEXT, Branch_Code TEXT, Version INTEGER, Stop INTEGER, Vehicle_Index INTEGER, Day Integer, Time TEXT)"
#define BUFFER_SIZE 256

int main(int argc, char **argv) {

    sqlite3 * db;
    sqlite3_stmt * stmt;
    char * sErrMsg = 0;
    char * tail = 0;
    int nRetCode;
    int n = 0;

    clock_t cStartClock;

    FILE * pFile;
    char sInputBuf [BUFFER_SIZE] = "";

    char * sRT = 0;  /* Route */
    char * sBR = 0;  /* Branch */
    char * sVR = 0;  /* Version */
    char * sST = 0;  /* Stop Number */
    char * sVI = 0;  /* Vehicle */
    char * sDT = 0;  /* Date */
    char * sTM = 0;  /* Time */

    char sSQL [BUFFER_SIZE] = "";

    /*********************************************/
    /* Open the Database and create the Schema */
    sqlite3_open(DATABASE, &db);
    sqlite3_exec(db, TABLE, NULL, NULL, &sErrMsg);

    /*********************************************/
    /* Open input file and import into Database*/
    cStartClock = clock();

    pFile = fopen (INPUTDATA,"r");
    while (!feof(pFile)) {

        fgets (sInputBuf, BUFFER_SIZE, pFile);

        sRT = strtok (sInputBuf, "t");     /* Get Route */
        sBR = strtok (NULL, "t");            /* Get Branch */
        sVR = strtok (NULL, "t");            /* Get Version */
        sST = strtok (NULL, "t");            /* Get Stop Number */
        sVI = strtok (NULL, "t");            /* Get Vehicle */
        sDT = strtok (NULL, "t");            /* Get Date */
        sTM = strtok (NULL, "t");            /* Get Time */

        /* ACTUAL INSERT WILL GO HERE */

        n++;
    }
    fclose (pFile);

    printf("Imported %d records in %4.2f secondsn", n, (clock() - cStartClock) / (double)CLOCKS_PER_SEC);

    sqlite3_close(db);
    return 0;
}

"Управления"

запуск кода как есть фактически не выполняет никаких операций с базой данных, но это даст нам представление о том, насколько быстры операции ввода-вывода файлов raw C и обработки строк.

импортированные записи 864913 в 0.94 секунды!--23-->

великолепно! Мы можем делать 920,000 вставок в секунду, при условии, что мы на самом деле не делаем никаких вставок :-)


"Худший Сценарий"

мы собираемся создать строку SQL, используя значения, считанные из файла, и вызвать эту операцию SQL с помощью sqlite3_exec:

sprintf(sSQL, "INSERT INTO TTC VALUES (NULL, '%s', '%s', '%s', '%s', '%s', '%s', '%s')", sRT, sBR, sVR, sST, sVI, sDT, sTM);
sqlite3_exec(db, sSQL, NULL, NULL, &sErrMsg);

это будет медленно, потому что SQL будет скомпилирован в код VDBE для каждой вставки, и каждая вставка будет происходить в своей собственной транзакции. как медленно?

импортированные записи 864913 в 9933.61 секунды!--23-->

хлоп! 2 часа 45 минут! Вот только 85 вставок в секунду.

С помощью операции

по умолчанию SQLite будет оценивать каждый оператор INSERT / UPDATE в уникальной транзакции. При выполнении большого количества вставок рекомендуется обернуть операцию в транзакцию:

sqlite3_exec(db, "BEGIN TRANSACTION", NULL, NULL, &sErrMsg);

pFile = fopen (INPUTDATA,"r");
while (!feof(pFile)) {

    ...

}
fclose (pFile);

sqlite3_exec(db, "END TRANSACTION", NULL, NULL, &sErrMsg);

импортированные записи 864913 в 38.03 секунды!--23-->

что лучше. Просто упаковка всех наших вставок в одной транзакции улучшила нашу производительность до 23,000 вставок в секунду.

используя подготовленное заявление

использование транзакции было огромным улучшением, но перекомпиляция оператора SQL для каждой вставки не имеет смысла, если мы используем один и тот же SQL снова и снова. Давайте использовать sqlite3_prepare_v2 чтобы скомпилировать наш оператор SQL один раз, а затем привязать наши параметры к этому оператору, используя sqlite3_bind_text:

/* Open input file and import into the database */
cStartClock = clock();

sprintf(sSQL, "INSERT INTO TTC VALUES (NULL, @RT, @BR, @VR, @ST, @VI, @DT, @TM)");
sqlite3_prepare_v2(db,  sSQL, BUFFER_SIZE, &stmt, &tail);

sqlite3_exec(db, "BEGIN TRANSACTION", NULL, NULL, &sErrMsg);

pFile = fopen (INPUTDATA,"r");
while (!feof(pFile)) {

    fgets (sInputBuf, BUFFER_SIZE, pFile);

    sRT = strtok (sInputBuf, "t");   /* Get Route */
    sBR = strtok (NULL, "t");        /* Get Branch */
    sVR = strtok (NULL, "t");        /* Get Version */
    sST = strtok (NULL, "t");        /* Get Stop Number */
    sVI = strtok (NULL, "t");        /* Get Vehicle */
    sDT = strtok (NULL, "t");        /* Get Date */
    sTM = strtok (NULL, "t");        /* Get Time */

    sqlite3_bind_text(stmt, 1, sRT, -1, SQLITE_TRANSIENT);
    sqlite3_bind_text(stmt, 2, sBR, -1, SQLITE_TRANSIENT);
    sqlite3_bind_text(stmt, 3, sVR, -1, SQLITE_TRANSIENT);
    sqlite3_bind_text(stmt, 4, sST, -1, SQLITE_TRANSIENT);
    sqlite3_bind_text(stmt, 5, sVI, -1, SQLITE_TRANSIENT);
    sqlite3_bind_text(stmt, 6, sDT, -1, SQLITE_TRANSIENT);
    sqlite3_bind_text(stmt, 7, sTM, -1, SQLITE_TRANSIENT);

    sqlite3_step(stmt);

    sqlite3_clear_bindings(stmt);
    sqlite3_reset(stmt);

    n++;
}
fclose (pFile);

sqlite3_exec(db, "END TRANSACTION", NULL, NULL, &sErrMsg);

printf("Imported %d records in %4.2f secondsn", n, (clock() - cStartClock) / (double)CLOCKS_PER_SEC);

sqlite3_finalize(stmt);
sqlite3_close(db);

return 0;

импортированные записи 864913 в 16.27 секунды!--23-->

приятно! Там немного больше кода (не забудьте позвонить sqlite3_clear_bindings и sqlite3_reset), но мы более чем удвоили нашу производительность 53,000 вставок в секунду.

Pragma synchronous = OFF

по умолчанию SQLite приостанавливается после выполнения команды записи на уровне ОС. Это гарантирует, что данные будут записаны на диск. Установив synchronous = OFF, мы инструктируем SQLite просто передать данные в ОС для записи, а затем продолжить. Существует вероятность того, что файл базы данных может быть поврежден, если компьютер терпит катастрофический сбой (или сбой питания), прежде чем данные будут записаны на блюдо:

/* Open the database and create the schema */
sqlite3_open(DATABASE, &db);
sqlite3_exec(db, TABLE, NULL, NULL, &sErrMsg);
sqlite3_exec(db, "PRAGMA synchronous = OFF", NULL, NULL, &sErrMsg);

импортированные записи 864913 в 12.41 секунды!--23-->

улучшения теперь меньше, но мы до 69,600 вставки в второй.

PRAGMA journal_mode = память

рассмотрите возможность хранения журнала отката в памяти, оценив PRAGMA journal_mode = MEMORY. Ваша транзакция будет быстрее, но если вы потеряете питание или ваша программа выйдет из строя во время транзакции, вы можете оставить базу данных в поврежденном состоянии с частично завершенной транзакцией:

/* Open the database and create the schema */
sqlite3_open(DATABASE, &db);
sqlite3_exec(db, TABLE, NULL, NULL, &sErrMsg);
sqlite3_exec(db, "PRAGMA journal_mode = MEMORY", NULL, NULL, &sErrMsg);

импортированные записи 864913 в 13.50 секунды!--23-->

немного медленнее, чем предыдущий оптимизация на 64,000 вставок в секунду.

Pragma synchronous = OFF и PRAGMA journal_mode = память

давайте объединим предыдущие две оптимизации. Это немного более рискованно (в случае сбоя), но мы просто импортируем данные (не запускаем банк):

/* Open the database and create the schema */
sqlite3_open(DATABASE, &db);
sqlite3_exec(db, TABLE, NULL, NULL, &sErrMsg);
sqlite3_exec(db, "PRAGMA synchronous = OFF", NULL, NULL, &sErrMsg);
sqlite3_exec(db, "PRAGMA journal_mode = MEMORY", NULL, NULL, &sErrMsg);

импортированные записи 864913 в 12.00 секунды!--23-->

фантастика! Мы в состоянии сделать 72,000 вставки в второй.

использование базы данных в памяти

просто для удовольствия, давайте построим на всех предыдущих оптимизациях и переопределим имя файла базы данных, чтобы мы работали полностью в ОЗУ:

#define DATABASE ":memory:"

импортированные записи 864913 в 10.94 секунды!--23-->

это не супер-практично хранить нашу базу данных в ОЗУ, но впечатляет, что мы можем выполнить 79,000 вставки в второй.

Рефакторинг Кода C

хотя и не специально улучшение SQLite, мне не нравится extra char* операции назначения в while петли. Давайте быстро рефакторинг кода, чтобы пройти вывод strtok() непосредственно в sqlite3_bind_text(), и пусть компилятор попытается ускорить работу для нас:

pFile = fopen (INPUTDATA,"r");
while (!feof(pFile)) {

    fgets (sInputBuf, BUFFER_SIZE, pFile);

    sqlite3_bind_text(stmt, 1, strtok (sInputBuf, "t"), -1, SQLITE_TRANSIENT); /* Get Route */
    sqlite3_bind_text(stmt, 2, strtok (NULL, "t"), -1, SQLITE_TRANSIENT);    /* Get Branch */
    sqlite3_bind_text(stmt, 3, strtok (NULL, "t"), -1, SQLITE_TRANSIENT);    /* Get Version */
    sqlite3_bind_text(stmt, 4, strtok (NULL, "t"), -1, SQLITE_TRANSIENT);    /* Get Stop Number */
    sqlite3_bind_text(stmt, 5, strtok (NULL, "t"), -1, SQLITE_TRANSIENT);    /* Get Vehicle */
    sqlite3_bind_text(stmt, 6, strtok (NULL, "t"), -1, SQLITE_TRANSIENT);    /* Get Date */
    sqlite3_bind_text(stmt, 7, strtok (NULL, "t"), -1, SQLITE_TRANSIENT);    /* Get Time */

    sqlite3_step(stmt);        /* Execute the SQL Statement */
    sqlite3_clear_bindings(stmt);    /* Clear bindings */
    sqlite3_reset(stmt);        /* Reset VDBE */

    n++;
}
fclose (pFile);

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

импортированные записи 864913 в 8.94 секунды!--23-->

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


резюме (so далеко)

надеюсь, ты все еще со мной! причина, по которой мы начали этот путь, заключается в том, что производительность массовой вставки сильно варьируется с SQLite, и не всегда очевидно, какие изменения необходимо внести, чтобы ускорить нашу работу. Используя тот же компилятор (и параметры компилятора), ту же версию SQLite и те же данные, мы оптимизировали наш код и наше использование SQLite для go от наихудшего сценария 85 вставок в секунду до более 96 000 вставок в секунду второй!


CREATE INDEX затем INSERT vs. INSERT затем CREATE INDEX

прежде чем мы начнем измерения SELECT производительность, мы знаем, что мы будем создавать индексы. В одном из ответов ниже было предложено, что при выполнении массовых вставок быстрее создавать индекс после вставки данных (в отличие от создания индекса сначала, а затем вставки данных). Попробуем:

создать индекс после вставки Данные

sqlite3_exec(db, "CREATE  INDEX 'TTC_Stop_Index' ON 'TTC' ('Stop')", NULL, NULL, &sErrMsg);
sqlite3_exec(db, "BEGIN TRANSACTION", NULL, NULL, &sErrMsg);
...

импортированные записи 864913 в 18.13 секунды!--23-->

вставьте данные, затем создайте индекс

...
sqlite3_exec(db, "END TRANSACTION", NULL, NULL, &sErrMsg);
sqlite3_exec(db, "CREATE  INDEX 'TTC_Stop_Index' ON 'TTC' ('Stop')", NULL, NULL, &sErrMsg);

импортированные записи 864913 в 13.66 секунды!--23-->

как и ожидалось, массовые вставки медленнее, если один столбец индексируется, но это имеет значение, если индекс создается после вставки данных. Наша базовая линия no-index составляет 96,000 вставок в секунду. создание индекса сначала, а затем вставка данных дает нам 47,700 вставок в секунду, тогда как вставка данных сначала, а затем создание индекса дает нам 63,300 вставок в секунду.


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

9 ответов


несколько советов:

  1. поместите вставки / обновления в транзакцию.
  2. для более старых версий SQLite-рассмотрим менее параноидальный режим журнала (pragma journal_mode). Есть NORMAL, и есть OFF, что может значительно увеличить скорость вставки, если вы не слишком беспокоитесь о базе данных, возможно, поврежден, если ОС аварийно завершает работу. Если ваше приложение аварийно завершает работу, данные должны быть в порядке. Обратите внимание, что в более новых версиях OFF/MEMORY настройки не безопасны для сбой уровня приложения.
  3. игра с размерами страниц также имеет значение (PRAGMA page_size). Наличие больших размеров страниц может сделать чтение и запись немного быстрее, так как большие страницы хранятся в памяти. Обратите внимание, что для вашей базы данных будет использоваться больше памяти.
  4. если у вас есть индексы, подумайте о вызове CREATE INDEX после выполнения всех своих вставок. Это значительно быстрее, чем создание индекса, а затем выполнение вставок.
  5. вы должны быть очень осторожны, если вы иметь параллельный доступ к SQLite, так как вся база данных блокируется при записи, и хотя возможно несколько читателей, записи будут заблокированы. Это было несколько улучшено с добавлением WAL в более новых версиях SQLite.
  6. воспользуйтесь преимуществами экономии места...меньшие базы данных работают быстрее. Например, если у вас есть пары значений ключей, попробуйте сделать ключ INTEGER PRIMARY KEY если возможно, который заменит подразумеваемый уникальный столбец номера строки в таблица.
  7. если вы используете несколько потоков, вы можете попробовать использовать общий кэш страницы, что позволит совместно использовать загруженные страницы между потоками, что позволит избежать дорогостоящих вызовов ввода-вывода.
  8. не используйте !feof(file)!

Я также задавал подобные вопросы здесь и здесь.


попробуйте использовать SQLITE_STATIC вместо SQLITE_TRANSIENT для тех, кто вставляет.

SQLITE_TRANSIENT заставит SQLite скопировать строковые данные перед возвратом.

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


избегайте sqlite3_clear_bindings (stmt);

код в тесте устанавливает привязки каждый раз, через которые должно быть достаточно.

вступление C API из документов SQLite говорит

перед вызовом sqlite3_step () в первый раз или сразу после sqlite3_reset(), приложение может вызвать один из sqlite3_bind () интерфейсы для присоединения значений к параметрам. Каждый вызов sqlite3_bind () переопределяет предыдущие привязки по тому же параметру

(см.:sqlite.org/cintro.html). В документах нет ничего для функции говоря, что вы должны вызвать его в дополнение к простой установке Привязок.

Подробнее: http://www.hoogli.com/blogs/micro/index.html#Avoid_sqlite3_clear_bindings()


на массовые вставки

вдохновленный этим сообщением и вопросом переполнения стека, который привел меня сюда -- можно ли вставлять несколько строк одновременно в базу данных SQLite? -- я опубликовал свой первый Git репозитория:

https://github.com/rdpoor/CreateOrUpdate

который навалом загружает массив ActiveRecords в в MySQL, SQLite или PostgreSQL базы данных. Он включает возможность игнорировать существующие записи, перезаписывать их или вызывать ошибку. Мои рудиментарные тесты показывают улучшение скорости 10x по сравнению с последовательными записями -- YMMV.

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


массовый импорт, кажется, работает лучше, если вы можете откусить свой ВСТАВИТЬ / ОБНОВИТЬ заявления. Значение 10,000 или около того хорошо сработало для меня на таблице с несколькими строками, YMMV...


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

сначала найдите элементы в таблице:

 SELECT COUNT(*) FROM table

затем прочитайте на страницах (ограничение / смещение)

  SELECT * FROM table ORDER BY _ROWID_ LIMIT <limit> OFFSET <offset>

где и рассчитываются по потоку, например:

int limit = (count + n_threads - 1)/n_threads;

для каждого потока:

int offset = thread_index * limit

для нашего небольшого дБ (200Мб) это сделало скорость-вверх 50-75% (3.8.0.2 64-разрядная версия Windows 7). Наши таблицы сильно ненормализованы (1000-1500 столбцов, примерно 100 000 или более строк).

слишком много или слишком мало потоков не сделают этого, вам нужно проверить и профилировать себя.

также для нас SHAREDCACHE сделал производительность медленнее, поэтому я вручную поставил PRIVATECACHE (потому что он был включен глобально для нас)


Я не мог получить никакой выгоды от транзакций, пока я не поднял cache_size до более высокого значения, т. е. PRAGMA cache_size=10000;


после прочтения этого урока я попытался реализовать его в своей программе.

У меня есть 4-5 файлов, которые содержат адреса. Каждый файл имеет около 30 миллионов записей. Я использую ту же конфигурацию, которую вы предлагаете, но мое количество вставок в секунду является низким (~10.000 записей в секунду).

вот где ваше предложение терпит неудачу. Вы используете одну транзакцию для всех записей и одну вставку без ошибок/сбоев. Допустим, вы разделяете друг друга. запись в несколько вставок на разных таблицах. Что произойдет, если рекорд будет побит?

команда ON CONFLICT не применяется, потому что если у вас есть 10 элементов в записи, и вам нужно, чтобы каждый элемент был вставлен в другую таблицу, если элемент 5 получает ошибку ограничения, то все предыдущие 4 вставки тоже должны идти.

Итак, вот где происходит откат. Единственная проблема с откатом заключается в том, что вы теряете все свои вставки и начинаете сверху. Как можно решить это?

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

Это решение помогло мне обойти проблемы у меня при работе с файлами, содержащими плохие / дубликаты записей (у меня было почти 4% плохих записей).

алгоритм, который я создал, помог мне сократить мой процесс на 2 часа. Окончательный процесс загрузки файла 1hr 30m, который все еще медленный, но не по сравнению с 4hrs, которые он первоначально взял. Мне удалось ускорить вставки с 10.000/s до ~14.000 / s

Если у кого-то есть другие идеи о том, как ускорить его, я открыт для предложений.

обновление:

In В дополнение к моему ответу выше, вы должны иметь в виду, что вставки в секунду в зависимости от жесткого диска вы используете тоже. Я тестировал его на 3 разных ПК с разными жесткими дисками и получил огромные различия во времени. PC1 (1hr 30m), PC2 (6hrs) PC3 (14hrs), поэтому я начал задаваться вопросом, почему это будет.

после двух недель исследований и проверки нескольких ресурсов: жесткий диск, ОЗУ, Кэш, я узнал, что некоторые настройки на вашем жестком диске могут повлиять на скорость ввода-вывода. При нажатии свойства на желаемом выходном диске вы можете увидеть два варианта на вкладке Общие. Opt1: сжать этот диск, Opt2: разрешить индексирование содержимого файлов этого диска.

отключив эти две опции, Все 3 ПК теперь занимают примерно одно и то же время, чтобы закончить (1 час и 20 до 40 минут). Если вы столкнулись с медленными вставками, проверьте, настроен ли ваш жесткий диск с этими параметрами. Это сэкономит вам много времени и головных болей, пытаясь найти решение


ответ на ваш вопрос заключается в том, что новый sqlite3 улучшил производительность, используйте это.

ответ почему SQLAlchemy insert с sqlite в 25 раз медленнее, чем с помощью sqlite3 напрямую? автор SqlAlchemy Orm имеет 100k вставок за 0,5 сек, и я видел аналогичные результаты с python-sqlite и SqlAlchemy. Это заставляет меня думать, что производительность улучшилась с sqlite3