Menu

Transakcje w SQLite: BEGIN, COMMIT i ROLLBACK

Jak działają transakcje w SQLite: BEGIN, COMMIT, ROLLBACK, tryb autocommit oraz tryby DEFERRED/IMMEDIATE/EXCLUSIVE, które decydują, kiedy zakładane są blokady.

Na tej stronie są działające edytory: edytuj, uruchamiaj i od razu zobacz wynik.

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: jak IMMEDIATE, a do tego żadne inne połączenie nie może też czytać. W trybie WAL działa tak samo jak IMMEDIATE; 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 nawet DROP TABLE można wycofać. Pod tym względem SQLite jest nietypowy: wiele baz danych automatycznie zatwierdza DDL.
  • VACUUM nie może działać wewnątrz transakcji. Podobnie kilka innych poleceń konserwacyjnych. Uruchamiaj je w trybie autocommit.
  • Nieudany COMMIT to wciąż prawdziwy błąd. Jeśli COMMIT zwraca SQLITE_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.

Ilustracja języków programowania w Coddy

Ucz się programowania z Coddy

ZACZNIJ