Předchozí příspěvek v této sérii ukázal, jak příkaz T-SQL běžící pod izolací snímku potvrzeného čtení (RCSI ) normálně vidí snímek potvrzeného stavu databáze, jaký byl, když se příkaz spustil. To je dobrý popis toho, jak věci fungují u příkazů, které čtou data, ale existují důležité rozdíly pro příkazy spuštěné pod RCSI, které upravují existující řádky .
Kladu důraz na úpravu stávajících řádků výše, protože následující úvahy platí pouze pro UPDATE
a DELETE
operace (a odpovídající akce MERGE
prohlášení). Aby bylo jasno, INSERT
výroky jsou konkrétně vyloučeny z chování, které se chystám popsat, protože vložky nemodifikují existující data.
Aktualizujte zámky a verze řádků
První rozdíl spočívá v tom, že příkazy pro aktualizaci a odstranění nečtou verze řádků pod RCSI při hledání zdrojových řádků k úpravě. Aktualizace a odstranění příkazů pod RCSI místo toho získá zámky aktualizace při hledání kvalifikačních řádků. Použití aktualizačních zámků zajistí, že operace vyhledávání najde řádky, které lze upravit pomocí nejnovějších potvrzených dat .
Bez aktualizačních zámků by bylo vyhledávání založeno na možná zastaralé verzi datové sady (zavázaná data, jaká byla při zahájení příkazu k úpravě dat). To vám může připomenout příklad spouštěče, který jsme viděli minule, kde READCOMMITTEDLOCK
nápověda byla použita k návratu z RCSI k zamykací implementaci izolace potvrzení o čtení. Tato nápověda byla v tomto příkladu vyžadována, aby se zabránilo založení důležité akce na zastaralých informacích. Zde se používá stejný druh uvažování. Jeden rozdíl je v tom, že READCOMMITTEDLOCK
hint získává sdílené zámky namísto aktualizačních zámků. Kromě toho SQL Server automaticky získává aktualizační zámky, aby chránil úpravy dat pod RCSI, aniž bychom museli přidávat explicitní nápovědu.
Použitím aktualizačních zámků také zajistíte, že se příkaz k aktualizaci nebo odstranění zablokuje pokud narazí na nekompatibilní zámek, například výhradní zámek chránící úpravu dat za letu provedenou jinou souběžnou transakcí.
Další komplikací je, že upravené chování pouze platí k tabulce, která je cílem operace aktualizace nebo odstranění. Další tabulky ve stejném odstranit nebo aktualizovat prohlášení, včetně dalších odkazů do cílové tabulky pokračujte v používání verzí řádků .
Některé příklady jsou pravděpodobně vyžadovány, aby bylo toto matoucí chování trochu jasnější…
Nastavení testu
Následující skript zajistí, že jsme všichni nastaveni na používání RCSI, vytvoří jednoduchou tabulku a přidá do ní dva ukázkové řádky:
ALTER DATABASE Sandpit SET READ_COMMITTED_SNAPSHOT ON WITH ROLLBACK IMMEDIATE; GO SET TRANSACTION ISOLATION LEVEL READ COMMITTED; GO CREATE TABLE dbo.Test ( RowID integer PRIMARY KEY, Data integer NOT NULL ); GO INSERT dbo.Test (RowID, Data) VALUES (1, 1234), (2, 2345);
Další krok je třeba spustit v samostatné relaci . Zahájí transakci a odstraní oba řádky z testovací tabulky (zdá se to divné, ale brzy to bude dávat smysl):
BEGIN TRANSACTION; DELETE dbo.Test WHERE RowID IN (1, 2);
Upozorňujeme, že transakce je záměrně nechána otevřená . Tím se zachovají výhradní zámky na obou řádcích, které se odstraňují (spolu s obvyklými výhradními zámky na obsahující stránce a samotné tabulce), jak lze použít k zobrazení následujícího dotazu:
SELECT resource_type, resource_description, resource_associated_entity_id, request_mode, request_status FROM sys.dm_tran_locks WHERE request_session_id = @@SPID;
Test výběru
Přepnutí zpět na původní relaci , první věc, kterou chci ukázat, je, že běžné příkazy select používající RCSI stále vidí, že se dva řádky mažou. Výběrový dotaz níže používá verze řádků k vrácení nejnovějších potvrzených dat v době, kdy příkaz začíná:
SELECT * FROM dbo.Test;
V případě, že se to zdá překvapivé, pamatujte, že zobrazení řádků jako smazaných by znamenalo zobrazení nepotvrzeného zobrazení dat, což není povoleno při izolaci potvrzení o čtení.
Test odstranění
Navzdory úspěchu vybraného testu, pokus o smazání tyto stejné řádky z aktuální relace budou zablokovány. Můžete si představit, že k tomuto zablokování dochází, když se operace pokouší získat exkluzivní zámky, ale není tomu tak.
Odstranění nepoužívá verzování řádků k vyhledání řádků k odstranění; místo toho se pokusí získat aktualizační zámky. Zámky aktualizace nejsou kompatibilní s výhradními zámky řádků drženými relací s otevřenou transakcí, takže dotaz blokuje:
DELETE dbo.Test WHERE RowID IN (1, 2);
Odhadovaný plán dotazů pro tento příkaz ukazuje, že řádky, které mají být odstraněny, jsou identifikovány běžnou vyhledávací operací předtím, než samostatný operátor provede skutečné odstranění:
Zámky držené v této fázi můžeme vidět spuštěním stejného zamykacího dotazu jako předtím (z jiné relace), přičemž nezapomeňte změnit odkaz SPID na odkaz používaný blokovaným dotazem. Výsledky vypadají takto:
Náš dotaz na odstranění je blokován operátorem Clustered Index Seek, který čeká na získání aktualizačního zámku pro čtení data. To ukazuje, že vyhledáním řádků k odstranění pod RCSI se získá aktualizační zámky spíše než čtení potenciálně zastaralých verzovaných dat. Ukazuje také, že zablokování není způsobeno odstraněním části operace čekající na získání exkluzivního zámku.
Test aktualizace
Zrušte blokovaný dotaz a zkuste místo toho následující aktualizaci:
UPDATE dbo.Test SET Data = Data + 1000 WHERE RowID IN (1, 2);
Odhadovaný plán provádění je podobný plánu uvedenému v testu odstranění:
Compute Scalar je zde proto, aby určil výsledek přidání 1000 k aktuální hodnotě sloupce Data v každém řádku, který je čten Clustered Index Seek. Toto prohlášení bude také zablokováno při spuštění kvůli aktualizačnímu zámku požadovanému operací čtení. Snímek obrazovky níže ukazuje zablokování, když se dotaz zablokuje:
Stejně jako dříve je dotaz při hledání zablokován a čeká na uvolnění nekompatibilního exkluzivního zámku, aby bylo možné získat aktualizační zámek.
Test vložení
Další test obsahuje příkaz, který vloží nový řádek do naší testovací tabulky pomocí hodnoty sloupce Data ze stávajícího řádku s ID 1 v tabulce. Připomeňme, že tento řádek je stále výhradně uzamčen relací s otevřenou transakcí:
INSERT dbo.Test (RowID, Data) SELECT 3, Data FROM dbo.Test WHERE RowID = 1;
Plán provádění je opět podobný jako u předchozích testů:
Tentokrát dotaz není blokován . To ukazuje, že při čtení nebyly získány zámky aktualizace údaje pro vložku. Tento dotaz místo toho použil verzování řádků k získání hodnoty sloupce Data pro nově vložený řádek. Aktualizační zámky nebyly získány, protože tento příkaz nenašel žádné řádky k úpravě , pouze čte data pro použití v insertu.
Tento nový řádek v tabulce můžeme vidět pomocí výběrového testovacího dotazu z dříve:
Všimněte si, že jsme moci aktualizovat a odstranit nový řádek (což bude vyžadovat aktualizační zámky), protože neexistuje žádný konfliktní výhradní zámek. Relace s otevřenou transakcí má výhradní zámky pouze na řádcích 1 a 2:
-- Update the new row UPDATE dbo.Test SET Data = 9999 WHERE RowID = 3; -- Show the data SELECT * FROM dbo.Test; -- Delete the new row DELETE dbo.Test WHERE RowID = 3;
Tento test potvrzuje, že příkazy vložení nezískávají při čtení aktualizační zámky , protože na rozdíl od aktualizací a mazání se neupravují existující řádek. Čtená část přílohy používá normální chování při verzování řádků RCSI.
Test více referencí
Již jsem zmínil, že pouze odkaz na jedinou tabulku používaný k vyhledání řádků k úpravě získává aktualizační zámky; ostatní tabulky ve stejném příkazu update nebo delete stále čtou verze řádků. Zvláštním případem tohoto obecného principu je příkaz k úpravě dat s více odkazy na stejnou tabulku použije aktualizační zámky pouze na jednu instanci slouží k vyhledání řádků k úpravě. Tento závěrečný test ilustruje toto složitější chování krok za krokem.
První věc, kterou budeme potřebovat, je nový třetí řádek pro naši testovací tabulku, tentokrát s nulou ve sloupci Data:
INSERT dbo.Test (RowID, Data) VALUES (3, 0);
Jak se očekávalo, toto vložení pokračuje bez blokování, výsledkem je tabulka, která vypadá takto:
Pamatujte, že druhá relace je stále exkluzivní zámky na řádcích 1 a 2 v tomto bodě. V případě potřeby můžeme volně získat zámky na řádku 3. Následující dotaz je ten, který použijeme k zobrazení chování s více odkazy na cílovou tabulku:
-- Multi-reference update test UPDATE WriteRef SET Data = ReadRef.Data * 2 OUTPUT ReadRef.RowID, ReadRef.Data, INSERTED.RowID AS UpdatedRowID, INSERTED.Data AS NewDataValue FROM dbo.Test AS ReadRef JOIN dbo.Test AS WriteRef ON WriteRef.RowID = ReadRef.RowID + 2 WHERE ReadRef.RowID = 1;
Jedná se o složitější dotaz, ale jeho obsluha je poměrně jednoduchá. Existují dva odkazy na testovací tabulku, jeden mám alias jako ReadRef a druhý jako WriteRef. Cílem je číst z řádku 1 (pomocí verze řádku) přes ReadRef a do aktualizace třetí řádek (který bude vyžadovat aktualizační zámek) pomocí WriteRef.
Dotaz specifikuje řádek 1 explicitně v klauzuli where pro odkaz na tabulku čtení. Připojí se k odkazu na zápis do stejné tabulky přidáním 2 k tomuto RowID (takže identifikuje řádek 3). Příkaz aktualizace také používá výstupní klauzuli k vrácení sady výsledků zobrazující hodnoty načtené ze zdrojové tabulky a výsledné změny provedené na řádku 3.
Odhadovaný plán dotazů pro tento příkaz je následující:
Vlastnosti hledání označené (1) ukázat, že toto hledání je na ReadRef alias, čtení dat z řádku s RowID 1:
Tato operace hledání nenajde řádek, který bude aktualizován, takže zámky aktualizace nejsou přijato; čtení se provádí pomocí verzovaných dat. Čtení není blokováno výhradními zámky drženými druhou relací.
Výpočetní skalár označený (2) definuje výraz označený 1004, který vypočítává aktualizovanou hodnotu sloupce Data. Výraz 1009 vypočítá ID řádku, které se má aktualizovat (1 + 2 =ID řádku 3):
Druhé hledání je odkaz na stejnou tabulku (3). Toto hledání vyhledá řádek, který bude aktualizován (řádek 3) pomocí výrazu 1009:
Protože toto hledání vyhledá řádek, který má být změněn, zámek aktualizace místo použití řádkových verzí. Na řádku ID 3 není žádný konfliktní výhradní zámek, takže požadavek na zámek je udělen okamžitě.
Poslední zvýrazněný operátor (4) je samotná operace aktualizace. Zámek aktualizace na řádku 3 je upgradován na exkluzivní zamknout v tomto okamžiku, těsně před tím, než je úprava skutečně provedena. Tento operátor také vrátí data uvedená v klauzuli výstupu aktualizačního prohlášení:
Výsledek aktualizačního příkazu (generovaného výstupní klauzulí) je uveden níže:
Konečný stav tabulky je uveden níže:
Zámky provedené během provádění můžeme potvrdit pomocí trasování Profiler:
To ukazuje, že pouze jedna aktualizace je získán zámek klíče řady. Když tento řádek dosáhne operátora aktualizace, zámek se převede na exkluzivní zámek. Na konci příkazu se zámek uvolní.
Z výstupu trasování můžete vidět, že hodnota hash zámku pro řádek uzamčený aktualizací je (98ec012aa510) v mé testovací databázi. Následující dotaz ukazuje, že tato hodnota hash zámku je skutečně přidružena k RowID 3 v seskupeném indexu:
SELECT RowID, %%LockRes%% FROM dbo.Test;
Všimněte si, že aktualizační zámky použité v těchto příkladech mají kratší životnost než aktualizační zámky přijaté, pokud zadáme UPDLOCK
náznak. Tyto vnitřní aktualizační zámky jsou uvolněny na konci příkazu, zatímco UPDLOCK
zámky jsou drženy do konce transakce.
Tím končí ukázka případů, kdy RCSI získává aktualizační zámky pro čtení aktuálních potvrzených dat namísto použití verzování řádků.
Sdílené zámky a zámky s rozsahem klíčů pod RCSI
Existuje řada dalších scénářů, kde databázový stroj může stále získat zámky pod RCSI. Všechny tyto situace souvisí s potřebou zachovat správnost, která by byla ohrožena spoléháním se na data s potenciálně zastaralými verzemi.
Sdílené zámky použité pro ověření cizího klíče
U dvou tabulek v přímém vztahu cizího klíče musí databázový stroj podniknout kroky, aby zajistil, že nebudou porušena omezení spoléháním na potenciálně zastaralá čtení verzí. Aktuální implementace to dělá přepnutím na uzamykání potvrzeného čtení při přístupu k datům v rámci automatické kontroly cizího klíče.
Použití sdílených zámků zajišťuje, že kontrola integrity přečte nejnovější potvrzená data (nikoli starou verzi) nebo bloky kvůli souběžné modifikaci za letu. Přepnutí na uzamčení potvrzeného čtení se vztahuje pouze na konkrétní přístupovou metodu použitou ke kontrole dat cizího klíče; ostatní přístup k datům ve stejném příkazu nadále používá verze řádků.
Toto chování se týká pouze příkazů, které mění data, kde změna přímo ovlivňuje vztah cizího klíče. Pro úpravy odkazované (nadřazené) tabulky to znamená aktualizace, které ovlivňují odkazovanou hodnotu (pokud není nastavena na NULL
) a všechna smazání. Pro referenční (podřízenou) tabulku to znamená všechna vložení a aktualizace (opět, pokud není odkaz na klíč NULL
). Stejné úvahy platí pro efekty komponent MERGE
.
Příklad plánu provádění zobrazující vyhledávání cizího klíče, které používá sdílené zámky, je zobrazen níže:
Serializovatelné pro kaskádování cizích klíčů
Pokud má vztah cizího klíče kaskádovou akci, správnost vyžaduje místní eskalaci na serializovatelnou sémantiku izolace. To znamená, že uvidíte zámky rozsahu klíče provedené pro kaskádovou referenční akci. Stejně jako v případě aktualizačních zámků, které jsme viděli dříve, jsou tyto zámky rozsahu klíčů omezeny na výpis, nikoli na transakci. Příklad plánu provádění ukazující, kde se přebírají interní serializovatelné zámky pod RCSI, je zobrazen níže:
Další scénáře
Existuje mnoho dalších specifických případů, kdy engine automaticky prodlužuje životnost zámků nebo lokálně eskaluje úroveň izolace, aby byla zajištěna správnost. Patří mezi ně serializovatelná sémantika používaná při udržování souvisejícího indexovaného pohledu nebo při údržbě indexu, který má IGNORE_DUP_KEY
sada možností.
Zpráva je taková, že RCSI snižuje míru zamykání, ale nemůže je vždy zcela odstranit.
Příště
Další příspěvek v této sérii se zabývá úrovní izolace snímku.
[ Viz rejstřík pro celou sérii ]