Transakcja to pakiet typu „wszystko albo nic”
Transakcja grupuje kilka instrukcji tak, że albo wszystkie zostają wykonane, albo żadna. Jeśli coś pójdzie nie tak w połowie, możesz wycofać zmiany i baza wraca dokładnie do stanu początkowego.
Klasyczny przykład to przelew pieniędzy:
Te dwa UPDATE należą do siebie. Gdyby baza padła między nimi, Ada byłaby o 2000 centów biedniejsza, a Boris nie dostałby nic. Ujęcie ich w BEGIN ... COMMIT sprawia, że para jest atomowa: wykonują się obie albo żadna.
Autocommit: domyślny tryb, którego już używasz
Każda instrukcja SQL uruchomiona do tej pory była transakcją. SQLite domyślnie działa w trybie autocommit: każda instrukcja dostaje własne, niejawne BEGIN i COMMIT.
Trzy wstawienia, trzy osobne transakcje, trzy zapisy na dysk z fsync. To wystarcza przy pojedynczych zapisach, ale przy masowym ładowaniu danych jest wolne, a do tego nie możesz cofnąć grupy instrukcji jako całości. BEGIN wyłącza autocommit do najbliższego COMMIT albo ROLLBACK.
ROLLBACK: udajemy, że nic się nie stało
ROLLBACK odrzuca wszystko, co zrobiono od odpowiadającego mu BEGIN. Baza wraca do stanu sprzed transakcji.
Znikają zarówno UPDATE, jak i DELETE: tabela wygląda tak jak przed BEGIN. To siatka bezpieczeństwa, dzięki której kod aplikacji może czysto przerwać operację złożoną z wielu instrukcji, gdy w połowie trafi na błąd.
Przy okazji: naruszenie ograniczenia wewnątrz transakcji nie wycofuje automatycznie całej transakcji. Wycofuje tylko instrukcję, która je spowodowała, i zostawia transakcję otwartą, czekając na twoją decyzję. Jeśli chcesz „wszystko albo nic”, aplikacja musi wykonać ROLLBACK, gdy zobaczy błąd.
Przyspieszanie masowych wstawień
Ponieważ każda instrukcja w trybie autocommit wykonuje własny fsync, ujęcie paczki w jedną transakcję jest często nawet 100 razy szybsze:
Jedna synchronizacja z dyskiem przy COMMIT zamiast jednej na każdy wiersz. Jeśli kiedyś importujesz tysiące wierszy i zastanawiasz się, czemu to się tak wlecze, odpowiedź prawie zawsze jest właśnie ta.
DEFERRED, IMMEDIATE, EXCLUSIVE
BEGIN przyjmuje tryb, który decyduje, kiedy SQLite zakłada blokady:
BEGIN DEFERRED(domyślny): żadnej blokady, dopóki nie zaczniesz czytać lub pisać. Blokada zapisu jest zakładana leniwie, przy pierwszej instrukcji zapisującej.BEGIN IMMEDIATE: blokada zapisu od razu. Inne połączenia nadal mogą czytać, ale żadne inne połączenie nie może zacząć pisać.BEGIN EXCLUSIVE: jakIMMEDIATE, a do tego żadne inne połączenie nie może też czytać. W trybie WAL działa tak samo jakIMMEDIATE; różnica ma znaczenie tylko w starszym trybie rollback journal.
BEGIN DEFERRED; -- to samo co zwykłe BEGIN
BEGIN IMMEDIATE; -- zarezerwuj blokadę zapisu teraz
BEGIN EXCLUSIVE; -- zarezerwuj wszystko (tryb rollback journal)
Ten wybór ma znaczenie dla współbieżności. Przy zwykłym BEGIN dwa połączenia mogą rozpocząć transakcję, spokojnie czytać, a potem ścigać się przy próbie zapisu: to, które drugie poprosi o blokadę zapisu, dostaje SQLITE_BUSY, a co gorsza, wykonało już odczyty, które teraz musi wyrzucić.
BEGIN IMMEDIATE to rozwiązuje: jeśli wiesz, że będziesz pisać, najpierw poproś o blokadę zapisu. Drugie połączenie zostaje zablokowane (albo szybko dostaje błąd) od razu, zanim wykona pracę, którą musiałoby odrzucić.
Praktyczna zasada: jeśli twoja transakcja będzie zapisywać dane, użyj BEGIN IMMEDIATE.
Odczyty w transakcji widzą migawkę
Dopóki transakcja jest otwarta, twoje odczyty widzą spójną migawkę bazy z chwili rozpoczęcia transakcji (w trybie WAL) albo z chwili pierwszego odczytu (w trybie rollback journal). Zmiany zatwierdzane przez inne połączenia nie pojawią się nagle w twoich zapytaniach.
Widzisz własne niezatwierdzone zapisy; inne połączenia ich nie widzą. Po COMMIT nowa wartość staje się widoczna dla wszystkich. To właśnie mają na myśli ludzie, którzy mówią, że SQLite jest serializable: nie ma przełącznika READ COMMITTED, bo domyślny poziom jest już najsilniejszy.
Transakcja w kodzie aplikacji
W prawdziwym programie schemat to zwykle try/except (albo try/catch) wokół treści, z ROLLBACK na ścieżce błędu:
-- Pseudokod dla dowolnej biblioteki klienckiej
BEGIN IMMEDIATE;
try:
UPDATE accounts SET cents = cents - 2000 WHERE owner = 'Ada';
UPDATE accounts SET cents = cents + 2000 WHERE owner = 'Boris';
COMMIT;
except:
ROLLBACK;
raise;
Większość bibliotek klienckich (sqlite3 w Pythonie, better-sqlite3 itd.) opakowuje to za ciebie w blok with albo pomocniczą funkcję transaction(). Warto sprawdzić dokumentację swojej biblioteki: domyślne ustawienia nie zawsze są takie, jakich się spodziewasz. Zwłaszcza sqlite3 w Pythonie miał historycznie dziwne zachowanie autocommit; nowsze wersje dodały porządny parametr autocommit, który to naprawia.
Na czym ludzie się potykają
- DDL wewnątrz transakcji działa.
CREATE TABLE,ALTER TABLE, a nawetDROP TABLEmożna wycofać. Pod tym względem SQLite jest nietypowy: wiele baz danych automatycznie zatwierdza DDL. VACUUMnie może działać wewnątrz transakcji. Podobnie kilka innych poleceń konserwacyjnych. Uruchamiaj je w trybie autocommit.- Nieudany
COMMITto wciąż prawdziwy błąd. JeśliCOMMITzwracaSQLITE_BUSY(rzadko, ale możliwe), transakcja nie jest zatwierdzona. Twój kod musi to obsłużyć, zwykle przez ponowienie próby. - Długie transakcje blokują piszących. Transakcja otwarta przez kilka minut przez kilka minut blokuje inne zapisy. Otwieraj je późno, zatwierdzaj szybko.
Dalej: savepointy
BEGIN i COMMIT działają na zasadzie „wszystko albo nic”. Czasem chcesz wycofać tylko część transakcji, na przykład porzucić jeden ryzykowny krok, a resztę zachować. Do tego służą savepointy i to one są następne.
Najczęściej zadawane pytania
Jak rozpocząć transakcję w SQLite?
Uruchom BEGIN; (albo BEGIN TRANSACTION;), wykonaj swoją pracę, a potem COMMIT;, żeby ją zapisać, albo ROLLBACK;, żeby ją odrzucić. Bez jawnego BEGIN każda instrukcja działa we własnej, automatycznie zatwierdzanej transakcji.
Czym różnią się BEGIN, BEGIN IMMEDIATE i BEGIN EXCLUSIVE?
BEGIN (to samo co BEGIN DEFERRED) nie zakłada blokady zapisu, dopóki faktycznie czegoś nie zapiszesz, więc później może się nie udać z błędem SQLITE_BUSY, jeśli ktoś inny był szybszy. BEGIN IMMEDIATE zakłada blokadę zapisu od razu. BEGIN EXCLUSIVE idzie dalej i blokuje też innych czytelników (ma to znaczenie tylko poza trybem WAL).
Czy SQLite obsługuje poziomy izolacji transakcji?
Nie w sensie standardu SQL. SQLite w praktyce działa jak SERIALIZABLE: transakcja widzi spójny obraz danych, a zapisy są wykonywane po kolei. Nie ma przełączników READ COMMITTED ani REPEATABLE READ: wybierasz między DEFERRED, IMMEDIATE i EXCLUSIVE, co decyduje o tym, kiedy zakładane są blokady, a nie o tym, co widzisz.
Czy SQLite obsługuje zagnieżdżone transakcje?
Nie bezpośrednio: nie możesz wywołać BEGIN wewnątrz innego BEGIN. Do zagnieżdżania służą SAVEPOINT i RELEASE / ROLLBACK TO, które pozwalają wycofać część zmian w obrębie jednej transakcji. Omawiamy to na następnej stronie.