SQLite to popularna, relacyjna baza danych, którą osadzasz w swojej aplikacji. Wraz ze wzrostem ilości danych w Twojej bazie danych musisz zastosować dostrajanie wydajności SQLite. W tym artykule omówiono indeksy i ich pułapki, użycie planera zapytań, tryb dziennika Write-Ahead-Logging (WAL) i zwiększanie rozmiaru pamięci podręcznej. Opisuje również znaczenie mierzenia wpływu twoich poprawek za pomocą automatycznych testów.
Wprowadzenie
SQLite to popularny system relacyjnych baz danych (DB) . W przeciwieństwie do swoich większych braci opartych na kliencie i serwerze, takich jak MySQL, SQLite może być osadzony w Twojej aplikacji jako biblioteka . SQLite ma bardzo podobny zestaw funkcji i może również obsługiwać miliony wierszy, biorąc pod uwagę, że znasz kilka wskazówek i wskazówek dotyczących dostrajania wydajności. Jak pokażą poniższe sekcje, jest więcej informacji na temat dostrajania wydajności SQLite niż tylko tworzenie indeksów.
Twórz indeksy, ale ostrożnie
Podstawową ideą indeksu jest przyspieszenie czytania określonych danych , czyli SELECT
instrukcje z WHERE
klauzula. Indeksy przyspieszają także sortowanie dane (ORDER BY
) lub JOIN
stoły. Niestety, indeksy są mieczem obosiecznym, ponieważ zajmują dodatkowe miejsce na dysku i spowalniają manipulację danymi (INSERT
, UPDATE
, DELETE
).
Ogólna rada to tworzenie jak najmniejszej liczby indeksów, ale tak wielu, jak to konieczne . Ponadto indeksy mają sens tylko w przypadku większych bazy danych z tysiącami lub milionami wierszy.
Użyj planera zapytań do analizy zapytań
Sposób, w jaki indeksy są używane wewnętrznie przez SQLite, jest udokumentowany, ale niezbyt łatwy do zrozumienia. Jak szczegółowo omówiono w tym artykule, dobrym pomysłem jest analizowanie zapytania przez poprzedzenie go przedrostkiem EXPLAIN QUERY PLAN
. Spójrz na każdą linię wyjściową, która ma trzy podstawowe warianty:
SEARCH table ...
linie to dobry znak – SQLite używa jednego z Twoich indeksów!SCAN table ... USING INDEX
to zły znak,SCAN table ...
jest jeszcze gorszy!
Staraj się unikać SCAN table [using index]
wpisy na wyjściu EXPLAIN QUERY PLAN
w miarę możliwości, ponieważ napotkasz problemy z wydajnością na większych bazach danych. Użyj EXPLAIN QUERY PLAN
do iteracyjnego dodawania lub modyfikowania indeksów, aż nie będzie więcej SCAN table
pojawiają się wpisy.
Optymalizuj zapytania zawierające IS NOT
Sprawdzanie, czy IS NOT ...
jest drogi ponieważ SQLite będzie musiał skanować wszystkie wiersze tabeli, nawet jeśli dana kolumna ma indeks . Indeksy są przydatne tylko wtedy, gdy szukasz konkretnych wartości, tj. porównań z udziałem < (mniejszy), (większe) lub = (równe), ale nie ubiegają się o !=(nierówne).
Sprytna sztuczka polega na tym, że możesz zastąpić WHERE column != value
z WHERE column > value OR column < value
. Użyje to indeksu kolumny i skutecznie wpłynie na wszystkie wiersze, których wartość nie jest równa value
. Podobnie WHERE stringColumn != ''
można zastąpić przez WHERE stringColumn > ''
, ponieważ ciągi można sortować. Jednak stosując tę sztuczkę, upewnij się, że wiesz, jak SQLite obsługuje NULL
porównania. Na przykład SQLite oblicza NULL > ''
jako FALSE
.
Jeśli użyjesz takiej sztuczki porównawczej, istnieje jeszcze jedno zastrzeżenie na wypadek, gdyby zapytanie zawierało WHERE
i ORDER BY
, każda z inną kolumną:spowoduje to, że zapytanie znów będzie nieefektywne. Jeśli to możliwe, użyj tego samego kolumna w WHERE
i ORDER BY
lub zbuduj indeks pokrywający która obejmuje zarówno WHERE
i ORDER BY
kolumna.
Zwiększ szybkość zapisu dzięki zapisowi z wyprzedzeniem
Rejestracja zapisu z wyprzedzeniem (WAL) tryb dziennika znacznie poprawia wydajność zapisu/aktualizacji , w porównaniu z domyślnym wycofaniem tryb dziennika. Jednak, jak udokumentowano tutaj, jest kilka zastrzeżeń . Na przykład tryb WAL nie jest dostępny w niektórych systemach operacyjnych. Ponadto istnieją ograniczone gwarancje spójności danych w przypadku awarii sprzętu . Przeczytaj kilka pierwszych stron, aby zrozumieć, co robisz.
Odkryłem, że polecenie PRAGMA synchronous = NORMAL
zapewnia 3-4-krotne przyspieszenie. Ustawianie journal_mode
do WAL
następnie znacznie poprawia wydajność (około 10x lub więcej, w zależności od systemu operacyjnego).
Oprócz zastrzeżeń, o których już wspomniałem, powinieneś również pamiętać o następujących kwestiach:
- Korzystając z trybu dziennika WAL, obok pliku bazy danych w systemie plików będą znajdować się dwa dodatkowe pliki, które mają taką samą nazwę jak baza danych, ale mają przyrostek „-shm” i „-wal”. Zwykle nie musisz się tym przejmować, ale jeśli miałbyś wysłać bazę danych na inny komputer, gdy aplikacja jest uruchomiona, nie zapomnij dołączyć tych dwóch plików. SQLite skompaktuje te dwa pliki do głównego pliku za każdym razem, gdy zwykle zamykasz wszystkie otwarte połączenia z bazą danych.
- Wydajność wstawiania lub aktualizowania sporadycznie spada, gdy zapytanie wyzwala scalanie zawartości pliku dziennika WAL z głównym plikiem bazy danych. Nazywa się to punktami kontrolnymi , zobacz tutaj.
- Znalazłem, że
PRAGMA
s, które zmieniająjournal_mode
isynchronous
nie wydają się być trwale przechowywane w bazie danych. Dlatego zawsze wykonuj je ponownie za każdym razem, gdy otwieram nowe połączenie z bazą danych, a nie tylko podczas tworzenia tabel po raz pierwszy.
Zmierz wszystko
Za każdym razem, gdy dodajesz poprawki wydajności, pamiętaj o zmierzeniu wpływu. Zautomatyzowane (jednostkowe) testy są do tego świetnym podejściem. Pomagają dokumentować Twoje ustalenia dla Twojego zespołu, a z czasem odkryją odmienne zachowania , np. po aktualizacji do nowszej wersji SQLite. Przykłady tego, co można zmierzyć:
- Jaki jest efekt korzystania z WAL tryb dziennika po cofnięciu tryb? Jaki jest efekt innych (podobno) zwiększających wydajność
PRAGMA
tak? - O ile szybciej po dodaniu/zmienieniu/usunięciu indeksu wykonaj
SELECT
oświadczenia stają się? O ile wolniej robiINSERT/DELETE/UPDATE
oświadczenia stają się? - Ile dodatkowego miejsca na dysku zajmują indeksy?
W przypadku dowolnego z tych testów rozważ powtórzenie ich z różnymi rozmiarami bazy danych. Np. uruchom je na pustej bazie danych, a także na bazie, która zawiera już tysiące (lub miliony) wpisów. Powinieneś także przeprowadzać testy na różnych urządzeniach i systemach operacyjnych, zwłaszcza jeśli Twoje środowisko programistyczne i produkcyjne znacznie się różnią.
Dostosuj rozmiar pamięci podręcznej
SQLite przechowuje tymczasowe informacje w pamięci podręcznej (w pamięci RAM), m.in. podczas budowania wyników SELECT
zapytanie lub podczas manipulowania danymi, które nie zostały jeszcze zatwierdzone. Domyślnie ten rozmiar to marne 2 MB . Nowoczesne komputery stacjonarne mogą zaoszczędzić znacznie więcej. Uruchom PRAGMA cache_size = -kibibytes
aby zwiększyć tę wartość (uwaga na minus znak przed wartością!). Więcej informacji znajdziesz tutaj. Ponownie zmierz jaki wpływ to ustawienie ma na wydajność!
Użyj REPLACE INTO, aby utworzyć lub zaktualizować wiersz
To może nie być tak duża zmiana wydajności, jak to zgrabna sztuczka. Załóżmy, że musisz zaktualizować wiersz w tabeli t
lub utwórz wiersz, jeśli jeszcze nie istnieje. Zamiast używać dwóch zapytań (SELECT
po którym następuje INSERT
lub UPDATE
), użyj REPLACE INTO
(oficjalna dokumentacja).
Aby to zadziałało, dodaj dodatkową fikcyjną kolumnę (np. replacer
) do tabeli t
, który ma UNIQUE
wymusić. Deklaracja kolumny mogłaby m.in. be ... replacer INTEGER UNIQUE ...
która jest częścią twojego CREATE TABLE
oświadczenie. Następnie użyj zapytania, takiego jak
REPLACE INTO t (col1, col2, ..., replacer) VALUES (?,?,...,1)
Code language: SQL (Structured Query Language) (sql)
Kiedy to zapytanie zostanie uruchomione po raz pierwszy, po prostu wykona INSERT
. Kiedy jest uruchamiany po raz drugi, UNIQUE
ograniczenie replacer
kolumna zostanie wyzwolona, a zachowanie rozwiązywania konfliktów spowoduje porzucenie starego wiersza, automatycznie tworząc nowy. Przydatne może być również powiązane polecenie UPSERT.
Wniosek
Gdy liczba wierszy w Twojej bazie danych wzrośnie, poprawki wydajności stają się koniecznością. Indeksy to najczęstsze rozwiązanie. Zamieniają ulepszoną złożoność czasową na zmniejszoną złożoność przestrzenną, poprawiając prędkość odczytu, jednocześnie negatywnie wpływając na wydajność modyfikacji danych. Pokazałem, że przy porównywaniu nierówności należy zachować szczególną ostrożność w SELECT
oświadczenia, ponieważ SQLite nie może używać indeksów do tego rodzaju porównań. Generalnie polecam korzystanie z planera zapytań to wyjaśnia, co dzieje się wewnętrznie dla każdego zapytania SQL. Za każdym razem, gdy coś zmieniasz, mierz wpływ!