SQLite
 sql >> Baza danych >  >> RDS >> SQLite

Pułapki i pułapki SQLite

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,
  • 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:

  1. 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 .
  2. Użyj PRAGMA auto_vacuum = INCREMENTAL podczas tworzenia DB. Zrób to PRAGMA 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łasz PRAGMA incremental_vacuum(N) . To wywołanie odzyskuje do N stron. Oficjalne dokumenty zawierają dalsze szczegóły, a także inne możliwe wartości dla auto_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ą przez PRAGMA freelist_count z PRAGMA page_size .

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 i COMMIT 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 w ORDER 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.


  1. Database
  2.   
  3. Mysql
  4.   
  5. Oracle
  6.   
  7. Sqlserver
  8.   
  9. PostgreSQL
  10.   
  11. Access
  12.   
  13. SQLite
  14.   
  15. MariaDB
  1. Jak zaimplementować SQLCipher podczas korzystania z SQLiteOpenHelper

  2. SQLiteException:tabela już istnieje

  3. Spowodowane przez:android.database.sqlite.SQLiteException:brak takiej tabeli:(kod 1) Android

  4. Jak wstawić obraz do biblioteki trwałości pomieszczenia?

  5. Połączenie z lewej strony SQLite