SQLite to popularna, relacyjna baza danych, którą osadzasz w swojej aplikacji. Istnieje jednak wiele pułapek i pułapek, których należy unikać. W tym artykule omówiono kilka pułapek (i sposoby ich uniknięcia), takich jak korzystanie z ORM-ów, odzyskiwanie miejsca na dysku, zwracanie uwagi na maksymalną liczbę zmiennych zapytań, typy danych w kolumnach i sposób obsługi dużych liczb całkowitych.
Wprowadzenie
SQLite to popularny system relacyjnych baz danych (DB) . Ma bardzo podobny zestaw funkcji do swoich większych braci, takich jak MySQL , które są systemami opartymi na kliencie/serwerze. Jednak SQLite jest wbudowany baza danych . Może być dołączona do twojego programu jako biblioteka statyczna (lub dynamiczna). To upraszcza wdrożenie , ponieważ nie jest wymagany osobny proces serwera. Wiązania i biblioteki opakowujące umożliwiają dostęp do SQLite w większości języków programowania .
Pracowałem intensywnie z SQLite podczas opracowywania BSync w ramach mojej pracy doktorskiej. Ten artykuł to (losowa) lista pułapek i pułapek, na które natknąłem się podczas tworzenia . Mam nadzieję, że uznasz je za przydatne i unikniesz popełniania tych samych błędów, co kiedyś.
Pułapki i pułapki
Używaj bibliotek ORM ostrożnie
Biblioteki Object-Relational Mapping (ORM) wyodrębniają szczegóły z konkretnych silników baz danych i ich składni (takich jak określone instrukcje SQL) do wysokopoziomowego, zorientowanego obiektowo interfejsu API. Istnieje wiele bibliotek stron trzecich (patrz Wikipedia). Biblioteki ORM mają kilka zalet:
- Oni oszczędzają czas podczas opracowywania , ponieważ szybko mapują Twój kod/klasy na struktury DB,
- Są często wieloplatformowe , czyli pozwalają na podmianę konkretnej technologii DB (np. SQLite na MySQL),
- Oferują kod pomocniczy do migracji schematu .
Jednak mają też kilka poważnych wad powinieneś być świadomy:
- Sprawiają, że praca z bazami danych pojawia się łatwe . Jednak w rzeczywistości silniki DB mają skomplikowane szczegóły, które po prostu musisz znać . Gdy coś pójdzie nie tak, np. gdy biblioteka ORM zgłasza wyjątki, których nie rozumiesz, lub gdy wydajność w czasie wykonywania spada, czas programowania zaoszczędzony dzięki użyciu ORM zostanie szybko pochłonięty przez wysiłki wymagane do debugowania problemu . Na przykład, jeśli nie wiesz, jakie indeksy są, będziesz miał trudności z rozwiązywaniem wąskich gardeł wydajności spowodowanych przez ORM, gdy nie tworzy on automatycznie wszystkich wymaganych indeksów. W skrócie:nie ma darmowego obiadu.
- Ze względu na abstrakcję konkretnego dostawcy bazy danych funkcjonalność specyficzna dla dostawcy jest albo trudno dostępna, albo w ogóle niedostępna .
- Istnieje pewne obciążenie obliczeniowe w porównaniu do bezpośredniego pisania i wykonywania zapytań SQL. Powiedziałbym jednak, że ten punkt jest dyskusyjny w praktyce, ponieważ często traci się wydajność po przejściu na wyższy poziom abstrakcji.
Ostatecznie korzystanie z biblioteki ORM jest kwestią osobistych preferencji. Jeśli to zrobisz, po prostu przygotuj się, że będziesz musiał poznać dziwactwa relacyjnych baz danych (i zastrzeżenia specyficzne dla dostawców), gdy wystąpią nieoczekiwane zachowania lub wąskie gardła wydajności.
Dołącz tabelę migracji od początku
Jeśli nie korzystając z biblioteki ORM, będziesz musiał zająć się migracją schematu bazy danych . Wiąże się to z pisaniem kodu migracji, który zmienia schematy tabel i w jakiś sposób przekształca przechowywane dane. Polecam stworzyć tabelę o nazwie „migracje” lub „wersja”, z jednym wierszem i kolumną, która po prostu przechowuje wersję schematu, np. używając monotonicznie rosnącej liczby całkowitej. Dzięki temu funkcja migracji może wykryć, które migracje nadal wymagają zastosowania. Za każdym razem, gdy krok migracji został pomyślnie zakończony, Twój kod narzędzi migracji zwiększa ten licznik za pomocą UPDATE
Instrukcja SQL.
Automatycznie utworzona kolumna wierszy
Za każdym razem, gdy tworzysz tabelę, SQLite automatycznie utworzy INTEGER
kolumna o nazwie rowid
dla ciebie – chyba że podałeś WITHOUT ROWID
klauzula (ale są szanse, że nie wiedziałeś o tej klauzuli). rowid
wiersz jest kolumną klucza podstawowego. Jeśli sam określisz taką kolumnę klucza podstawowego (np. za pomocą składni some_column INTEGER PRIMARY KEY
) ta kolumna będzie po prostu aliasem dla rowid
. Zobacz tutaj, aby uzyskać więcej informacji, które opisują to samo w dość tajemniczych słowach. Zauważ, że SELECT * FROM table
oświadczenie nie uwzględnij rowid
domyślnie – musisz poprosić o rowid
kolumna jawnie.
Zweryfikuj, że PRAGMA
naprawdę działa
Między innymi PRAGMA
instrukcje są używane do konfigurowania ustawień bazy danych lub do wywoływania różnych funkcji (dokumenty urzędowe). Jednak istnieją nieudokumentowane skutki uboczne, w których czasami ustawienie zmiennej w rzeczywistości nie ma żadnego wpływu . Innymi słowy, nie działa i po cichu zawodzi.
Na przykład, jeśli wydasz następujące oświadczenia w podanej kolejności, ostatni oświadczenie nie mieć jakikolwiek wpływ. Zmienna auto_vacuum
nadal ma wartość 0
(NONE
), bez dobrego powodu.
PRAGMA journal_mode = WAL
PRAGMA synchronous = NORMAL
PRAGMA auto_vacuum = INCREMENTAL
Code language: SQL (Structured Query Language) (sql)
Możesz odczytać wartość zmiennej, wykonując PRAGMA variableName
i pomijając znak równości i wartość.
Aby naprawić powyższy przykład, użyj innej kolejności. Korzystanie z kolejności wierszy 3, 1, 2 będzie działać zgodnie z oczekiwaniami.
Możesz nawet uwzględnić takie kontrole w swojej produkcji kod, ponieważ te efekty uboczne mogą zależeć od konkretnej wersji SQLite i sposobu jej zbudowania. Biblioteka używana w produkcji może różnić się od tej używanej podczas tworzenia.
Reklamowanie miejsca na dysku dla dużych baz danych
Domyślnie rozmiar pliku bazy danych SQLite monotonicznie rośnie . Usunięcie wierszy oznacza tylko określone strony jako wolne , aby można było ich użyć do INSERT
dane w przyszłości. Aby faktycznie odzyskać miejsce na dysku i przyspieszyć wydajność, istnieją dwie opcje:
- Wykonaj
VACUUM
oświadczenie . Ma to jednak kilka skutków ubocznych:- Blokuje całą bazę danych. Podczas
VACUUM
nie mogą odbywać się żadne równoległe operacje operacja. - Zajmuje to dużo czasu (w przypadku większych baz danych), ponieważ wewnętrznie odtwarza DB w osobnym, tymczasowym pliku, a na koniec usuwa oryginalną bazę danych, zastępując ją tym plikiem tymczasowym.
- Plik tymczasowy zużywa dodatkowe miejsce na dysku podczas działania operacji. Dlatego nie jest dobrym pomysłem uruchamianie
VACUUM
w przypadku braku miejsca na dysku. Nadal możesz to zrobić, ale musiałbyś regularnie sprawdzać, czy(freeDiskSpace - currentDbFileSize) > 0
.
- Blokuje całą bazę danych. Podczas
- Użyj
PRAGMA auto_vacuum = INCREMENTAL
podczas tworzenia DB. Zrób toPRAGMA
pierwszy oświadczenie po utworzeniu pliku! Umożliwia to pewne wewnętrzne zarządzanie, pomagając bazie danych odzyskać miejsce za każdym razem, gdy wywołaszPRAGMA incremental_vacuum(N)
. To wywołanie odzyskuje doN
stron. Oficjalne dokumenty zawierają dalsze szczegóły, a także inne możliwe wartości dlaauto_vacuum
.- Uwaga:możesz określić, ile wolnego miejsca na dysku (w bajtach) zostanie uzyskane podczas wywoływania
PRAGMA incremental_vacuum(N)
:pomnóż wartość zwróconą przezPRAGMA freelist_count
zPRAGMA page_size
.
- Uwaga:możesz określić, ile wolnego miejsca na dysku (w bajtach) zostanie uzyskane podczas wywoływania
Lepsza opcja zależy od kontekstu. W przypadku bardzo dużych plików baz danych polecam opcję 2 , ponieważ opcja 1 irytowałaby użytkowników minutami lub godzinami oczekiwania na wyczyszczenie bazy danych. Opcja 1 jest odpowiednia dla mniejszych baz danych . Jego dodatkową zaletą jest to, że wydajność DB ulegnie poprawie (co nie ma miejsca w przypadku opcji 2), ponieważ odtwarzanie eliminuje skutki uboczne fragmentacji danych.
Pamiętaj o maksymalnej liczbie zmiennych w zapytaniach
Domyślnie maksymalna liczba zmiennych („parametry hosta”), których można użyć w zapytaniu, jest zakodowana na stałe do 999 (patrz tutaj, sekcja Maksymalna liczba parametrów hosta w pojedynczej instrukcji SQL ). Ten limit może się różnić, ponieważ jest to czas kompilacji parametr, którego domyślna wartość Ty (lub ktokolwiek inny skompilował SQLite) mógł zostać zmieniony.
Jest to problematyczne w praktyce, ponieważ często zdarza się, że aplikacja udostępnia (dowolnie dużą) listę do silnika bazy danych. Na przykład, jeśli chcesz masowo-DELETE
(lub SELECT
) wiersze na podstawie, powiedzmy, listy identyfikatorów. Oświadczenie takie jak
DELETE FROM some_table WHERE rowid IN (?, ?, ?, ?, <999 times "?, ">, ?)
Code language: SQL (Structured Query Language) (sql)
zgłosi błąd i nie zakończy się.
Aby rozwiązać ten problem, rozważ następujące kroki:
- Przeanalizuj swoje listy i podziel je na mniejsze listy,
- Jeśli podział był konieczny, należy użyć opcji
BEGIN TRANSACTION
iCOMMIT
naśladować niepodzielność, jaką miałoby jedno zdanie . - Pamiętaj, aby wziąć pod uwagę również inne
?
zmienne, których możesz użyć w zapytaniu, a które nie są związane z listą przychodzącą (np.?
zmienne używane wORDER BY
warunek), aby całkowita liczba zmiennych nie przekracza limitu.
Alternatywnym rozwiązaniem jest zastosowanie tabel tymczasowych. Chodzi o to, aby utworzyć tabelę tymczasową, wstawić zmienne zapytania jako wiersze, a następnie użyć tej tabeli tymczasowej w podzapytaniu, np.
DROP TABLE IF EXISTS temp.input_data
CREATE TABLE temp.input_data (some_column TEXT UNIQUE)
# Insert input data, running the next query multiple times
INSERT INTO temp.input_data (some_column) VALUES (...)
# The above DELETE statement now changes to this one:
DELETE FROM some_table WHERE rowid IN (SELECT some_column from temp.input_data)
Code language: SQL (Structured Query Language) (sql)
Uważaj na powinowactwo typów SQLite
Kolumny SQLite nie są ściśle typowane, a konwersje niekoniecznie przebiegają tak, jak można by się spodziewać. Podane typy to tylko wskazówki . SQLite często przechowuje dane dowolnych wpisz jego oryginalny typ i konwertuj dane na typ kolumny tylko w przypadku, gdy konwersja jest bezstratna. Na przykład możesz po prostu wstawić "hello"
ciąg do INTEGER
kolumna. SQLite nie będzie narzekać ani ostrzegać o niezgodnościach typu. I odwrotnie, możesz nie oczekiwać, że dane zwrócone przez SELECT
oświadczenie INTEGER
kolumna jest zawsze INTEGER
. Te wskazówki dotyczące typów są określane jako „powinowactwo typów” w języku SQLite, zobacz tutaj. Upewnij się, że dokładnie zapoznałeś się z tą częścią instrukcji SQLite, aby lepiej zrozumieć znaczenie typów kolumn, które określasz podczas tworzenia nowych tabel.
Uważaj na duże liczby całkowite
SQLite obsługuje podpisane 64-bitowe liczby całkowite , który może przechowywać lub wykonywać obliczenia. Innymi słowy, tylko liczby od -2^63
do (2^63) - 1
są obsługiwane, ponieważ do przedstawienia znaku potrzebny jest jeden bit!
Oznacza to, że jeśli spodziewasz się pracować z większymi liczbami, np. 128-bitowe (ze znakiem) liczby całkowite lub 64-bitowe liczby całkowite bez znaku, musisz przekonwertuj dane na tekst przed włożeniem .
Przerażenie zaczyna się, gdy zignorujesz to i po prostu wstawisz większe liczby (jako liczby całkowite). SQLite nie będzie narzekać i przechowywać zaokrąglone zamiast tego numer! Na przykład, jeśli wstawisz 2^63 (co jest już poza obsługiwanym zakresem), SELECT
Wartość ed będzie wynosić 9223372036854776000, a nie 2^63=9223372036854775808. W zależności od używanego języka programowania i biblioteki powiązań zachowanie może się jednak różnić! Na przykład powiązanie Pythona sqlite3 sprawdza, czy nie ma takich przepełnień liczb całkowitych!
Nie używaj REPLACE()
dla ścieżek plików
Wyobraź sobie, że przechowujesz względne lub bezwzględne ścieżki plików w TEXT
kolumna w SQLite, np. do śledzenia plików w rzeczywistym systemie plików. Oto przykład trzech wierszy:
foo/test.txt
foo/bar/
foo/bar/x.y
Załóżmy, że chcesz zmienić nazwę katalogu „foo” na „xyz”. Jakiego polecenia SQL byś użył? Ten?
REPLACE(path_column, old_path, new_path)
Code language: SQL (Structured Query Language) (sql)
Tak robiłem, dopóki nie zaczęły się dziać dziwne rzeczy. Problem z REPLACE()
jest to, że zastąpi wszystkie zdarzenia. Jeśli istnieje wiersz ze ścieżką „foo/bar/foo/”, to REPLACE(column_name, 'foo/', 'xyz/')
siać spustoszenie, ponieważ wynikiem nie będzie „xyz/bar/foo/”, ale „xyz/bar/xyz/”.
Lepszym rozwiązaniem jest coś takiego
UPDATE mytable SET path_column = 'xyz/' || substr(path_column, 4) WHERE path_column GLOB 'foo/*'"
Code language: SQL (Structured Query Language) (sql)
4
odzwierciedla długość starej ścieżki (w tym przypadku „foo/”). Zauważ, że użyłem GLOB
zamiast LIKE
aktualizować tylko te wiersze, które rozpoczyna się z ‘foo/’.
Wniosek
SQLite to fantastyczny silnik bazy danych, w którym większość poleceń działa zgodnie z oczekiwaniami. Jednak konkretne zawiłości, jak te, które właśnie przedstawiłem, nadal wymagają uwagi programisty. Oprócz tego artykułu, koniecznie przeczytaj również oficjalną dokumentację z zastrzeżeniami SQLite.
Czy w przeszłości spotkałeś się z innymi zastrzeżeniami? Jeśli tak, daj mi znać w komentarzach.