Оптимізація SQLite складна. Продуктивність додатків для масової вставки програми C може змінюватись від 85 вставок в секунду до понад 96 000 вставок в секунду!
Передумови: Ми використовуємо SQLite як частина настільного додатку. У нас є велика кількість конфігураційних даних, що зберігаються у файлах XML, які аналізуються та завантажуються в базу даних SQLite для подальшої обробки при ініціалізації програми. SQLite ідеально підходить для цієї ситуації, оскільки він швидкий, він не вимагає спеціалізованої конфігурації, а база даних зберігається на диску як один файл.
Обгрунтування: Спочатку я був розчарований виступом, який я бачив. Виявляється, продуктивність SQLite може суттєво відрізнятися (як для масових вставок, так і для вибору) залежно від того, як налаштована база даних та як ви використовуєте API. Зрозуміти, які всі варіанти та методи, було не тривіальною справою, тому я вважав за доцільне створити цей запис у вікі спільноти, щоб ділитися результатами з читачами Stack Overflow, щоб врятувати іншим проблеми тих же розслідувань.
Експеримент: Замість того, щоб просто говорити про поради щодо ефективності у загальному розумінні (тобто "Використовуйте транзакцію!" ), Я вважав, що найкраще написати якийсь код C і фактично виміряти вплив різних варіантів. Почнемо з простих даних:
- Текстовий файл з обмеженою таблицею на 28 Мб (приблизно 865 000 записів) повного графіку транзиту до міста Торонто
- Мій тестовий апарат - P4 3,60 ГГц під управлінням Windows XP.
- Код складено з Visual C ++ 2005 як "Release" з "Повною оптимізацією" (/ 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] = "\0";
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] = "\0";
/*********************************************/
/* 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 seconds\n", n, (clock() - cStartClock) / (double)CLOCKS_PER_SEC);
sqlite3_close(db);
return 0;
}
"Управління"
Запуск коду як є насправді не виконує жодних операцій з базою даних, але це дасть нам уявлення про те, наскільки швидко виконуються операції вводу / виводу сировинних файлів C і обробка рядків.
Імпортовано 864913 записів за 0,94 секунди
Чудово! Ми можемо зробити 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 секунди
Yikes! 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 секунди
Так краще. Просто загортання всіх наших вставок однією транзакцією покращило нашу ефективність до 23000 вставок за секунду.
Використання підготовленої заяви
Використання транзакції було величезним вдосконаленням, але перекомпілювати оператор 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 seconds\n", n, (clock() - cStartClock) / (double)CLOCKS_PER_SEC);
sqlite3_finalize(stmt);
sqlite3_close(db);
return 0;
Імпортовано 864913 записів за 16,27 секунди
Приємно! Існує трохи більше коду (не забудьте зателефонувати sqlite3_clear_bindings
і sqlite3_reset
), але ми збільшили свою ефективність до 53 000 вставок в секунду.
PRAGMA синхронний = ВИКЛ
За замовчуванням 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 секунди
Вдосконалення зараз менші, але ми до 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 секунди
Трохи повільніше, ніж попередня оптимізація, при 64 000 вставок в секунду.
PRAGMA синхронний = OFF та 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 synchronous = OFF", NULL, NULL, &sErrMsg);
sqlite3_exec(db, "PRAGMA journal_mode = MEMORY", NULL, NULL, &sErrMsg);
Імпортовано 864913 записів за 12.00 секунд
Фантастичний! Ми можемо зробити 72 000 вставок в секунду.
Використання бази даних в пам'яті
Щойно для ударів, давайте будемо спиратися на всі попередні оптимізації та переосмислювати ім’я файлу бази даних, щоб ми повністю працювали в оперативній пам'яті:
#define DATABASE ":memory:"
Імпортовано 864913 записів за 10,94 секунди
Зберігати нашу базу даних в оперативній пам’яті не надто практично, але вражає, що ми можемо виконувати 79000 вставок в секунду.
Кодекс рефакторингу C
Хоча конкретно не вдосконалення SQLite, мені не подобаються додаткові 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 секунди
Невелике рефакторинг на код обробки рядків, який використовується в нашому прив'язці параметрів, дозволив нам виконати 96 700 вставок в секунду. Я думаю, що можна впевнено сказати, що це досить швидко . Поки ми починаємо налаштовувати інші змінні (наприклад, розмір сторінки, створення індексу тощо), це стане нашим орієнтиром.
Підсумок (поки що)
Сподіваюся, ти все ще зі мною! Причина, по якій ми почали цю дорогу, полягає в тому, що продуктивність об'ємних вставок відрізняється настільки диво, як і SQLite, і не завжди очевидно, які зміни потрібно внести для прискорення нашої роботи. Використовуючи той самий компілятор (і параметри компілятора), ту саму версію SQLite та ті самі дані, ми оптимізували наш код і наше використання SQLite, щоб перейти від найгіршого сценарію від 85 вставок в секунду до понад 96 000 вставок в секунду!
СТВОРИТИ ІНДЕКС, потім ВСТАВЛЯТЬ проти ВСТУПУ, а потім СТВОРИТИСЬ індексу
Перш ніж розпочати вимірювання 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 секунди
Вставте дані, а потім створіть індекс
...
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 секунди
Як і очікувалося, масові вставки проходять повільніше, якщо один стовпець індексується, але це має значення, якщо індекс створюється після введення даних. Наш базовий рівень без індексу - 96 000 вставок в секунду. Створення спочатку індексу, а потім введення даних дає нам 47 700 вставок в секунду, тоді як вставлення даних спочатку, а потім створення індексу дає нам 63 300 вставок в секунду.
Я з радістю прийму пропозиції щодо інших сценаріїв, щоб спробувати ... І незабаром збираю подібні дані для SELECT запитів.
sqlite3_clear_bindings(stmt);
? Ви встановлюєте прив'язки щоразу, через які повинно бути достатньо: Перед тим, як вперше викликати sqlite3_step () або відразу після sqlite3_reset (), програма може викликати один з інтерфейсів sqlite3_bind () для приєднання значень до параметрів. Кожен виклик sqlite3_bind () переосмислює попередні прив’язки для того ж параметра (див.: Sqlite.org/cintro.html ). У документах немає нічого для цієї функції, яка б сказала, що ви повинні її викликати.
feof()
для контролю закінчення вхідного циклу. Скористайтеся результатом, повернутим користувачем fgets()
. stackoverflow.com/a/15485689/827263