Popraw wydajność SQLite w trybie INSERT na sekundę

2975

Optymalizacja SQLite jest trudna. Wydajność wkładania luzem w aplikacji C może wynosić od 85 płytek na sekundę do ponad 96 000 płytek na sekundę!

Tło: Używamy SQLite jako części aplikacji komputerowej. Mamy duże ilości danych konfiguracyjnych przechowywanych w plikach XML, które są analizowane i ładowane do bazy danych SQLite w celu dalszego przetwarzania podczas inicjalizacji aplikacji. SQLite jest idealny w tej sytuacji, ponieważ jest szybki, nie wymaga specjalnej konfiguracji, a baza danych jest przechowywana na dysku jako pojedynczy plik.

Uzasadnienie: Początkowo byłem rozczarowany występem, który oglądałem. Okazuje się, że wydajność SQLite może się znacznie różnić (zarówno w przypadku wstawiania zbiorczego, jak i selekcji) w zależności od konfiguracji bazy danych i sposobu korzystania z interfejsu API. Nie było trywialne ustalenie, jakie były wszystkie opcje i techniki, więc pomyślałem, że rozsądnie jest utworzyć ten wpis wiki społeczności, aby udostępnić wyniki czytelnikom stosu przepełnienia, aby zaoszczędzić innym problemów związanych z tymi samymi dochodzeniami.

Eksperyment: Zamiast po prostu mówić o ogólnych wskazówkach dotyczących wydajności (tj. „Użyj transakcji!” ), Pomyślałem, że najlepiej napisać trochę kodu C i zmierzyć wpływ różnych opcji. Zaczniemy od kilku prostych danych:

  • Plik tekstowy rozdzielony 28 MB TAB (około 865 000 rekordów) z kompletnym harmonogramem tranzytu dla miasta Toronto
  • Moja maszyna testowa to 3,60 GHz P4 z systemem Windows XP.
  • Kod jest kompilowany z Visual C ++ 2005 jako „Release” z „Full Optimization” (/ Ox) i Favor Fast Code (/ Ot).
  • Używam SQLite „Amalgamation”, skompilowanego bezpośrednio w mojej aplikacji testowej. Wersja SQLite, którą posiadam, jest nieco starsza (3.6.7), ale podejrzewam, że te wyniki będą porównywalne z najnowszą wersją (proszę zostawić komentarz, jeśli uważasz inaczej).

Napiszmy kod!

Kod: Prosty program C, który odczytuje plik tekstowy wiersz po wierszu, dzieli ciąg na wartości, a następnie wstawia dane do bazy danych SQLite. W tej „podstawowej” wersji kodu baza danych jest tworzona, ale tak naprawdę nie wstawiamy danych:

/*************************************************************
    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;
}

Kontrola"

Uruchamianie kodu w obecnej postaci nie wykonuje żadnych operacji na bazie danych, ale da nam wyobrażenie o tym, jak szybkie są operacje we / wy nieprzetworzonego pliku C i przetwarzania łańcucha.

Zaimportowano 864913 rekordów w 0,94 sekundy

Świetny! Możemy wykonać 920 000 wstawek na sekundę, pod warunkiem, że tak naprawdę nie zrobimy żadnych wstawek :-)


„Scenariusz najgorszego przypadku”

Wygenerujemy ciąg SQL za pomocą wartości odczytanych z pliku i wywołamy tę operację SQL za pomocą 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);

To będzie powolne, ponieważ SQL zostanie skompilowany w kodzie VDBE dla każdej wstawki, a każda wstawka nastąpi w ramach własnej transakcji. Jak wolno

Zaimportowano 864913 rekordów w 9933.61 sekund

Yikes! 2 godziny i 45 minut! To tylko 85 wstawek na sekundę.

Korzystanie z transakcji

Domyślnie SQLite ocenia każdą instrukcję INSERT / UPDATE w ramach unikalnej transakcji. W przypadku wykonywania dużej liczby wstawek wskazane jest zawinięcie operacji w transakcję:

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

Zaimportowano 864913 rekordów w 38,03 sekundy

Tak lepiej Po prostu zawinięcie wszystkich naszych wkładek w jedną transakcję poprawiło naszą wydajność do 23 000 wkładek na sekundę.

Korzystanie z przygotowanego wyciągu

Wykorzystanie transakcji było ogromnym ulepszeniem, ale rekompilacja instrukcji SQL dla każdej wstawki nie ma sensu, jeśli używamy tego samego SQL w kółko. Użyjmy raz, sqlite3_prepare_v2aby skompilować naszą instrukcję SQL, a następnie powiązać nasze parametry z tą instrukcją, używając 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;

Zaimportowano 864913 rekordów w 16,27 sekund

Miły! Jest trochę więcej kodu (nie zapomnij zadzwonić sqlite3_clear_bindingsi sqlite3_reset), ale podwoiliśmy naszą wydajność do 53 000 wstawek na sekundę.

PRAGMA synchroniczny = WYŁ

Domyślnie SQLite zatrzyma się po wydaniu polecenia zapisu na poziomie systemu operacyjnego. To gwarantuje, że dane są zapisywane na dysku. Ustawiając synchronous = OFF, instruujemy SQLite, aby po prostu przekazał dane do systemu operacyjnego w celu zapisu, a następnie kontynuował. Istnieje prawdopodobieństwo, że plik bazy danych zostanie uszkodzony, jeśli komputer ulegnie katastrofalnej awarii (lub awarii zasilania) przed zapisaniem danych na talerzu:

/* 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);

Zaimportowano 864913 rekordów w 12,41 sekund

Ulepszenia są teraz mniejsze, ale mamy do 69 600 wkładek na sekundę.

PRAGMA journal_mode = MEMORY

Rozważ zapisanie dziennika wycofania w pamięci, oceniając PRAGMA journal_mode = MEMORY. Twoja transakcja będzie szybsza, ale jeśli stracisz moc lub program ulegnie awarii podczas transakcji, baza danych może pozostać w stanie uszkodzonym z częściowo zakończoną transakcją:

/* 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);

Zaimportowano 864913 rekordów w 13,50 sekundy

Nieco wolniej niż poprzednia optymalizacja przy 64 000 płytek na sekundę.

PRAGMA synchroniczny = WYŁ. I PRAGMA tryb_ dziennika = PAMIĘĆ

Połączmy dwie poprzednie optymalizacje. Jest to trochę bardziej ryzykowne (w przypadku awarii), ale po prostu importujemy dane (nie prowadzimy banku):

/* 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);

Zaimportowano 864913 rekordów w 12,00 sekund

Fantastyczny! Jesteśmy w stanie wykonać 72 000 wstawek na sekundę.

Korzystanie z bazy danych w pamięci

Na wszelki wypadek wykorzystajmy wszystkie poprzednie optymalizacje i ponownie zdefiniuj nazwę pliku bazy danych, abyśmy pracowali całkowicie w pamięci RAM:

#define DATABASE ":memory:"

Zaimportowano 864913 rekordów w 10,94 sekund

Przechowywanie naszej bazy danych w pamięci RAM nie jest zbyt praktyczne, ale imponujące jest to, że możemy wykonać 79 000 wstawek na sekundę.

Refaktoryzacja kodu C.

Chociaż nie jest to specjalnie poprawa SQLite, nie lubię dodatkowych char*operacji przypisywania w whilepętli. Szybko przekształćmy ten kod, aby przekazać dane wyjściowe strtok()bezpośrednio do sqlite3_bind_text(), i pozwólmy kompilatorowi spróbować przyspieszyć dla nas:

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

Uwaga: wróciliśmy do używania prawdziwego pliku bazy danych. Bazy danych w pamięci są szybkie, ale niekoniecznie praktyczne

Zaimportowano 864913 rekordów w 8,94 sekundy

Nieznaczne przefakturowanie kodu przetwarzania łańcucha używanego w naszym wiązaniu parametrów pozwoliło nam na wykonanie 96 700 wstawień na sekundę. Myślę, że można śmiało powiedzieć, że to dość szybko . Gdy zaczniemy dostosowywać inne zmienne (tj. Rozmiar strony, tworzenie indeksu itp.), Będzie to nasz punkt odniesienia.


Podsumowanie (do tej pory)

Mam nadzieję, że wciąż jesteś ze mną! Powodem, dla którego zaczęliśmy tę drogę, jest to, że wydajność wstawiania zbiorczego zmienia się tak bardzo w zależności od SQLite, i nie zawsze jest oczywiste, jakie zmiany należy wprowadzić, aby przyspieszyć naszą działalność. Korzystając z tego samego kompilatora (i opcji kompilatora), tej samej wersji SQLite i tych samych danych zoptymalizowaliśmy nasz kod i nasze użycie SQLite, aby przejść od najgorszego scenariusza z 85 wstawkami na sekundę do ponad 96 000 wstawień na sekundę!


UTWÓRZ INDEKS, następnie WSTAW vs. WSTAW, a następnie UTWÓRZ INDEKS

Zanim zaczniemy mierzyć SELECTwydajność, wiemy, że będziemy tworzyć wskaźniki. Zasugerowano w jednej z poniższych odpowiedzi, że podczas wykonywania wstawiania zbiorczego szybciej jest tworzyć indeks po wstawieniu danych (w przeciwieństwie do tworzenia indeksu najpierw, a następnie wstawiania danych). Spróbujmy:

Utwórz indeks, a następnie wstaw dane

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

Zaimportowano 864913 rekordów w 18,13 sekund

Wstaw dane, a następnie Utwórz indeks

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

Zaimportowano 864913 rekordów w 13,66 sekund

Zgodnie z oczekiwaniami wstawianie zbiorcze jest wolniejsze, jeśli jedna kolumna jest indeksowana, ale robi to różnicę, jeśli indeks jest tworzony po wstawieniu danych. Nasza linia bazowa bez indeksu wynosi 96 000 wstawek na sekundę. Utworzenie indeksu najpierw, a następnie wstawienie danych daje nam 47 700 wstawek na sekundę, podczas gdy wstawienie danych, a następnie utworzenie indeksu daje nam 63 300 wstawek na sekundę.


Z przyjemnością skorzystam z sugestii dotyczących innych scenariuszy do wypróbowania ... I wkrótce skompiluję podobne dane dla zapytań SELECT.

Mike Willekes
źródło
8
Słuszna uwaga! W naszym przypadku mamy do czynienia z około 1,5 miliona par klucz / wartość odczytanych z plików tekstowych XML i CSV do 200 000 rekordów. Mały w porównaniu do baz danych, które obsługują witryny takie jak SO - ale wystarczająco duży, aby dostrajać wydajność SQLite.
Mike Willekes,
51
„Mamy duże ilości danych konfiguracyjnych przechowywanych w plikach XML, które są analizowane i ładowane do bazy danych SQLite w celu dalszego przetwarzania podczas inicjalizacji aplikacji.” dlaczego nie przechowujesz wszystkiego w bazie danych sqlite, zamiast przechowywać w XML, a następnie ładować wszystko podczas inicjalizacji?
CAFxX
14
Próbowałeś nie dzwonić sqlite3_clear_bindings(stmt);? Powiązania ustawia się za każdym razem, gdy powinno wystarczyć: Przed pierwszym wywołaniem funkcji sqlite3_step () lub bezpośrednio po funkcji sqlite3_reset () aplikacja może wywołać jeden z interfejsów sqlite3_bind () w celu dołączenia wartości do parametrów. Każde wywołanie funkcji sqlite3_bind () zastępuje wcześniejsze wiązania tego samego parametru (patrz: sqlite.org/cintro.html ). Nie ma nic w docs dla tej funkcji mówiąc trzeba to nazwać.
ahcox,
21
Czy powtórzyłeś pomiary? „Wygrana” 4s za uniknięcie 7 lokalnych wskaźników jest dziwna, nawet przy założeniu dezorientacji.
peterchen
5
Nie używaj feof()do kontrolowania zakończenia pętli wejściowej. Użyj wyniku zwróconego przez fgets(). stackoverflow.com/a/15485689/827263
Keith Thompson

Odpowiedzi:

785

Kilka wskazówek:

  1. Umieść wstawki / aktualizacje w transakcji.
  2. W przypadku starszych wersji SQLite - rozważ mniej paranoiczny tryb dziennika ( pragma journal_mode). Jest NORMAL, a potem jest OFF, co może znacznie zwiększyć szybkość wstawiania, jeśli nie martwisz się zbytnio możliwością uszkodzenia bazy danych w przypadku awarii systemu operacyjnego. Jeśli aplikacja ulegnie awarii, dane powinny być w porządku. Pamiętaj, że w nowszych wersjach OFF/MEMORYustawienia nie są bezpieczne w przypadku awarii na poziomie aplikacji.
  3. Różnica ma również gra z rozmiarami stron ( PRAGMA page_size). Większe rozmiary stron mogą sprawić, że odczyty i zapisy będą przebiegać nieco szybciej, ponieważ większe strony są przechowywane w pamięci. Zauważ, że twoja baza danych wykorzysta więcej pamięci.
  4. Jeśli masz indeksy, rozważ wykonanie połączenia CREATE INDEXpo wykonaniu wszystkich wstawek. Jest to znacznie szybsze niż tworzenie indeksu, a następnie wstawianie wstawek.
  5. Musisz być bardzo ostrożny, jeśli masz równoczesny dostęp do SQLite, ponieważ cała baza danych jest blokowana po zakończeniu zapisu i chociaż możliwe jest użycie wielu czytników, zapisy zostaną zablokowane. Zostało to nieco poprawione poprzez dodanie WAL w nowszych wersjach SQLite.
  6. Skorzystaj z oszczędności miejsca ... mniejsze bazy danych działają szybciej. Na przykład, jeśli masz pary klucz-wartość, spróbuj uczynić klucz an, INTEGER PRIMARY KEYjeśli to możliwe, co zastąpi domyślną kolumnę z unikalnym numerem wiersza w tabeli.
  7. Jeśli korzystasz z wielu wątków, możesz spróbować użyć współużytkowanej pamięci podręcznej stron , co pozwoli na udostępnienie załadowanych stron między wątkami, co pozwoli uniknąć kosztownych wywołań We / Wy.
  8. Nie używaj !feof(file)!

Zadałem również podobne pytania tutaj i tutaj .

Snazzer
źródło
9
Dokumenty nie znają trybu PRAGMA journal_mode NORMAL sqlite.org/pragma.html#pragma_journal_mode
OneWorld
4
Minęło trochę czasu, moje sugestie zastosowały się do starszych wersji, zanim wprowadzono WAL. Wygląda na to, że DELETE to nowe normalne ustawienie, a teraz są też ustawienia OFF i MEMORY. Przypuszczam, że OFF / MEMORY poprawi wydajność zapisu kosztem integralności bazy danych, a OFF całkowicie wyłącza wycofywanie zmian.
Snazzer
4
dla # 7, czy masz przykład, jak włączyć współużytkowaną pamięć podręczną strony za pomocą opakowania c # system.data.sqlite?
Aaron Hudon,
4
# 4 przywoływało stare wspomnienia - w przeszłości istniał co najmniej jeden przypadek, w którym upuszczenie indeksu przed grupą dodatków i ponowne utworzenie go później znacznie przyspieszyło wstawianie. Może nadal działać szybciej na nowoczesnych systemach dla niektórych dodatków, o których wiesz, że masz wyłączny dostęp do tabeli na ten okres.
Bill K
Kciuki za 1: Sam miałem bardzo dużo szczęścia z transakcjami.
Enno
146

Spróbuj użyć SQLITE_STATICzamiast SQLITE_TRANSIENTtych wkładek.

SQLITE_TRANSIENT spowoduje, że SQLite skopiuje dane ciągu przed zwróceniem.

SQLITE_STATICinformuje, że podany adres pamięci będzie ważny do momentu wykonania zapytania (co zawsze ma miejsce w tej pętli). Pozwoli to zaoszczędzić kilka operacji alokacji, kopiowania i cofania przydziału na pętlę. Prawdopodobnie duża poprawa.

Alexander Farber
źródło
109

Unikać sqlite3_clear_bindings(stmt).

Kod w teście ustawia wiązania za każdym razem, przez które powinno wystarczyć.

C API Intro z SQLite docs mówi:

Przed wywołaniem funkcji sqlite3_step () po raz pierwszy lub bezpośrednio po funkcji sqlite3_reset () aplikacja może wywołać interfejsy sqlite3_bind () w celu dołączenia wartości do parametrów. Każde wywołanie funkcji sqlite3_bind () przesłania wcześniejsze wiązania tego samego parametru

Dokumenty nie zawierają niczego sqlite3_clear_bindings, co mówi, że musisz to nazwać oprócz zwykłego ustawiania powiązań.

Więcej szczegółów: Avoid_sqlite3_clear_bindings ()

ahcox
źródło
5
Cudownie słusznie: „W przeciwieństwie do intuicji wielu, sqlite3_reset () nie resetuje powiązań w przygotowanej instrukcji. Użyj tej procedury, aby zresetować wszystkie parametry hosta do NULL.” - sqlite.org/c3ref/clear_bindings.html
Francis Straccia
63

Na wkładkach luzem

Zainspirowany tym postem i pytaniem o przepełnienie stosu, które mnie tu zaprowadziło - czy można wstawiać wiele wierszy jednocześnie w bazie danych SQLite? - Opublikowałem swoje pierwsze repozytorium Git :

https://github.com/rdpoor/CreateOrUpdate

który masowo ładuje tablicę ActiveRecords do baz danych MySQL , SQLite lub PostgreSQL . Zawiera opcję zignorowania istniejących rekordów, zastąpienia ich lub zgłoszenia błędu. Moje podstawowe testy porównawcze pokazują 10-krotną poprawę prędkości w porównaniu do zapisu sekwencyjnego - YMMV.

Używam go w kodzie produkcyjnym, w którym często muszę importować duże zestawy danych i jestem z tego całkiem zadowolony.

nieustraszony_głupiec
źródło
4
@Jess: Jeśli podążysz za linkiem, zobaczysz, że miał na myśli składnię wstawiania wsadowego.
Alix Axel
48

Wydaje się, że importowanie zbiorcze działa najlepiej, jeśli można podzielić fragmenty instrukcji INSERT / UPDATE . Około 10 000 działało dla mnie dobrze na stole z tylko kilkoma rzędami, YMMV ...

Leon
źródło
22
Chcesz dostroić x = 10 000, aby x = pamięć podręczna [= rozmiar_pamięci podręcznej * rozmiar_strony] / średni rozmiar wstawki.
Alix Axel
43

Jeśli zależy ci tylko na czytaniu, nieco szybszą (ale może czytać nieaktualne dane) wersją jest czytanie z wielu połączeń z wielu wątków (połączenie na wątek).

Najpierw znajdź przedmioty w tabeli:

SELECT COUNT(*) FROM table

następnie czytaj na stronach (LIMIT / OFFSET):

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

gdzie i są obliczane dla wątku, tak jak to:

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

dla każdego wątku:

int offset = thread_index * limit

W przypadku naszego małego (200 MB) db spowodowało to 50-75% przyspieszenie (3.8.0.2 64-bit w Windows 7). Nasze tabele są mocno nienormalizowane (1000-1500 kolumn, około 100 000 lub więcej wierszy).

Zbyt wiele lub zbyt mało wątków tego nie zrobi, musisz sam przeprowadzić testy porównawcze i profilować.

Również dla nas SHAREDCACHE spowolnił działanie, więc ręcznie ustawiłem PRIVATECACHE (ponieważ dla nas został włączony globalnie)

malkia
źródło
29

Nie mogę uzyskać żadnego zysku z transakcji, dopóki nie podniosłem cache_size do wyższej wartości, tj PRAGMA cache_size=10000;

anefeletos
źródło
Zauważ, że użycie dodatniej wartości dla cache_sizeustawia liczbę stron do buforowania , a nie całkowity rozmiar pamięci RAM. Przy domyślnym rozmiarze strony wynoszącym 4 kB to ustawienie może pomieścić do 40 MB danych na otwarty plik (lub na proces, jeśli działa ze współużytkowaną pamięcią podręczną ).
Groo
21

Po przeczytaniu tego samouczka próbowałem zaimplementować go w moim programie.

Mam 4-5 plików zawierających adresy. Każdy plik ma około 30 milionów rekordów. Używam tej samej konfiguracji, którą sugerujesz, ale moja liczba INSERTÓW na sekundę jest zdecydowanie niska (~ 10.000 rekordów na sekundę).

Oto, gdzie twoja sugestia zawodzi. Używasz jednej transakcji dla wszystkich rekordów i jednej wstawki bez błędów / niepowodzeń. Powiedzmy, że dzielisz każdy rekord na wiele wstawek w różnych tabelach. Co się stanie, jeśli rekord zostanie pobity?

Polecenie ON CONFLICT nie ma zastosowania, ponieważ jeśli masz 10 elementów w rekordzie i potrzebujesz każdego elementu wstawionego do innej tabeli, jeśli element 5 otrzyma błąd CONSTRAINT, wówczas wszystkie poprzednie 4 wstawki również muszą przejść.

Oto, gdzie przychodzi wycofanie. Jedynym problemem związanym z wycofywaniem jest to, że tracisz wszystkie wstawki i zaczynasz od góry. Jak możesz to rozwiązać?

Moim rozwiązaniem było użycie wielu transakcji. Zaczynam i kończę transakcję co 10.000 rekordów (nie pytaj, dlaczego ta liczba była najszybsza, którą testowałem). Utworzyłem tablicę o rozmiarze 10.000 i wstawiłem tam udane rekordy. Kiedy wystąpi błąd, robię wycofywanie, rozpoczynam transakcję, wstawiam rekordy z mojej tablicy, zatwierdzam, a następnie rozpoczynam nową transakcję po uszkodzonym rekordzie.

To rozwiązanie pomogło mi ominąć problemy związane z plikami zawierającymi złe / zduplikowane rekordy (miałem prawie 4% złych rekordów).

Algorytm, który stworzyłem, pomógł mi zredukować proces o 2 godziny. Końcowy proces ładowania pliku 1 godz. 30 m, który wciąż jest powolny, ale nie jest porównywany z 4 godzinami, które początkowo trwały. Udało mi się przyspieszyć wkładki z 10.000 / s do ~ 14.000 / s

Jeśli ktoś ma jakieś pomysły na przyspieszenie, jestem otwarty na sugestie.

AKTUALIZACJA :

Oprócz mojej powyższej odpowiedzi należy pamiętać, że liczba wkładek na sekundę zależy od używanego dysku twardego. Przetestowałem to na 3 różnych komputerach z różnymi dyskami twardymi i otrzymałem ogromne różnice w czasie. PC1 (1 godz. 30 min), PC2 (6 godz.) PC3 (14 godz.), Więc zacząłem się zastanawiać, dlaczego to miałoby być.

Po dwóch tygodniach badań i sprawdzania wielu zasobów: dysku twardego, pamięci RAM, pamięci podręcznej odkryłem, że niektóre ustawienia na dysku twardym mogą wpływać na szybkość operacji we / wy. Klikając właściwości żądanego napędu wyjściowego, możesz zobaczyć dwie opcje na karcie ogólnej. Opt1: Kompresuj ten dysk, Opt2: Zezwalaj na pliki tego dysku na indeksowanie zawartości.

Wyłączając te dwie opcje, wszystkie 3 komputery zajmują teraz mniej więcej ten sam czas (1 godzina i 20 do 40 minut). Jeśli napotkasz wolne wstawianie, sprawdź, czy na dysku twardym są skonfigurowane te opcje. Zaoszczędzi ci to dużo czasu i bólu głowy, próbując znaleźć rozwiązanie

Jimmy_A
źródło
Zasugeruję następujące. * Użyj SQLITE_STATIC vs SQLITE_TRANSIENT, aby uniknąć kopiowania ciągu, musisz upewnić się, że łańcuch nie zostanie zmieniony przed wykonaniem transakcji * Użyj wstawiania zbiorczego INSERT INTO STOP_times VALUE (NULL,?,?,?,?,?,?,?,?,?,? ,?), (NULL,?,?,?,?,?,?,?,?,?), (NULL,?,?,?,?,?,?,?,?,?), (NULL ,?,?,?,?,?,?,?,?,?), (NULL,?,?,?,?,?,?,?,?,?) * Mmap plik, aby zmniejszyć liczbę wywołania systemowe.
rouzier
Robiąc to, jestem w stanie zaimportować 5.582.642 rekordy w 11.51 sekund
rouzier
-1

Użyj ContentProvider do wstawiania danych zbiorczych w db. Poniższa metoda służy do wstawiania danych zbiorczych do bazy danych. Powinno to poprawić wydajność SQLite INSERT na sekundę.

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();

}

Wywołaj metodę bulkInsert:

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

Link: https://www.vogella.com/tutorials/AndroidSQLite/article.html sprawdź Więcej sekcji Korzystanie z ContentProvider

vishnuc156
źródło