Поліпшення INSERT за секунду продуктивності SQLite


2975

Оптимізація 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 запитів.


8
Гарна думка! У нашому випадку ми маємо справу з приблизно 1,5 мільйонами пар ключів / значень, прочитаних з текстових файлів XML та CSV в 200 000 записів. Невеликий порівняно з базами даних, на яких розміщені сайти типу SO, але досить великий, що настройка продуктивності SQLite стає важливою.
Майк Віллекс

51
"У нас є велика кількість даних конфігурації, що зберігаються у файлах XML, які аналізуються та завантажуються в базу даних SQLite для подальшої обробки при ініціалізації програми." чому б вам не зберегти все в базі даних sqlite в першу чергу, а не зберігати в XML і не завантажувати все під час ініціалізації?
CAFxX

14
Ви намагалися не дзвонити sqlite3_clear_bindings(stmt);? Ви встановлюєте прив'язки щоразу, через які повинно бути достатньо: Перед тим, як вперше викликати sqlite3_step () або відразу після sqlite3_reset (), програма може викликати один з інтерфейсів sqlite3_bind () для приєднання значень до параметрів. Кожен виклик sqlite3_bind () переосмислює попередні прив’язки для того ж параметра (див.: Sqlite.org/cintro.html ). У документах немає нічого для цієї функції, яка б сказала, що ви повинні її викликати.
ahcox

21
Ви робили повторні вимірювання? "Виграти" 4s за те, щоб уникнути 7 локальних покажчиків дивно, навіть якщо припустити, що заплутаний оптимізатор.
peterchen

5
Не використовуйте feof()для контролю закінчення вхідного циклу. Скористайтеся результатом, повернутим користувачем fgets(). stackoverflow.com/a/15485689/827263
Кіт Томпсон

Відповіді:


785

Кілька порад:

  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)!

Я також задавав подібні запитання тут і тут .


9
Документи не знають PRAGMA journal_mode NORMAL sqlite.org/pragma.html#pragma_journal_mode
OneWorld

4
Минуло багато часу, мої пропозиції застосовувались до старих версій до введення WAL. Схоже, DELETE - це новий звичайний параметр, тепер також є параметри OFF і MEMORY. Я вважаю, що OFF / MEMORY покращить продуктивність запису за рахунок цілісності бази даних, а OFF повністю відключить відкат.
Snazzer

4
для №7, чи є у вас приклад того, як увімкнути кеш-пам'ять спільної сторінки за допомогою обгортки c # system.data.sqlite?
Аарон Хадон

4
№4 повернув вікові старі спогади - У попередні часи був принаймні один випадок, коли випадання індексу перед групою доповнень та його повторне створення після цього значно просунуло вставки. Можливо, все одно швидше працювати в сучасних системах для деяких додатків, коли ви знаєте, що ви маєте єдиний доступ до таблиці за цей період.
Білл К

Великі пальці за №1: мені дуже пощастило з транзакціями.
Енно

146

Спробуйте використовувати SQLITE_STATICзамість SQLITE_TRANSIENTцих вставок.

SQLITE_TRANSIENT змусить SQLite скопіювати рядкові дані перед поверненням.

SQLITE_STATICповідомляє, що адреса пам'яті, яку ви вказали, буде дійсною, поки запит не буде виконаний (що в цьому циклі завжди є). Це дозволить заощадити кілька операцій з розподілу, копіювання та розподілення за циклом. Можливо, велике поліпшення.


109

Уникайте sqlite3_clear_bindings(stmt).

Код у тесті встановлює прив'язки кожного разу, через які має бути достатньо.

C API інтро з SQLite документації каже:

Перед тим, як вперше викликати sqlite3_step () або відразу після sqlite3_reset () , програма може викликати інтерфейси sqlite3_bind () для приєднання значень до параметрів. Кожен виклик sqlite3_bind () переосмислює попередні прив’язки для того ж параметра

У документах немає нічого для того, щоб sqlite3_clear_bindingsсказати, що вам потрібно зателефонувати, крім просто встановлення прив’язки.

Більш детально: Avoid_sqlite3_clear_bindings ()


5
Чудово правильно: "Всупереч інтуїції багатьох, sqlite3_reset () не скидає прив'язки для підготовленого оператора. Використовуйте цю процедуру для скидання всіх параметрів хоста на NULL." - sqlite.org/c3ref/clear_bindings.html
Франциск

63

На об'ємних вставках

Натхненний цією публікацією та запитанням про переповнення стека, яке мене привело сюди - чи можна вставляти кілька рядків одночасно в базу даних SQLite? - Я опублікував своє перше сховище Git :

https://github.com/rdpoor/CreateOrUpdate

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

Я використовую його у виробничому коді, куди мені часто потрібно імпортувати великі набори даних, і я дуже задоволений цим.


4
@Jess: Якщо ви перейдете за посиланням, ви побачите, що він мав на увазі синтаксис пакетної вставки.
Алікс Аксель

48

Масовий імпорт, здається, працює найкраще, якщо ви можете скинути свої заяви INSERT / UPDATE . Значення 10 000 або близько того добре працювало для мене на столі з кількома рядами, YMMV ...


22
Ви хочете налаштувати x = 10 000 так, щоб x = кеш [= cache_size * page_size] / середній розмір вашої вставки.
Алікс Аксель

43

Якщо ви дбаєте лише про читання, дещо швидша (але може читати несвіжі дані) версія - це зчитування з декількох з'єднань з декількох потоків (з'єднання за ниткою).

Спочатку знайдіть предмети в таблиці:

SELECT COUNT(*) FROM table

потім читайте на сторінках (LIMIT / OFFSET):

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

де і обчислюються за нитку, як це:

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

для кожної нитки:

int offset = thread_index * limit

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

Занадто багато або занадто мало потоків не зробить це, вам потрібно зробити орієнтир та профайл.

Також для нас SHAREDCACHE зробив продуктивність повільніше, тому я вручну поставив PRIVATECACHE (тому що це було включено для нас у всьому світі)


29

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


Зауважте, що використовуючи додатне значення для cache_sizeвстановлення кількості кеш-сторінок , а не загального розміру ОЗУ. Якщо розмір сторінки за замовчуванням становить 4 КБ, цей параметр вміщує до 40 МБ даних на відкритий файл (або на процес, якщо він працює зі спільним кешем ).
Груо

21

Прочитавши цей підручник, я спробував реалізувати його у своїй програмі.

У мене 4-5 файлів, які містять адреси. Кожен файл має приблизно 30 мільйонів записів. Я використовую ту саму конфігурацію, яку ви пропонуєте, але кількість моїх ВСТУП в секунду є низькою (~ 10 000 записів в секунду).

Ось де ваша пропозиція не вдається. Ви використовуєте одну транзакцію для всіх записів і одну вставку без помилок / помилок. Скажімо, ви розділяєте кожен запис на кілька вставок на різних таблицях. Що станеться, якщо запис буде порушений?

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

Тож ось де відбувається відкат. Єдине питання відкату полягає в тому, що ви втрачаєте всі свої вставки та починаєте зверху. Як ви можете це вирішити?

Моє рішення полягало у використанні декількох транзакцій. Я починаю і закінчую транзакцію кожні 10 000 записів (не запитуйте, чому саме це число, це найшвидший тестування). Я створив масив розміром 10.000 і вставив туди успішні записи. Коли виникає помилка, я роблю відкат, починаю транзакцію, вставляю записи з мого масиву, здійснюю фіксацію, а потім починаю нову транзакцію після зламаної записи.

Це рішення допомогло мені обійти проблеми, що виникають при роботі з файлами, що містять погані / дублікати записів (у мене було майже 4% поганих записів).

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

Якщо у когось є якісь інші ідеї щодо його прискорення, я відкритий для пропозицій.

ОНОВЛЕННЯ :

На додаток до моєї відповіді вище, слід пам’ятати, що вставляє в секунду залежно від жорсткого диска, який ви також використовуєте. Я перевірив його на 3-х різних ПК з різними жорсткими дисками і отримав величезні відмінності в часі. PC1 (1h 30m), PC2 (6hrs) PC3 (14hrs), тому я почав цікавитись, чому це було б.

Після двох тижнів досліджень і перевірки декількох ресурсів: Жорсткий диск, Рам, Кеш, я з’ясував, що деякі параметри вашого жорсткого диска можуть впливати на швидкість вводу / виводу. Клацнувши властивості на потрібному вихідному накопичувачі, ви побачите два варіанти на загальній вкладці. Opt1: Стисніть цей диск, Opt2: Дозвольте файлам цього диска індексувати вміст.

Якщо вимкнути ці два варіанти, усі 3 комп'ютери тепер потребують приблизно однакового часу (1 год. І 20-40 хв.). Якщо у вас виникають повільні вставки, перевірте, чи налаштований ваш жорсткий диск із цими параметрами. Це заощадить вам багато часу та головного болю, намагаючись знайти рішення


Я запропоную наступне. * Використовуйте SQLITE_STATIC проти SQLITE_TRANSIENT, щоб уникнути копіювання рядка, ви повинні переконатися, що рядок не буде змінено перед виконанням транзакції. * Використовуйте вставку масово ВСТАВИТЬСЯ В НАТОЧНІ СТОРОНИ (NULL,?,?,?,?,?,?,?,?,?,?,? ,?), (NULL,?,?,?,?,?,?,?,?,?), (NULL,?,?,?,?,?,?,?,?,?), (NULL ,?,?,?,?,?,?,?,?,?), (NULL,?,?,?,?,?,?,?,?,?) * Mmap файл, щоб зменшити кількість систематичні дзвінки.
rouzier

Здійснюючи це, я можу імпортувати 5,582,642 записів за 11,51 секунди
rouzier

11

Відповідь на ваше запитання полягає в тому, що новіший SQLite 3 покращив продуктивність, використовуйте це.

Ця відповідь Чому SQLAlchemy вставляє sqlite в 25 разів повільніше, ніж безпосередньо використовувати sqlite3? автор SqlAlchemy Orm Автор має 100k вставок за 0,5 сек., і я бачив подібні результати з python-sqlite та SqlAlchemy. Що змушує мене вважати, що продуктивність покращилася за допомогою SQLite 3.


-1

Використовуйте ContentProvider для вставки об'ємних даних у db. Наведений нижче метод використовується для вставки об'ємних даних у базу даних. Це повинно покращити INSERT за секунду продуктивність SQLite.

private SQLiteDatabase database;
database = dbHelper.getWritableDatabase();

public int bulkInsert(@NonNull Uri uri, @NonNull ContentValues[] values) {

database.beginTransaction();

for (ContentValues value : values)
 db.insert("TABLE_NAME", null, value);

database.setTransactionSuccessful();
database.endTransaction();

}

Метод виклику bulkInsert:

App.getAppContext().getContentResolver().bulkInsert(contentUriTable,
            contentValuesArray);

Посилання: https://www.vogella.com/tutorials/AndroidSQLite/article.html перевірити Використання розділу ContentProvider для отримання більш детальної інформації

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.