Улучшить производительность вставки в секунду 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 ответов
несколько советов:
- поместите вставки / обновления в транзакцию.
- для более старых версий SQLite-рассмотрим менее параноидальный режим журнала (
pragma journal_mode
). ЕстьNORMAL
, и естьOFF
, что может значительно увеличить скорость вставки, если вы не слишком беспокоитесь о базе данных, возможно, поврежден, если ОС аварийно завершает работу. Если ваше приложение аварийно завершает работу, данные должны быть в порядке. Обратите внимание, что в более новых версияхOFF/MEMORY
настройки не безопасны для сбой уровня приложения. - игра с размерами страниц также имеет значение (
PRAGMA page_size
). Наличие больших размеров страниц может сделать чтение и запись немного быстрее, так как большие страницы хранятся в памяти. Обратите внимание, что для вашей базы данных будет использоваться больше памяти. - если у вас есть индексы, подумайте о вызове
CREATE INDEX
после выполнения всех своих вставок. Это значительно быстрее, чем создание индекса, а затем выполнение вставок. - вы должны быть очень осторожны, если вы иметь параллельный доступ к SQLite, так как вся база данных блокируется при записи, и хотя возможно несколько читателей, записи будут заблокированы. Это было несколько улучшено с добавлением WAL в более новых версиях SQLite.
- воспользуйтесь преимуществами экономии места...меньшие базы данных работают быстрее. Например, если у вас есть пары значений ключей, попробуйте сделать ключ
INTEGER PRIMARY KEY
если возможно, который заменит подразумеваемый уникальный столбец номера строки в таблица. - если вы используете несколько потоков, вы можете попробовать использовать общий кэш страницы, что позволит совместно использовать загруженные страницы между потоками, что позволит избежать дорогостоящих вызовов ввода-вывода.
- не используйте
!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