Odczyt niezatwierdzony jest najsłabszym z czterech poziomów izolacji transakcji zdefiniowanych w standardzie SQL (i sześciu zaimplementowanych w SQL Server). Pozwala na wszystkie trzy tak zwane „zjawiska współbieżności”, brudne odczyty , niepowtarzalne odczyty i widmo:
Większość osób zajmujących się bazami danych zdaje sobie sprawę z tych zjawisk, przynajmniej w zarysie, ale nie wszyscy zdają sobie sprawę, że nie opisują w pełni oferowanych gwarancji izolacji; nie opisują też intuicyjnie różnych zachowań, jakich można się spodziewać w konkretnej implementacji, takiej jak SQL Server. Więcej o tym później.
Izolacja transakcji – „I” w ACID
Każde polecenie SQL jest wykonywane w ramach transakcji (jawnej, niejawnej lub automatycznego zatwierdzania). Każda transakcja ma skojarzony poziom izolacji, który określa stopień izolacji od skutków innych współbieżnych transakcji. Ta nieco techniczna koncepcja ma ważne implikacje dla sposobu wykonywania zapytań i jakości generowanych wyników.
Rozważ proste zapytanie, które zlicza wszystkie wiersze w tabeli. Gdyby to zapytanie mogło zostać wykonane natychmiast (lub bez równoczesnych modyfikacji danych), poprawna odpowiedź mogłaby być tylko jedna:liczba wierszy fizycznie obecnych w tabeli w danym momencie. W rzeczywistości wykonanie zapytania zajmie pewną ilość czasu, a wynik będzie zależał od tego, ile wierszy faktycznie napotka silnik wykonawczy podczas przechodzenia przez dowolną strukturę fizyczną wybraną w celu uzyskania dostępu do danych.
Jeśli wiersze są dodawane do (lub usuwane) z tabeli przez współbieżne transakcje w trakcie operacji zliczania, można uzyskać różne wyniki w zależności od tego, czy transakcja zliczania wierszy napotka wszystkie, niektóre lub żadne z tych współbieżnych zmian – co z kolei zależy od poziomu izolacji transakcji zliczania wierszy.
W zależności od poziomu izolacji, szczegółów fizycznych i czasu współbieżnych operacji, nasza transakcja zliczania może nawet dać wynik, który nigdy nie był prawdziwym odzwierciedleniem zatwierdzonego stanu tabeli w dowolnym momencie transakcji.
Przykład
Rozważ transakcję zliczania wierszy, która rozpoczyna się w czasie T1 i skanuje tabelę od początku do końca (w kolejności kluczy indeksu klastrowego ze względu na argument). W tej chwili w tabeli jest 100 zatwierdzonych wierszy. Jakiś czas później (w czasie T2) nasza transakcja liczenia napotkała 50 takich wierszy. W tym samym momencie transakcja współbieżna wstawia dwa wiersze do tabeli, a chwilę później w czasie T3 (przed zakończeniem zliczania transakcji) zostaje zatwierdzona. Jeden z wstawionych wierszy znajduje się w połowie struktury indeksu klastrowego, którą nasza transakcja zliczania już przetworzyła, podczas gdy drugi wstawiony wiersz znajduje się w niepoliczonej części.
Po zakończeniu transakcji zliczania wierszy zgłosi 101 wierszy w tym scenariuszu; 100 wierszy początkowo w tabeli plus jeden wstawiony wiersz, który został znaleziony podczas skanowania. Ten wynik jest sprzeczny z zadeklarowaną historią tabeli:było 100 zatwierdzonych wierszy w czasach T1 i T2, a następnie 102 zatwierdzone wiersze w czasie T3. Nigdy nie było 101 zatwierdzonych wierszy.
Zaskakującą rzeczą (być może, w zależności od tego, jak głęboko myślałeś o tych rzeczach wcześniej) jest to, że ten wynik jest możliwy na domyślnym (zablokowanym) poziomie izolacji popełnionej odczytu, a nawet przy powtarzalnej izolacji odczytu. Oba te poziomy izolacji gwarantują odczytanie tylko zatwierdzonych danych, ale uzyskaliśmy wynik, który nie reprezentuje zatwierdzonego stanu bazy danych!
Analiza
Jedynym poziomem izolacji transakcji, który zapewnia pełną izolację od efektów współbieżności, jest możliwość serializacji. Implementacja poziomu izolacji z możliwością serializacji w programie SQL Server oznacza, że transakcja zobaczy najnowsze zatwierdzone dane od momentu, w którym dane zostały po raz pierwszy zablokowane w celu uzyskania dostępu. Ponadto zestaw danych napotkanych podczas serializacji izolacji gwarantuje, że nie zmieni swojego członkostwa przed zakończeniem transakcji.
Przykład liczenia wierszy podkreśla fundamentalny aspekt teorii bazy danych:musimy jasno określić, co oznacza „poprawny” wynik dla bazy danych, w której występują współbieżne modyfikacje, i musimy zrozumieć kompromisy, jakich dokonujemy przy wyborze izolacji poziom niższy niż możliwy do serializacji.
Jeśli potrzebujemy widoku stanu bazy danych z punktu w czasie, powinniśmy użyć izolacji migawki (w przypadku gwarancji na poziomie transakcji) lub odczytać izolację migawki zatwierdzonej (w przypadku gwarancji na poziomie instrukcji). Zauważ jednak, że widok punktu w czasie oznacza, że niekoniecznie operujemy na bieżącym zatwierdzonym stanie bazy danych; w efekcie możemy używać nieaktualnych informacji. Z drugiej strony, jeśli jesteśmy zadowoleni z wyników opartych tylko na zatwierdzonych danych (choć prawdopodobnie z różnych punktów w czasie), możemy zdecydować się na pozostanie przy domyślnym poziomie izolacji zatwierdzonego odczytu z blokowaniem.
Aby mieć pewność generowania wyników (i podejmowania decyzji!) na podstawie najnowszego zestawu zatwierdzonych danych, w przypadku pewnej serii operacji na bazie danych potrzebna byłaby serializowalna izolacja transakcji. Oczywiście ta opcja jest zazwyczaj najdroższa pod względem wykorzystania zasobów i obniżonej współbieżności (w tym zwiększonego ryzyka zakleszczenia).
W przykładzie zliczania wierszy oba poziomy izolacji migawki (SI i RCSI) dadzą wynik 100 wierszy, reprezentujących liczbę zatwierdzonych wierszy na początku instrukcji (i transakcji w tym przypadku). Uruchomienie zapytania przy blokowaniu odczytu zatwierdzonego lub powtarzalnej izolacji odczytu może dać wynik 100, 101 lub 102 wierszy — w zależności od czasu, szczegółowości blokady, pozycji wstawiania wiersza i wybranej metody dostępu fizycznego. W przypadku izolacji z możliwością serializacji wynikiem byłoby 100 lub 102 wiersze, w zależności od tego, która z dwóch współbieżnych transakcji została wykonana jako pierwsza.
Jak zły jest odczyt niezatwierdzony?
Po wprowadzeniu niezatwierdzonej izolacji odczytu jako najsłabszego z dostępnych poziomów izolacji, należy oczekiwać, że zaoferuje ona jeszcze niższe gwarancje izolacji niż blokowanie odczytu zatwierdzonego (kolejny najwyższy poziom izolacji). Rzeczywiście tak; ale pytanie brzmi:o ile jest to gorsze niż blokowanie odczytu popełnionej izolacji?
Aby zacząć od właściwego kontekstu, oto lista głównych efektów współbieżności, które można doświadczyć w przypadku domyślnego poziomu izolacji zatwierdzonego odczytu SQL Server:
- Brak wierszy zatwierdzonych
- Wiersze napotkane wiele razy
- Różne wersje tego samego wiersza napotkane w jednym planie instrukcji/zapytań
- Dane zatwierdzonej kolumny z różnych punktów w czasie w tym samym wierszu (przykład)
Wszystkie te efekty współbieżności wynikają z implementacji blokowania odczytu popełnionego tylko przy bardzo krótkoterminowych blokadach współdzielonych podczas odczytu danych. Poziom izolacji niezatwierdzonych odczytów idzie o krok dalej, nie biorąc w ogóle blokad współdzielonych, co skutkuje dodatkową możliwością „brudnych odczytów”.
Brudne odczyty
Krótko mówiąc, „brudny odczyt” odnosi się do odczytu danych, które są zmieniane przez inną równoczesną transakcję (gdzie „zmiana” obejmuje operacje wstawiania, aktualizowania, usuwania i scalania). Innymi słowy, nieczysty odczyt ma miejsce, gdy transakcja odczytuje dane, które zmodyfikowała inna transakcja, zanim modyfikująca transakcja zatwierdziła lub przerwała te zmiany.
Zalety i wady
Podstawowe zalety niezatwierdzonej izolacji odczytu to zmniejszony potencjał blokowania i zakleszczania z powodu niekompatybilnych blokad (w tym niepotrzebnego blokowania z powodu eskalacji blokad) i prawdopodobnie zwiększona wydajność (poprzez uniknięcie konieczności nabywania i zwalniania współdzielonych blokad).
Najbardziej oczywistą potencjalną wadą izolacji odczytu niezatwierdzonych jest (jak sama nazwa wskazuje), że możemy odczytać niezatwierdzone dane (nawet dane, które nigdy popełnione, w przypadku wycofania transakcji). W bazie danych, w której wycofania są stosunkowo rzadkie, kwestia odczytywania niezatwierdzonych danych może być postrzegana jako zwykły problem z czasem, ponieważ dane, o których mowa, z pewnością zostaną wprowadzone na pewnym etapie i prawdopodobnie wkrótce. Widzieliśmy już niespójności związane z czasem w przykładzie liczenia wierszy (który działał na wyższym poziomie izolacji), więc można zadać sobie pytanie, jak dużym problemem jest odczytywanie danych „zbyt wcześnie”.
Oczywiście odpowiedź zależy od lokalnych priorytetów i kontekstu, ale świadoma decyzja o zastosowaniu niezaangażowanej izolacji z pewnością wydaje się możliwa. Jest jednak więcej do przemyślenia. Implementacja poziomu izolacji do odczytu niezatwierdzonego serwera SQL Server obejmuje pewne subtelne zachowania, o których musimy wiedzieć przed dokonaniem „świadomego wyboru”.
Skanowanie zamówień alokacji
Użycie niezatwierdzonej izolacji do odczytu jest odbierane przez SQL Server jako sygnał, że jesteśmy gotowi zaakceptować niespójności, które mogą powstać w wyniku skanowania z kolejnością alokacji.
Zwykle aparat pamięci masowej może wybrać skanowanie z kolejnością alokacji tylko wtedy, gdy dane bazowe mamy gwarancję, że się nie zmienią podczas skanowania (ponieważ na przykład baza danych jest tylko do odczytu lub określono wskazówkę dotyczącą blokowania tabeli). Jednak w przypadku użycia niezatwierdzonej izolacji odczytu, aparat pamięci masowej może nadal wybrać skanowanie uporządkowane według alokacji, nawet jeśli dane bazowe mogą być modyfikowane przez współbieżne transakcje.
W takich okolicznościach skanowanie uporządkowane według alokacji może całkowicie pominąć niektóre zatwierdzone dane lub napotkać inne zatwierdzone dane więcej niż jeden raz. Nacisk kładziony jest na brakujące lub podwójne liczenie popełnione dane (nie czytanie niezatwierdzonych danych), więc nie jest to przypadek „brudnych odczytów” jako takich. Ta decyzja projektowa (aby zezwolić na skanowanie z kolejnością alokacji w ramach nieprzeczytanej, niezatwierdzonej izolacji) jest postrzegana przez niektórych jako raczej kontrowersyjna.
Jako zastrzeżenie, powinienem jasno powiedzieć, że bardziej ogólne ryzyko pominięcia lub podwójnego liczenia zatwierdzonych wierszy nie ogranicza się do odczytania niezatwierdzonej izolacji. Z pewnością można zaobserwować podobne efekty pod blokowaniem odczytu popełnionego i odczytu powtarzalnego (jak widzieliśmy wcześniej), ale dzieje się to za pośrednictwem innego mechanizmu. Brakujące zatwierdzone wiersze lub wielokrotne ich napotkanie z powodu uporządkowanego skanowania zmieniających się danych jest specyficzny dla używania nieprzeczytanej izolacji.
Czytanie „uszkodzonych” danych
Wyniki, które wydają się zaprzeczać logice (a nawet sprawdzają ograniczenia!) są możliwe przy blokowaniu odczytanej popełnionej izolacji (ponownie, zobacz ten artykuł Craiga Freedmana, aby uzyskać kilka przykładów). Podsumowując, chodzi o to, że blokowanie zatwierdzonego odczytu może zobaczyć zatwierdzone dane z różnych punktów w czasie – nawet dla pojedynczego wiersza, jeśli na przykład plan zapytania wykorzystuje techniki takie jak przecięcie indeksu.
Wyniki te mogą być nieoczekiwane, ale są całkowicie zgodne z gwarancją odczytu tylko zatwierdzonych danych. Nie da się uciec od faktu, że wyższe gwarancje spójności danych wymagają wyższych poziomów izolacji.
Te przykłady mogą być nawet dość szokujące, jeśli nie widziałeś ich wcześniej. Oczywiście te same wyniki są możliwe w przypadku niezatwierdzonej izolacji odczytu, ale zezwolenie na nieczyste odczyty dodaje dodatkowy wymiar:wyniki mogą obejmować zatwierdzone i niezatwierdzone dane z różnych punktów w czasie, nawet dla tego samego wiersza.
Idąc dalej, możliwe jest nawet odczytanie niezatwierdzonej transakcji, która odczyta wartość w jednej kolumnie w mieszanym stanie danych zatwierdzonych i niezatwierdzonych. Może się to zdarzyć podczas odczytywania wartości LOB (na przykład xml lub dowolnego typu „max”), jeśli wartość jest przechowywana na wielu stronach danych. Niezatwierdzony odczyt może napotkać zatwierdzone lub niezatwierdzone dane z różnych punktów w czasie na różnych stronach, co skutkuje ostateczną wartością w pojedynczej kolumnie, która jest mieszanką wartości!
Weźmy przykład, rozważmy pojedynczą kolumnę varchar(max), która początkowo zawiera 10 000 znaków „x”. Jednoczesna transakcja aktualizuje tę wartość do 10 000 znaków „y”. Odczyt niezatwierdzonej transakcji może odczytywać znaki „x” z jednej strony LOB i znaki „y” z drugiej, w wyniku czego końcowa wartość odczytu zawiera kombinację znaków „x” i „y”. Trudno argumentować, że nie oznacza to odczytywania „uszkodzonych” danych.
Demo
Utwórz tabelę klastrową z jednym wierszem danych LOB:
CREATE TABLE dbo.Test ( RowID integer PRIMARY KEY, LOB varchar(max) NOT NULL, ); INSERT dbo.Test (RowID, LOB) VALUES (1, REPLICATE(CONVERT(varchar(max), 'X'), 16100));
W oddzielnej sesji uruchom następujący skrypt, aby odczytać wartość LOB podczas odczytu niezatwierdzonej izolacji:
-- Run this in session 2 SET NOCOUNT ON; DECLARE @ValueRead varchar(max) = '', @AllXs varchar(max) = REPLICATE(CONVERT(varchar(max), 'X'), 16100), @AllYs varchar(max) = REPLICATE(CONVERT(varchar(max), 'Y'), 16100); WHILE 1 = 1 BEGIN SELECT @ValueRead = T.LOB FROM dbo.Test AS T WITH (READUNCOMMITTED) WHERE T.RowID = 1; IF @ValueRead NOT IN (@AllXs, @AllYs) BEGIN PRINT LEFT(@ValueRead, 8000); PRINT RIGHT(@ValueRead, 8000); BREAK; END END;
W pierwszej sesji uruchom ten skrypt, aby zapisać naprzemienne wartości w kolumnie LOB:
-- Run this in session 1 SET NOCOUNT ON; DECLARE @AllXs varchar(max) = REPLICATE(CONVERT(varchar(max), 'X'), 16100), @AllYs varchar(max) = REPLICATE(CONVERT(varchar(max), 'Y'), 16100); WHILE 1 = 1 BEGIN UPDATE dbo.Test SET LOB = @AllYs WHERE RowID = 1; UPDATE dbo.Test SET LOB = @AllXs WHERE RowID = 1; END;
Po krótkim czasie skrypt w sesji drugiej zakończy działanie po odczytaniu stanu mieszanego dla wartości LOB, na przykład:
Ten konkretny problem jest ograniczony do odczytów wartości kolumn LOB, które są rozłożone na wielu stronach, nie z powodu jakichkolwiek gwarancji zapewnianych przez poziom izolacji, ale dlatego, że SQL Server używa zatrzasków na poziomie strony w celu zapewnienia integralności fizycznej. Efektem ubocznym tych szczegółów implementacji jest zapobieganie takim „uszkodzonym” odczytom danych, jeśli dane dla pojedynczej operacji odczytu znajdują się na jednej stronie.
W zależności od posiadanej wersji programu SQL Server, jeśli dane „stanu mieszanego” zostaną odczytane dla kolumny xml, otrzymasz albo błąd wynikający z prawdopodobnie zniekształconego wyniku xml, brak błędu lub błąd specyficzny 601. , "nie można kontynuować skanowania za pomocą NOLOCK z powodu przenoszenia danych." Odczytywanie danych o stanie mieszanym dla innych typów obiektów LOB zazwyczaj nie skutkuje wyświetleniem komunikatu o błędzie; konsumująca aplikacja lub zapytanie nie ma możliwości dowiedzenia się, że właśnie doświadczyła najgorszego rodzaju brudnego odczytu. Aby zakończyć analizę, wiersz o stanie mieszanym innym niż LOB odczytany w wyniku przecięcia indeksu nigdy nie jest zgłaszany jako błąd.
Komunikat tutaj jest taki, że jeśli używasz niezatwierdzonej izolacji odczytu, akceptujesz, że brudne odczyty obejmują możliwość odczytania „uszkodzonych” wartości LOB w stanie mieszanym.
Podpowiedź NOLOCK
Przypuszczam, że żadna dyskusja na temat odczytu niezatwierdzonego poziomu izolacji nie byłaby kompletna, gdyby nie wspomnieć o tej (powszechnie nadużywanej i źle rozumianej) wskazówce tabeli. Sama wskazówka jest tylko synonimem wskazówki dotyczącej tabeli READUNCOMMITTED. Pełni dokładnie tę samą funkcję:dostęp do obiektu, do którego jest stosowany, jest uzyskiwany przy użyciu semantyki izolacji niezatwierdzonej odczytu (choć istnieje wyjątek).
Jeśli chodzi o nazwę „NOLOCK”, oznacza to po prostu, że podczas odczytu danych nie są brane żadne współdzielone blokady . Inne blokady (stabilność schematu, blokady na wyłączność na modyfikację danych itd.) są nadal przyjmowane jak zwykle.
Ogólnie rzecz biorąc, wskazówki NOLOCK powinny być mniej więcej tak powszechne, jak inne wskazówki dotyczące tabeli poziomu izolacji dla poszczególnych obiektów, takie jak SERIALIZABLE i READCOMMITTEDLOCK. To znaczy:wcale nie bardzo powszechne i używane tylko tam, gdzie nie ma dobrej alternatywy, dobrze zdefiniowanego celu i kompletnego zrozumienie konsekwencji.
Jednym z przykładów legalnego użycia NOLOCK (lub READUNCOMMITTED) jest dostęp do DMV lub innych widoków systemowych, gdzie wyższy poziom izolacji może powodować niechcianą rywalizację o struktury danych niebędące użytkownikami. Innym skrajnym przykładem może być sytuacja, w której zapytanie musi uzyskać dostęp do znacznej części dużej tabeli, co gwarantuje, że nigdy nie nastąpią zmiany danych podczas wykonywania zapytania ze wskazówką. Musiałby istnieć dobry powód, aby zamiast tego nie używać migawki lub odczytywać izolacji zatwierdzonej migawki, a oczekiwany wzrost wydajności musiałby zostać przetestowany, zweryfikowany i porównany, powiedzmy, z użyciem pojedynczej wskazówki dotyczącej blokady wspólnej tabeli.
Najmniej pożądanym zastosowaniem NOLOCKa jest to, które jest niestety najczęstsze:zastosowanie go do każdego obiektu w zapytaniu jako rodzaj szybszego magicznego przełącznika. Przy najlepszej woli na świecie nie ma lepszego sposobu, aby kod SQL Server wyglądał zdecydowanie amatorsko. Jeśli w uzasadniony sposób potrzebujesz odczytać niezatwierdzoną izolację dla zapytania, bloku kodu lub modułu, prawdopodobnie lepiej jest odpowiednio ustawić poziom izolacji sesji i podać komentarze uzasadniające działanie.
Ostateczne myśli
Odczyt bez zobowiązań jest uzasadnionym wyborem dla poziomu izolacji transakcji, ale musi to być świadomy wybór. Przypominamy, że oto niektóre ze zjawisk współbieżności możliwych w ramach domyślnego blokowania odczytu popełnionej izolacji SQL Server:
- Brakujące wcześniej zatwierdzone wiersze
- Wiersze zatwierdzone wielokrotnie napotkano
- Różne zatwierdzone wersje tego samego wiersza napotkano w jednym planie instrukcji/zapytań
- Zatwierdzone dane z różnych punktów w czasie w tym samym wierszu (ale w różnych kolumnach)
- Zatwierdzone odczyty danych, które wydają się być sprzeczne z włączonymi i zaznaczonymi ograniczeniami
W zależności od twojego punktu widzenia, może to być dość szokująca lista możliwych niespójności dla domyślnego poziomu izolacji. Do tej listy przeczytaj niezatwierdzoną izolację dodaje:
- Brudne odczyty (napotkane dane, które jeszcze nie zostały i mogą nigdy nie zostać zatwierdzone)
- Wiersze zawierające mieszankę danych zatwierdzonych i niezatwierdzonych
- Pominięte/zduplikowane wiersze z powodu skanowań uporządkowanych według alokacji
- Poszczególne (jednokolumnowe) wartości LOB w stanie mieszanym („uszkodzonym”)
- Błąd 601 – „nie można kontynuować skanowania z NOLOCK z powodu przeniesienia danych” (przykład).
Jeśli Twoje główne problemy transakcyjne dotyczą skutków ubocznych blokowania izolacji zatwierdzonej do odczytu — blokowanie, blokowanie narzutów, zmniejszona współbieżność z powodu eskalacji blokady itp. — może być lepiej obsługiwany przez poziom izolacji wersjonowania wierszy, taki jak izolacja migawek odczytu popełnionego (RCSI) lub izolacja migawki (SI). Nie są one jednak bezpłatne, a aktualizacje w ramach RCSI mają pewne zachowania sprzeczne z intuicją.
W przypadku scenariuszy wymagających najwyższych poziomów gwarancji spójności jedynym bezpiecznym wyborem pozostaje serializacja. W przypadku operacji krytycznych dla wydajności na danych tylko do odczytu (na przykład dużych baz danych, które są efektywnie tylko do odczytu między oknami ETL), dobrym wyborem może być również jawne ustawienie bazy danych na READ_ONLY (blokady współdzielone nie są tylko do odczytu i nie ma ryzyka niespójności).
Będzie również stosunkowo niewielka liczba aplikacji, dla których izolacja niezatwierdzona do odczytu jest właściwym wyborem. Aplikacje te muszą być zadowolone z przybliżonych wyników i możliwości sporadycznie niespójnych, pozornie niepoprawnych (pod względem ograniczeń) lub „prawdopodobnie uszkodzonych” danych. Jeśli dane zmieniają się stosunkowo rzadko, ryzyko tych niespójności jest odpowiednio niższe.
[ Zobacz indeks dla całej serii ]