Det här inlägget ger ny information om förutsättningarna för minimalloggad bulkbelastning när du använder INSERT...SELECT
i indexerade tabeller .
Den interna funktionen som möjliggör dessa fall kallas FastLoadContext
. Den kan aktiveras från SQL Server 2008 till och med 2014 med den dokumenterade spårningsflaggan 610. Från SQL Server 2016 och framåt, FastLoadContext
är aktiverat som standard; spårningsflaggan krävs inte.
Utan FastLoadContext
, de enda indexinlägg som kan minimalt loggas är de i en tom klustrade index utan sekundära index, som behandlas i del två av denna serie. Den minimala loggningen villkoren för oindexerade högtabeller behandlades i del ett.
För mer bakgrund, se Data Performance Loading Guide och Tiger Team anteckningar om beteendeförändringar för SQL Server 2016.
Snabbladdningskontext
Som en snabb påminnelse, RowsetBulk
anläggning (omfattas i del 1 och 2) möjliggör minimalt loggade bulklast för:
- Tom och icke-tom hög tabeller med:
- Bordlåsning; och
- Inga sekundära index.
- Tömma klustrade tabeller , med:
- Bordlåsning; och
- Inga sekundära index; och
DMLRequestSort=true
på Clustered Index Insert operatör.
FastLoadContext
kodsökväg lägger till stöd för minimalloggad och samtidigt bulklast på:
- Tom och icke-tom klustrade b-tree index.
- Tom och icke-tom icke-klustrade b-tree-index upprätthålls av en dedikerad Indexinfoga planoperatör.
FastLoadContext
kräver också DMLRequestSort=true
på motsvarande planoperatör i samtliga fall.
Du kanske har märkt en överlappning mellan RowsetBulk
och FastLoadContext
för tomma klustrade tabeller utan sekundära index. En TABLOCK
tips är inte obligatoriskt med FastLoadContext
, men det är inte obligatoriskt att vara frånvarande antingen. Som en konsekvens, en lämplig insats med TABLOCK
kan fortfarande kvalificera sig för minimal loggning via FastLoadContext
om det misslyckas med den detaljerade RowsetBulk
tester.
FastLoadContext
kan inaktiveras på SQL Server 2016 med den dokumenterade spårningsflaggan 692. Debug-kanalen Extended Event fastloadcontext_enabled
kan användas för att övervaka FastLoadContext
användning per indexpartition (raduppsättning). Den här händelsen aktiveras inte för RowsetBulk
laddar.
Blandad loggning
En enda INSERT...SELECT
uttalande med FastLoadContext
kan logga fullständigt några rader medan du minimalt loggar andra.
Rader infogas en i taget genom Indexinfogning operatör och fullständigt inloggad i följande fall:
- Alla rader har lagts till i den första indexsida, om indexet var tomt i början av operationen.
- Rader har lagts till i befintliga indexsidor.
- Rader flyttade mellan sidor med en siddelning.
Annars läggs rader från den beställda infogningsströmmen till på en helt ny sida med en optimerad och minimalloggad kodsökväg. När så många rader som möjligt har skrivits till den nya sidan länkas den direkt till den befintliga målindexstrukturen.
Den nyligen tillagda sidan kommer inte nödvändigtvis vara full (även om det uppenbarligen är det idealiska fallet) eftersom SQL Server måste vara noga med att inte lägga till rader på den nya sidan som logiskt hör hemma på en befintlig indexsida. Den nya sidan kommer att "häftas in" i indexet som en enhet, så vi kan inte ha några rader på den nya sidan som hör hemma någon annanstans. Detta är främst ett problem när du lägger till rader inom det befintliga nyckelintervallet för index, snarare än före början eller efter slutet av det befintliga indexnyckelintervallet.
Det är fortfarande möjligt för att lägga till nya sidor inom det befintliga indexnyckelintervallet, men de nya raderna måste sorteras högre än den högsta nyckeln på föregående befintlig indexsida och sortera lägre än den lägsta tangenten på följande befintlig indexsida. För bästa chans att uppnå minimal loggning under dessa omständigheter, se till att de infogade raderna inte överlappar befintliga rader så långt det är möjligt.
DMLRequestSort-villkor
Kom ihåg att FastLoadContext
kan endast aktiveras om DMLRequestSort
är inställd på true för motsvarande Indexinfoga operatör i utförandeplanen.
Det finns två huvudkodsökvägar som kan ställa in DMLRequestSort
till sant för indexinlägg. Endera vägen returnerar true är tillräckligt.
1. FOptimizeInsert
sqllang!CUpdUtil::FOptimizeInsert
kod kräver:
- Fler än 250 rader uppskattad ska införas; och
- Fler än 2 sidor uppskattad infoga datastorlek; och
- Målindexet måste ha färre än tre bladsidor .
Dessa villkor är desamma som RowsetBulk
på ett tomt klustrat index, med ett ytterligare krav på högst två sidor på indexbladsnivå. Observera noga att detta hänvisar till storleken på det befintliga indexet före infogningen, inte den uppskattade storleken på data som ska läggas till.
Skriptet nedan är en modifiering av demon som användes i tidigare delar i den här serien. Den visar minimal loggning när färre än tre indexsidor är ifyllda före testet INSERT...SELECT
springer. Testtabellsschemat är sådant att 130 rader får plats på en enda 8KB-sida när radversionshantering är avstängd för databasen. Multiplikatorn i den första TOPP
klausul kan ändras för att bestämma antalet befintliga indexsidor före testet INSERT...SELECT
exekveras:
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
När det klustrade indexet är förladdat med 3 sidor , är testinlägget fullständigt loggat (transaktionsloggdetaljposter utelämnas för korthetens skull):
När tabellen är förladdad med bara 1 eller 2 sidor , är testinlägget minimalt loggat :
När tabellen inte är förladdad med alla sidor motsvarar testet att köra den tomma klustrade tabelldemon från del två, men utan TABLOCK
tips:
De första 130 raderna är fullständigt loggade . Detta beror på att indexet var tomt innan vi började, och 130 rader fick plats på första sidan. Kom ihåg att den första sidan alltid loggas helt när FastLoadContext
används och indexet var tomt i förväg. De återstående 139 raderna infogas med minimal loggning .
Om en TABLOCK
ledtråd läggs till i infogningen, alla sidor loggas minimalt (inklusive den första) eftersom den tomma klustrade indexbelastningen nu kvalificerar sig för RowsetBulk
mekanism (till bekostnad av att ta en Sch-M
lås).
2. FDemandRowsSortedForPerformance
Om FOptimizeInsert
tester misslyckas, DMLRequestSort
kan fortfarande vara inställd på true genom en andra uppsättning tester i sqllang!CUpdUtil::FDemandRowsSortedForPerformance
koda. Dessa villkor är lite mer komplexa, så det kommer att vara användbart att definiera några parametrar:
P
– antal befintliga sidor på bladnivå i målindexet .I
– uppskattad antal rader att infoga.R
=P
/I
(målsidor per infogat rad).T
– antal målpartitioner (1 för opartionerad).
Logiken för att bestämma värdet på DMLRequestSort
är då:
- Om
P <=16
returnera falskt , annars :- Om
R <8
:- Om
P> 524
returnera true , annars falskt .
- Om
- Om
R>=8
:- Om
T> 1
ochI> 250
returnera true , annars falskt .
- Om
- Om
Ovanstående tester utvärderas av frågeprocessorn under plankompileringen. Det finns ett slutvillkor utvärderas av lagringsmotorkod (IndexDataSetSession::WakeUpInternal
) vid körningstid:
DMLRequestSort
är för närvarande sant; ochI>=100
.
Vi kommer att bryta ner all denna logik i hanterbara bitar härnäst.
Fler än 16 befintliga målsidor
Det första testet P <=16
betyder att index med färre än 17 befintliga bladsidor inte kvalificerar sig för FastLoadContext
via denna kodsökväg. För att vara helt tydlig på denna punkt, P
är antalet sidor på bladnivå i målindexet före INSERT...SELECT
exekveras.
För att demonstrera denna del av logiken kommer vi att förladda den testklustrade tabellen med 16 sidor av data. Detta har två viktiga effekter (kom ihåg att båda kodsökvägarna måste returnera false att sluta med en falsk värde för DMLRequestSort
):
- Det säkerställer att föregående
FOptimizeInsert
testet misslyckas , eftersom det tredje villkoret inte är uppfyllt (P <3
). -
P <=16 villkor i FDemandRowsSortedForPerformance
kommer också inte uppfyllas.
Vi förväntar oss därför FastLoadContext
inte aktiveras. Det modifierade demoskriptet är:
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)';
Alla 269 rader är fullständigt loggade som förutspått:
Observera att oavsett hur högt vi ställer in antalet nya rader att infoga, kommer skriptet ovan aldrig att producera minimal loggning på grund av P <=16
test (och P <3
testa i FOptimizeInsert
).
Om du väljer att köra demon själv med ett större antal rader, kommentera avsnittet som visar enskilda transaktionsloggposter, annars kommer du att vänta väldigt länge och SSMS kan krascha. (För att vara rättvis kan det göra det ändå, men varför öka risken.)
Sidförhållande per infogat rad
Om det finns 17 eller fler bladsidor i det befintliga indexet, föregående P <=16
testet kommer inte att misslyckas. Nästa avsnitt av logiken handlar om förhållandet mellan befintliga sidor till nyligen infogade rader . Detta måste också godkännas för att uppnå minimal loggning . Som en påminnelse är de relevanta villkoren:
- Förhållande
R
=P
/I
. - Om
R <8
:- Om
P> 524
returnera true , annars falskt .
- Om
Vi måste också komma ihåg det slutliga lagringsmotortestet för minst 100 rader:
I>=100
.
Omorganisera de här förhållandena lite, alla av följande måste vara sant:
P> 524
(befintliga indexsidor)I>=100
(uppskattade infogade rader)P / I <8
(förhållandeR
)
Det finns flera sätt att uppfylla dessa tre villkor samtidigt. Låt oss välja de minsta möjliga värdena för P
(525) och I
(100) ger en R
värde på (525 / 100) =5,25. Detta uppfyller (R <8
test), så vi förväntar oss att denna kombination kommer att resultera i minimal loggning :
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)';
Den 100-radiga INSERT...SELECT
är verkligen minimalloggad :
Minskar den uppskattade infogade rader till 99 (bryter I>=100
), och/eller minska antalet befintliga indexsidor till 524 (bryta P> 524
) resulterar i full loggning . Vi kan också göra ändringar så att R
är inte längre mindre än 8 för att producera full loggning . Till exempel, ställa in P =1000
och I =125
ger R =8
, med följande resultat:
De 125 infogade raderna loggades helt som förväntat. (Detta beror inte på fullständig loggning på första sidan, eftersom indexet inte var tomt i förväg.)
Sidförhållande för partitionerade index
Om alla föregående test misslyckas kräver det återstående testet R>=8
och kan bara vara nöjd när antalet partitioner (T
) är större än 1 och det finns mer än 250 uppskattade infogade rader (I
). Minns:
- Om
R>=8
:- Om
T> 1
ochI> 250
returnera true , annars falskt .
- Om
En subtilitet:För partitionerad index, regeln som säger att alla förstasidesrader är helt loggade (för ett initialt tomt index) gäller per partition . För ett objekt med 15 000 partitioner betyder det 15 000 helt loggade "första" sidor.
Sammanfattning och slutliga tankar
Formlerna och utvärderingsordningen som beskrivs i kroppen är baserad på kodinspektion med en debugger. De presenterades i en form som nära representerar timingen och ordningen som används i den verkliga koden.
Det är möjligt att ordna om och förenkla de villkoren lite, för att skapa en mer kortfattad sammanfattning av de praktiska kraven för minimal loggning när du infogar i ett b-träd med INSERT...SELECT
. De förfinade uttrycken nedan använder följande tre parametrar:
P
=antal befintliga indexera sidor på bladnivå.I
=uppskattad antal rader att infoga.S
=uppskattad infoga datastorlek i 8KB-sidor.
Radset bulkbelastning
- Använder
sqlmin!RowsetBulk
. - Kräver en tom klustrade indexmål med
TABLOCK
(eller motsvarande). - Kräver
DMLRequestSort =true
på Clustered Index Insert operatör. DMLRequestSort
är satt tilltrue
omI> 250
ochS> 2
.- Alla infogade rader loggas minimalt .
- En
Sch-M
lås förhindrar samtidig tabellåtkomst.
Snabbladdningskontext
- Använder
sqlmin!FastLoadContext
. - Aktiverar minimalloggad infogar i b-trädindex:
- Klustrade eller icke-klustrade.
- Med eller utan bordslås.
- Målindex är tomt eller inte.
- Kräver
DMLRequestSort =true
på den associerade Indexinfogning planoperatör. - Endast rader skrivna till helt nya sidor är bulkladdade och minimalloggade .
- Den första sidan av ett tidigare tomt index partitionen är alltid fullständigt inloggad .
- Absolut minimum av
I>=100
. - Kräver spårningsflagga 610 före SQL Server 2016.
- Tillgänglig som standard från SQL Server 2016 (spårningsflagga 692 inaktiveras).
DMLRequestSort
är satt till true
för:
- Alla index (partitionerad eller inte) om:
I> 250
ochP <3
ochS> 2
; ellerI>=100
ochP> 524
ochP
Endast för partitionerade index (med> 1 partition), DMLRequestSort
är också satt till true
om:
I> 250
ochP> 16
ochP>=I * 8
Det finns några intressanta fall som härrör från dessa FastLoadContext
villkor:
- Alla infogar i en icke-partitionerad index med mellan 3 och 524 (inklusive) befintliga bladsidor kommer att loggas fullständigt oavsett antal och totalstorlek på raderna som lagts till. Detta kommer mest märkbart att påverka stora inlägg till små (men inte tomma) tabeller.
- Alla infogas i en partitionerad index med mellan 3 och 16 befintliga sidor kommer att loggas fullständigt .
- Stora skär till stora icke-partitionerade index kanske inte minimalloggas på grund av olikheten
P . När
P
är stor, en motsvarande stor uppskattad antal infogade rader (I
) krävs. Till exempel kan ett index med 8 miljoner sidor inte stödja minimal loggning när du infogar 1 miljon rader eller färre.
Icke-klustrade index
Samma överväganden och beräkningar som tillämpas på klustrade index i demos gäller för icke-klustrade b-tree-index också, så länge som indexet underhålls av en dedikerad planoperatör (en bred , eller per-index planen). Icke-klustrade index som underhålls av en bastabelloperator (t.ex. Clustered Index Insert ) är inte kvalificerade för FastLoadContext
.
Observera att formelparametrarna måste utvärderas på nytt för varje icke-klustrad indexoperator — beräknad radstorlek, antal befintliga indexsidor och uppskattning av kardinalitet.
Allmänna kommentarer
Se upp för låga uppskattningar av kardinalitet vid Indexinfogning operatör, eftersom dessa kommer att påverka I
och S
parametrar. Om ett tröskelvärde inte nås på grund av ett kardinalitetsuppskattningsfel, kommer infogningen att loggas fullständigt .
Kom ihåg att DMLRequestSort
är cachelagrat med planen — Den utvärderas inte vid varje genomförande av en återanvänd plan. Detta kan introducera en form av det välkända Parameter Sensitivity Problem (även känt som "parameter sniffing").
Värdet på P
(indexbladssidor) är inte uppdaterad i början av varje uttalande. Den aktuella implementeringen cachar värdet för hela batchen . Detta kan ha oväntade biverkningar. Till exempel en TRUNCATE TABLE
i samma batch som en INSERT...SELECT
kommer inte att återställa P
till noll för beräkningarna som beskrivs i den här artikeln — de kommer att fortsätta att använda pre-truncate-värdet, och en omkompilering hjälper inte. En lösning är att skicka in stora ändringar i separata partier.
Spåra flaggor
Det är möjligt att tvinga fram FDemandRowsSortedForPerformance
för att returnera true genom att ställa in odokumenterad och ostödd trace flag 2332, som jag skrev i Optimizing T-SQL-frågor som ändrar data. När TF 2332 är aktiv, antalet uppskattade rader att infoga måste fortfarande vara minst 100 . TF 2332 påverkar minimal loggning beslut för FastLoadContext
endast (det är effektivt för partitionerade heaps så långt som DMLRequestSort
är bekymrad, men har ingen effekt på själva heapen, eftersom FastLoadContext
gäller endast index).
En bred/per-index planform för underhåll av icke-klustrade index kan tvingas fram för radlagringstabeller med spårningsflagga 8790 (ej officiellt dokumenterad, men nämnt i en Knowledge Base-artikel såväl som i min artikel som länkad till TF2332 precis ovan).
Relaterad läsning
Allt av Sunil Agarwal från SQL Server-teamet:
- Vad är massimportoptimeringarna?
- Massimportoptimeringar (minimal loggning)
- Minsta loggningsändringar i SQL Server 2008
- Minsta loggningsändringar i SQL Server 2008 (del-2)
- Minsta loggningsändringar i SQL Server 2008 (del-3)