Tento příspěvek poskytuje nové informace o předpokladech pro minimálně protokolované hromadné zatížení při použití INSERT...SELECT
do indexovaných tabulek .
Interní zařízení, které umožňuje tyto případy, se nazývá FastLoadContext
. Lze jej aktivovat od SQL Server 2008 až 2014 včetně pomocí zdokumentovaného příznaku trasování 610. Od SQL Server 2016 dále, FastLoadContext
je ve výchozím nastavení povoleno; příznak trasování není vyžadován.
Bez FastLoadContext
, jediné indexové vložky, které lze minimálně protokolovat jsou do prázdného seskupený index bez sekundárních indexů, jak je popsáno ve druhé části této řady. Minimální protokolování podmínky pro neindexované tabulky haldy byly popsány v první části.
Další informace naleznete v příručce Data Performance Loading Guide a Tiger Team poznámky ke změnám chování pro SQL Server 2016.
Kontext rychlého načítání
Pro rychlé připomenutí, RowsetBulk
zařízení (popsané v částech 1 a 2) umožňuje minimálně protokolované hromadné zatížení pro:
- Prázdná a neprázdná hromada tabulky s:
- Zamykání stolu; a
- Žádné sekundární indexy.
- Prázdné seskupené tabulky , s:
- Zamykání stolu; a
- Žádné sekundární indexy; a
DMLRequestSort=true
na Vložení seskupeného rejstříku operátor.
FastLoadContext
kódová cesta přidává podporu pro minimálně protokolované a souběžně hromadné zatížení na:
- Prázdné a neprázdné seskupené indexy b-stromu.
- Prázdné a neprázdné neshlukované b-tree indexy spravované vyhrazeným Vložení indexu operátor plánu.
FastLoadContext
také vyžaduje DMLRequestSort=true
na příslušného operátora plánu ve všech případech.
Možná jste si všimli překrývání mezi RowsetBulk
a FastLoadContext
pro prázdné seskupené tabulky bez sekundárních indexů. TABLOCK
nápověda není vyžadována pomocí FastLoadContext
, ale není vyžadováno, aby chyběl buď. V důsledku toho vhodná vložka s TABLOCK
může stále splňovat podmínky pro minimální protokolování prostřednictvím FastLoadContext
pokud selže podrobný RowsetBulk
testy.
FastLoadContext
lze zakázat na SQL Server 2016 pomocí zdokumentovaného příznaku trasování 692. Rozšířená událost kanálu ladění fastloadcontext_enabled
lze použít ke sledování FastLoadContext
využití na oddíl indexu (sadu řádků). Tato událost se pro RowsetBulk
nespustí načte.
Smíšené protokolování
Jediný INSERT...SELECT
pomocí FastLoadContext
může se plně přihlásit některé řádky při minimálním protokolování ostatní.
Řádky se vkládají po jednom pomocí vložení indexu operátor a plně přihlášeni v následujících případech:
- Všechny řádky přidány do prvního stránka indexu, pokud index byl prázdný na začátku operace.
- Řádky byly přidány do existujících indexové stránky.
- Řádky přesunuty mezi stránkami o rozdělení stránky.
V opačném případě se na zcela novou stránku přidají řádky z objednaného streamu vložení pomocí optimalizovaného a minimálně protokolovaného cesta kódu. Jakmile je na novou stránku zapsáno co nejvíce řádků, je tato stránka přímo propojena se stávající strukturou cílového indexu.
Nově přidaná stránka nemusí být být plný (ačkoli to je samozřejmě ideální případ), protože SQL Server si musí dávat pozor, aby na novou stránku nepřidal řádky, které logicky patří na existující indexová stránka. Nová stránka bude „sešita“ do indexu jako jednotka, takže na nové stránce nemůžeme mít žádné řádky, které by patřily jinam. Jde především o problém při přidávání řádků v rámci stávající rozsah klíčů indexu, spíše než před začátkem nebo po konci stávajícího rozsahu klíčů indexu.
Stále je to možné přidat nové stránky v rámci stávající rozsah klíče indexu, ale nové řádky musí být seřazeny výše než nejvyšší klíč v předchozím existující stránku indexu a seřadit níže než nejnižší klávesa na následujících existující indexovou stránku. Pro nejlepší šanci na dosažení minimálního protokolování za těchto okolností zajistěte, aby se vložené řádky pokud možno nepřekrývaly s existujícími řádky.
Podmínky DLRequestSort
Pamatujte, že FastLoadContext
lze aktivovat pouze v případě, že DMLRequestSort
je nastaveno na true pro odpovídající Vložení indexu operátora v prováděcím plánu.
Existují dvě hlavní cesty kódu, které mohou nastavit DMLRequestSort
pravda pro indexové vložky. Jakákoli cesta vrací true je dostačující.
1. FOptimizeInsert
sqllang!CUpdUtil::FOptimizeInsert
kód vyžaduje:
- Více než 250 řádků odhadem vložit; a
- Více než 2 stránky odhadem vložit velikost dat; a
- cílový index musí mít méně než 3 listové stránky .
Tyto podmínky jsou stejné jako RowsetBulk
na prázdném seskupeném indexu s dodatečným požadavkem na maximálně dvě stránky na úrovni listu indexu. Pozorně si všimněte, že se jedná o velikost existujícího indexu před vložkou, ne odhadovaná velikost dat, která mají být přidána.
Níže uvedený skript je modifikací dema použitého v dřívějších dílech této série. Zobrazuje minimální protokolování když jsou před naplněny méně než tři indexové stránky test INSERT...SELECT
běží. Schéma testovací tabulky je takové, že se na jednu stránku o velikosti 8 kB vejde 130 řádků, když je pro databázi vypnuté verzování řádků. Násobitel v prvním TOP
klauzuli lze změnit a určit tak počet existujících stránek indexu před test INSERT...SELECT
se provede:
IF OBJECT_ID(N'dbo.Test', N'U') IS NOT NULL BEGIN DROP TABLE dbo.Test; END; GO CREATE TABLE dbo.Test ( id integer NOT NULL IDENTITY CONSTRAINT [PK dbo.Test (id)] PRIMARY KEY, c1 integer NOT NULL, padding char(45) NOT NULL DEFAULT '' ); GO -- 130 rows per page for this table -- structure with row versioning off INSERT dbo.Test (c1) SELECT TOP (3 * 130) -- Change the 3 here CHECKSUM(NEWID()) FROM master.dbo.spt_values AS SV; GO -- Show physical index statistics -- to confirm the number of pages SELECT DDIPS.index_type_desc, DDIPS.alloc_unit_type_desc, DDIPS.page_count, DDIPS.record_count, DDIPS.avg_record_size_in_bytes FROM sys.dm_db_index_physical_stats ( DB_ID(), OBJECT_ID(N'dbo.Test', N'U'), 1, -- Index ID NULL, -- Partition ID 'DETAILED' ) AS DDIPS WHERE DDIPS.index_level = 0; -- leaf level only GO -- Clear the plan cache DBCC FREEPROCCACHE; GO -- Clear the log CHECKPOINT; GO -- Main test INSERT dbo.Test (c1) SELECT TOP (269) CHECKSUM(NEWID()) FROM master.dbo.spt_values AS SV; GO -- Show log entries SELECT FD.Operation, FD.Context, FD.[Log Record Length], FD.[Log Reserve], FD.AllocUnitName, FD.[Transaction Name], FD.[Lock Information], FD.[Description] FROM sys.fn_dblog(NULL, NULL) AS FD; GO -- Count the number of fully-logged rows SELECT [Fully Logged Rows] = COUNT_BIG(*) FROM sys.fn_dblog(NULL, NULL) AS FD WHERE FD.Operation = N'LOP_INSERT_ROWS' AND FD.Context = N'LCX_CLUSTERED' AND FD.AllocUnitName = N'dbo.Test.PK dbo.Test (id)'; GO
Když je v seskupeném indexu předem načten 3 stránky , testovací vložka je plně přihlášena (podrobné záznamy protokolu transakcí jsou pro stručnost vynechány):
Když je v tabulce předem načtena pouze 1 nebo 2 stránky , testovací vložka je minimálně přihlášena :
Když tabulka není předem načtena se všemi stránkami je test ekvivalentní spuštění prázdné ukázky seskupené tabulky z druhé části, ale bez TABLOCK
nápověda:
Prvních 130 řádků je plně protokolováno . Důvodem je, že index byl prázdný, než jsme začali, a na první stránku se vešlo 130 řádků. Pamatujte, že při FastLoadContext
je vždy plně zaprotokolována první stránka a index byl předem prázdný. Zbývajících 139 řádků je vloženo s minimálním protokolováním .
Pokud TABLOCK
nápověda je přidána do přílohy, všechny stránky jsou minimálně protokolovány (včetně prvního), protože prázdné načtení klastrovaného indexu se nyní kvalifikuje pro RowsetBulk
mechanismus (za cenu převzetí Sch-M
zámek).
2. FDemandRowsSortedForPerformance
Pokud FOptimizeInsert
testy se nezdaří, DMLRequestSort
může být stále nastaveno na pravda pomocí druhé sady testů v sqllang!CUpdUtil::FDemandRowsSortedForPerformance
kód. Tyto podmínky jsou trochu složitější, takže bude užitečné definovat některé parametry:
P
– počet existujících stránek na úrovni listu v cílovém indexu .I
– odhadem počet řádků k vložení.R
=P
/I
(cílové stránky na vložený řádek).T
– počet cílových oddílů (1 pro nerozdělené).
Logika k určení hodnoty DMLRequestSort
je pak:
- Pokud
P <= 16
vrátit false , jinak :- Pokud
R < 8
:- Pokud
P > 524
vrátit pravda , jinak false .
- Pokud
- Pokud
R >= 8
:- Pokud
T > 1
aI > 250
vrátit pravda , jinak false .
- Pokud
- Pokud
Výše uvedené testy jsou vyhodnoceny procesorem dotazů během sestavování plánu. Existuje konečná podmínka hodnoceno kódem úložiště (IndexDataSetSession::WakeUpInternal
) v době provádění:
DMLRequestSort
je aktuálně pravda; aI >= 100
.
Příště celou tuto logiku rozdělíme na zvládnutelné kousky.
Více než 16 stávajících cílových stránek
První test P <= 16
znamená, že indexy s méně než 17 existujícími listovými stránkami nebudou způsobilé pro FastLoadContext
přes tuto kódovou cestu. Aby bylo v tomto bodě naprosto jasno, P
je počet stránek na úrovni listu v cílovém indexu před INSERT...SELECT
se provede.
Abychom tuto část logiky demonstrovali, předem načteme testovací seskupenou tabulku s 16 stránkami dat. To má dva důležité efekty (pamatujte, že obě cesty kódu musí vrátit false skončí s nepravdou hodnotu pro DMLRequestSort
):
- Zajistí, že předchozí
FOptimizeInsert
test selhal , protože není splněna třetí podmínka (P < 3
). P <= 16
stav vFDemandRowsSortedForPerformance
také nebude být splněn.
Proto očekáváme FastLoadContext
nebude povoleno. Upravený demo skript je:
IF OBJECT_ID(N'dbo.Test', N'U') IS NOT NULL BEGIN DROP TABLE dbo.Test; END; GO CREATE TABLE dbo.Test ( id integer NOT NULL IDENTITY CONSTRAINT [PK dbo.Test (id)] PRIMARY KEY, c1 integer NOT NULL, padding char(45) NOT NULL DEFAULT '' ); GO -- 130 rows per page for this table -- structure with row versioning off INSERT dbo.Test (c1) SELECT TOP (16 * 130) -- 16 pages CHECKSUM(NEWID()) FROM master.dbo.spt_values AS SV; GO -- Show physical index statistics -- to confirm the number of pages SELECT DDIPS.index_type_desc, DDIPS.alloc_unit_type_desc, DDIPS.page_count, DDIPS.record_count, DDIPS.avg_record_size_in_bytes FROM sys.dm_db_index_physical_stats ( DB_ID(), OBJECT_ID(N'dbo.Test', N'U'), 1, -- Index ID NULL, -- Partition ID 'DETAILED' ) AS DDIPS WHERE DDIPS.index_level = 0; -- leaf level only GO -- Clear the plan cache DBCC FREEPROCCACHE; GO -- Clear the log CHECKPOINT; GO -- Main test INSERT dbo.Test (c1) SELECT TOP (269) CHECKSUM(NEWID()) FROM master.dbo.spt_values AS SV1 CROSS JOIN master.dbo.spt_values AS SV2; GO -- Show log entries SELECT FD.Operation, FD.Context, FD.[Log Record Length], FD.[Log Reserve], FD.AllocUnitName, FD.[Transaction Name], FD.[Lock Information], FD.[Description] FROM sys.fn_dblog(NULL, NULL) AS FD; GO -- Count the number of fully-logged rows SELECT [Fully Logged Rows] = COUNT_BIG(*) FROM sys.fn_dblog(NULL, NULL) AS FD WHERE FD.Operation = N'LOP_INSERT_ROWS' AND FD.Context = N'LCX_CLUSTERED' AND FD.AllocUnitName = N'dbo.Test.PK dbo.Test (id)';
Všech 269 řádků je plně přihlášeno podle předpovědi:
Všimněte si, že bez ohledu na to, jak vysoký počet nových řádků k vložení nastavíme, výše uvedený skript nikdy nebude produkovat minimální protokolování kvůli P <= 16
test (a P < 3
otestujte v FOptimizeInsert
).
Pokud se rozhodnete spustit demo sami s větším počtem řádků, zakomentujte sekci, která zobrazuje jednotlivé záznamy protokolu transakcí, jinak budete čekat velmi dlouho a SSMS může selhat. (Abych byl spravedlivý, může to udělat i tak, ale proč přidávat na riziku.)
Poměr stránek na vložený řádek
Pokud je jich 17 nebo více listové stránky ve stávajícím indexu, předchozí P <= 16
test neprojde. Další část logiky se zabývá poměrem existujících stránek do nově vložených řádků . To musí také projít, aby bylo dosaženo minimálního protokolování . Pro připomenutí, relevantní podmínky jsou:
- Poměr
R
=P
/I
. - Pokud
R < 8
:- Pokud
P > 524
vrátit pravda , jinak false .
- Pokud
Musíme si také pamatovat závěrečný test skladovacího motoru pro alespoň 100 řádků:
I >= 100
.
Trochu přeorganizujte ty podmínky, vše z následujícího musí platit:
P > 524
(stávající indexové stránky)I >= 100
(odhadované vložené řádky)P / I < 8
(poměrR
)
Existuje několik způsobů, jak splnit tyto tři podmínky současně. Zvolme minimální možné hodnoty pro P
(525) a I
(100) dává R
hodnota (525 / 100) =5,25. To splňuje (R < 8
test), takže očekáváme, že tato kombinace povede k minimálnímu protokolování :
IF OBJECT_ID(N'dbo.Test', N'U') IS NOT NULL BEGIN DROP TABLE dbo.Test; END; GO CREATE TABLE dbo.Test ( id integer NOT NULL IDENTITY CONSTRAINT [PK dbo.Test (id)] PRIMARY KEY, c1 integer NOT NULL, padding char(45) NOT NULL DEFAULT '' ); GO -- 130 rows per page for this table -- structure with row versioning off INSERT dbo.Test (c1) SELECT TOP (525 * 130) -- 525 pages CHECKSUM(NEWID()) FROM master.dbo.spt_values AS SV1 CROSS JOIN master.dbo.spt_values AS SV2; GO -- Show physical index statistics -- to confirm the number of pages SELECT DDIPS.index_type_desc, DDIPS.alloc_unit_type_desc, DDIPS.page_count, DDIPS.record_count, DDIPS.avg_record_size_in_bytes FROM sys.dm_db_index_physical_stats ( DB_ID(), OBJECT_ID(N'dbo.Test', N'U'), 1, -- Index ID NULL, -- Partition ID 'DETAILED' ) AS DDIPS WHERE DDIPS.index_level = 0; -- leaf level only GO -- Clear the plan cache DBCC FREEPROCCACHE; GO -- Clear the log CHECKPOINT; GO -- Main test INSERT dbo.Test (c1) SELECT TOP (100) CHECKSUM(NEWID()) FROM master.dbo.spt_values AS SV1 CROSS JOIN master.dbo.spt_values AS SV2; GO -- Show log entries SELECT FD.Operation, FD.Context, FD.[Log Record Length], FD.[Log Reserve], FD.AllocUnitName, FD.[Transaction Name], FD.[Lock Information], FD.[Description] FROM sys.fn_dblog(NULL, NULL) AS FD; GO -- Count the number of fully-logged rows SELECT [Fully Logged Rows] = COUNT_BIG(*) FROM sys.fn_dblog(NULL, NULL) AS FD WHERE FD.Operation = N'LOP_INSERT_ROWS' AND FD.Context = N'LCX_CLUSTERED' AND FD.AllocUnitName = N'dbo.Test.PK dbo.Test (id)';
100řádkový INSERT...SELECT
je skutečně minimálně přihlášen :
Snížení odhadu vložené řádky na 99 (přerušení I >= 100
) a/nebo snížením počtu stávajících stránek indexu na 524 (porušení P > 524
) vede k úplnému protokolování . Můžeme také provést změny, jako je R
již není menší než 8, aby bylo dosaženo úplného protokolování . Například nastavení P = 1000
a I = 125
dává R = 8
, s následujícími výsledky:
125 vložených řádků bylo plně protokolováno podle očekávání. (Není to způsobeno úplným protokolováním na první stránce, protože index nebyl předem prázdný.)
Poměr stránek pro dělené indexy
Pokud všechny předchozí testy selžou, jeden zbývající test vyžaduje R >= 8
a může pouze být spokojen, když počet oddílů (T
) je větší než 1 a existuje více než 250 odhadovaných vložené řádky (I
). Připomeňme:
- Pokud
R >= 8
:- Pokud
T > 1
aI > 250
vrátit pravda , jinak false .
- Pokud
Jedna jemnost:Pro rozdělené indexy, pravidlo, které říká, že všechny řádky na první stránce jsou plně protokolovány (pro původně prázdný index), platí na oddíl . Pro objekt s 15 000 oddíly to znamená 15 000 plně přihlášených „prvních“ stránek.
Souhrn a závěrečné myšlenky
Vzorce a pořadí vyhodnocení popsané v těle jsou založeny na kontrole kódu pomocí debuggeru. Byly prezentovány ve formě, která přesně reprezentuje načasování a pořadí použité ve skutečném kódu.
Tyto podmínky je možné přeuspořádat a trochu zjednodušit a vytvořit tak stručnější souhrn praktických požadavků na minimální protokolování při vkládání do b-stromu pomocí INSERT...SELECT
. Upřesněné výrazy níže používají následující tři parametry:
P
=počet existujících indexovat stránky na úrovni listu.I
=odhad počet řádků k vložení.S
=odhad vložte velikost dat ve stránkách o velikosti 8 kB.
Hromadné načtení sady řádků
- Používá
sqlmin!RowsetBulk
. - Vyžaduje prázdné seskupený cíl indexu s
TABLOCK
(nebo ekvivalentní). - Vyžaduje
DMLRequestSort = true
na Vložení seskupeného rejstříku operátor. DMLRequestSort
je nastavena natrue
pokudI > 250
aS > 2
.- Všechny vložené řádky jsou minimálně protokolovány .
Sch-M
zámek zabraňuje souběžnému přístupu k tabulce.
Kontext rychlého načítání
- Používá
sqlmin!FastLoadContext
. - Povoluje minimálně protokolované vloží do indexů b-stromu:
- Shlukovaný nebo neshlukovaný.
- Se zámkem stolu nebo bez něj.
- Cílový index je prázdný nebo není.
- Vyžaduje
DMLRequestSort = true
na přidružené Vložení indexu operátor plánu. - Pouze řádky zapsané na zcela nové stránky jsou hromadně načteny a minimálně protokolovány .
- První stránka dříve prázdného indexu oddíl je vždy plně přihlášen .
- Absolutní minimum
I >= 100
. - Vyžaduje příznak trasování 610 před SQL Server 2016.
- Ve výchozím nastavení k dispozici od SQL Server 2016 (příznak trasování 692 deaktivuje).
DMLRequestSort
je nastavena na true
pro:
- Jakýkoli index (rozdělené nebo ne) if:
I > 250
aP < 3
aS > 2
; neboI >= 100
aP > 524
aP < I * 8
Pouze pro dělené indexy (s> 1 oddílem), DMLRequestSort
je také nastaveno na true
pokud:
I > 250
aP > 16
aP >= I * 8
Existuje několik zajímavých případů vyplývajících z těchto FastLoadContext
podmínky:
- Vše vloží do nerozděleného index s mezi 3 a 524 (včetně) stávající listové stránky budou plně přihlášeny bez ohledu na počet a celkovou velikost přidaných řádků. Nejvýrazněji to ovlivní velké přílohy do malých (ale ne prázdných) tabulek.
- Vše vloží do rozděleného index s 3 až 16 existující stránky budou plně přihlášeny .
- Velké vložky na velké nerozdělené indexy nemusí být minimálně protokolovány kvůli nerovnosti
P < I * 8
. KdyžP
je velký, odpovídající odhadem počet vložených řádků (I
) je požadováno. Například index s 8 miliony stránek nemůže podporovat minimální protokolování při vkládání 1 milionu řádků nebo méně.
Neclusterované indexy
Stejné úvahy a výpočty použité pro seskupené indexy v ukázkách platí pro nonclustered indexy b-tree také, pokud je index udržován operátorem vyhrazeného plánu (široký nebo na index plán). Neshlukované indexy spravované operátorem základní tabulky (např. Vložení seskupeného indexu ) nejsou způsobilé pro FastLoadContext
.
Pamatujte, že parametry vzorce je třeba znovu vyhodnotit pro každý neshlukovaný operátor indexu — vypočítaná velikost řádku, počet existujících stránek indexu a odhad mohutnosti.
Obecné poznámky
Dejte si pozor na nízké odhady mohutnosti na Vložení indexu operátor, protože ty ovlivní I
a S
parametry. Pokud není prahové hodnoty dosaženo kvůli chybě odhadu mohutnosti, vložka bude úplně zaprotokolována .
Pamatujte, že DMLRequestSort
je uloženo do mezipaměti s plánem — nevyhodnocuje se při každém provedení znovu použitého plánu. To může zavést formu dobře známého problému citlivosti parametrů (také známého jako „sniffování parametrů“).
Hodnota P
(listové stránky indexu) není obnoveno na začátku každého prohlášení. Aktuální implementace ukládá do mezipaměti hodnotu pro celou dávku . To může mít neočekávané vedlejší účinky. Například TRUNCATE TABLE
ve stejné dávce jako INSERT...SELECT
neresetuje P
na nulu pro výpočty popsané v tomto článku — budou nadále používat hodnotu před oříznutím a rekompilace nepomůže. Řešením je odeslat velké změny v samostatných dávkách.
Příznaky sledování
Je možné vynutit FDemandRowsSortedForPerformance
vrátit true nastavením nedokumentované a nepodporované trasovací příznak 2332, jak jsem psal v Optimalizace T-SQL dotazů, které mění data. Když je TF 2332 aktivní, počet odhadovaných řádků k vložení stále musí být nejméně 100 . TF 2332 ovlivňuje minimální protokolování rozhodnutí pro FastLoadContext
pouze (je účinné pro rozdělené hromady až po DMLRequestSort
se týká, ale nemá žádný vliv na samotnou haldu, protože FastLoadContext
platí pouze pro indexy).
široký/na index tvar plánu pro údržbu neshlukovaného indexu lze vynutit pro tabulky rowstore pomocí příznaku trasování 8790 (není oficiálně zdokumentováno, ale zmíněno v článku znalostní báze Knowledge Base a také v mém článku, který je propojen s TF2332 výše).
Související čtení
Vše od Sunila Agarwala z týmu SQL Server:
- Co jsou optimalizace hromadného importu?
- Optimalizace hromadného importu (minimální protokolování)
- Minimální změny protokolování v SQL Server 2008
- Minimální změny protokolování v SQL Server 2008 (část 2)
- Minimální změny protokolování v SQL Server 2008 (část 3)