Dette indlæg giver nye oplysninger om forudsætningerne for minimalt logget massebelastning når du bruger INSERT...SELECT
i indekserede tabeller .
Den interne facilitet, der muliggør disse tilfælde, kaldes FastLoadContext
. Den kan aktiveres fra SQL Server 2008 til og med 2014 ved hjælp af dokumenteret sporingsflag 610. Fra SQL Server 2016 og frem, FastLoadContext
er aktiveret som standard; sporingsflaget er ikke påkrævet.
Uden FastLoadContext
, de eneste indeksindsættelser, der kan minimalt logges er dem i en tom klynget indeks uden sekundære indekser, som dækket i del to af denne serie. Den minimale logning betingelser for uindekserede heap-tabeller blev dækket i første del.
For mere baggrund, se Data Performance Loading Guide og Tiger Team noter om adfærdsændringerne for SQL Server 2016.
Hurtig indlæsningskontekst
Som en hurtig påmindelse, RowsetBulk
facilitet (dækket i del 1 og 2) muliggør minimalt logget massebelastning for:
- Tom og ikke-tom bunke tabeller med:
- Bordlåsning; og
- Ingen sekundære indekser.
- Tømme klyngede tabeller , med:
- Bordlåsning; og
- Ingen sekundære indekser; og
DMLRequestSort=true
på Clustered Index Insert operatør.
FastLoadContext
kodesti tilføjer understøttelse af minimalt logget og samtidig massebelastning på:
- Tom og ikke-tom grupperet b-tree indekser.
- Tom og ikke-tom ikke-klyngede b-tree indekser vedligeholdt af en dedikeret Indeksindsæt planoperatør.
FastLoadContext
kræver også DMLRequestSort=true
på den tilsvarende planoperatør i alle tilfælde.
Du har muligvis bemærket et overlap mellem RowsetBulk
og FastLoadContext
for tomme klyngede tabeller uden sekundære indekser. En TABLOCK
tip er ikke påkrævet med FastLoadContext
, men det er ikke påkrævet at være fraværende enten. Som en konsekvens heraf en passende indsats med TABLOCK
kan stadig kvalificere sig til minimal logning via FastLoadContext
hvis det mislykkes den detaljerede RowsetBulk
tests.
FastLoadContext
kan deaktiveres på SQL Server 2016 ved hjælp af dokumenteret sporingsflag 692. Debug-kanalen Extended Event fastloadcontext_enabled
kan bruges til at overvåge FastLoadContext
forbrug pr. indekspartition (rækkesæt). Denne hændelse udløses ikke for RowsetBulk
læsser.
Blandet logning
En enkelt INSERT...SELECT
sætning ved hjælp af FastLoadContext
kan logge fuldstændigt nogle rækker, mens du minimalt logger andre.
Rækker indsættes en ad gangen ved Index Insert operatør og fuldt logget i følgende tilfælde:
- Alle rækker tilføjet til den første indekssiden, hvis indekset var tomt ved starten af operationen.
- Rækker tilføjet til eksisterende indekssider.
- Rækker flyttet mellem sider ved en sideopdeling.
Ellers føjes rækker fra den bestilte indsættelsesstrøm til en helt ny side ved hjælp af en optimeret og minimalt logget kodesti. Når så mange rækker som muligt er skrevet til den nye side, linkes den direkte til den eksisterende målindeksstruktur.
Den nyligt tilføjede side vil ikke nødvendigvis være fuld (selvom det naturligvis er det ideelle tilfælde), fordi SQL Server skal passe på ikke at tilføje rækker til den nye side, der logisk hører hjemme på en eksisterende indeksside. Den nye side bliver 'syet ind i' indekset som en enhed, så vi kan ikke have nogen rækker på den nye side, der hører hjemme andre steder. Dette er primært et problem, når du tilføjer rækker indenfor det eksisterende nøgleinterval for indekset i stedet for før starten eller efter slutningen af det eksisterende indeksnøgleområde.
Det er stadig muligt for at tilføje nye sider indenfor det eksisterende indeksnøgleområde, men de nye rækker skal sorteres højere end den højeste nøgle på foregående eksisterende indeksside og sorter lavere end den laveste tast på følgende eksisterende indeksside. For den bedste chance for at opnå minimal logning under disse omstændigheder skal du sikre dig, at de indsatte rækker så vidt muligt ikke overlapper eksisterende rækker.
DMLRequestSort-betingelser
Husk at FastLoadContext
kan kun aktiveres hvis DMLRequestSort
er indstillet til true for den tilsvarende Index Insert operatør i udførelsesplanen.
Der er to hovedkodestier, der kan indstille DMLRequestSort
til sand til stikordsindlæg. Enhver vej returnerer sand er tilstrækkeligt.
1. FOptimizeInsert
sqllang!CUpdUtil::FOptimizeInsert
kode kræver:
- Mere end 250 rækker estimeret skal indsættes; og
- Mere end 2 sider estimeret indsæt datastørrelse; og
- Målindekset skal have færre end 3 bladsider .
Disse betingelser er de samme som RowsetBulk
på et tomt klynget indeks med et yderligere krav om højst to indeksbladsniveausider. Bemærk nøje, at dette refererer til størrelsen af det eksisterende indeks før indsættelsen, ikke den anslåede størrelse af de data, der skal tilføjes.
Scriptet nedenfor er en modifikation af demoen, der blev brugt i tidligere dele i denne serie. Den viser minimal logning når færre end tre indekssider er udfyldt før testen INSERT...SELECT
løber. Testtabelskemaet er sådan, at 130 rækker kan passe på en enkelt 8KB side, når rækkeversionering er slået fra for databasen. Multiplikatoren i den første TOP
klausul kan ændres for at bestemme antallet af eksisterende indekssider før testen INSERT...SELECT
udføres:
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 klyngede indeks er forudindlæst med 3 sider , er testindlægget fuldt logget (transaktionslogdetaljer er udeladt for kortheds skyld):
Når tabellen er forudindlæst med kun 1 eller 2 sider , er testindlægget minimalt logget :
Når tabellen er ikke forudindlæst med alle sider svarer testen til at køre den tomme klyngede tabeldemo fra del to, men uden TABLOCK
tip:
De første 130 rækker er fuldt logget . Dette skyldes, at indekset var tomt før vi startede, og 130 rækker fik plads på den første side. Husk, at den første side altid er fuldt logget, når FastLoadContext
er brugt, og indekset var tomt på forhånd. De resterende 139 rækker indsættes med minimal logning .
Hvis en TABLOCK
tip føjes til indsættelsen, alle sider er minimalt logget (inklusive den første), da den tomme klyngede indeksbelastning nu kvalificerer til RowsetBulk
mekanisme (på bekostning af at tage en Sch-M
lås).
2. FDemandRowsSortedForPerformance
Hvis FOptimizeInsert
test mislykkedes, DMLRequestSort
kan stadig være indstillet til true ved et andet sæt test i sqllang!CUpdUtil::FDemandRowsSortedForPerformance
kode. Disse forhold er lidt mere komplekse, så det vil være nyttigt at definere nogle parametre:
P
– antal eksisterende sider på bladniveau i målindekset .I
– estimeret antal rækker, der skal indsættes.R
=P
/I
(målsider pr. indsat række).T
– antal målpartitioner (1 for upartitioneret).
Logikken til at bestemme værdien af DMLRequestSort
er så:
- Hvis
P <=16
returner falsk , ellers :- Hvis
R <8
:- Hvis
P> 524
returner sand , ellers falsk .
- Hvis
- Hvis
R>=8
:- Hvis
T> 1
ogI> 250
returner sand , ellers falsk .
- Hvis
- Hvis
Ovenstående tests evalueres af forespørgselsprocessoren under plankompileringen. Der er en endelig betingelse evalueret af lagringsmotorkode (IndexDataSetSession::WakeUpInternal
) på udførelsestidspunktet:
DMLRequestSort
er i øjeblikket sand; ogI>=100
.
Vi vil herefter bryde al denne logik ned i håndterbare stykker.
Mere end 16 eksisterende målsider
Den første test P <=16
betyder, at indekser med færre end 17 eksisterende bladsider ikke vil kvalificere sig til FastLoadContext
via denne kodesti. For at være helt klar på dette punkt, P
er antallet af sider på bladniveau i målindekset før INSERT...SELECT
udføres.
For at demonstrere denne del af logikken vil vi forudindlæse testklyngetabellen med 16 sider af data. Dette har to vigtige effekter (husk at begge kodestier skal returnere false at ende med en falsk værdi for DMLRequestSort
):
- Det sikrer, at den tidligere
FOptimizeInsert
test mislykkedes , fordi den tredje betingelse ikke er opfyldt (P <3
). P <=16 betingelse i FDemandRowsSortedForPerformance
vil også ikke blive opfyldt.
Vi forventer derfor FastLoadContext
ikke at blive aktiveret. Det modificerede demoscript er:
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)';
Alle 269 rækker er fuldstændig logget som forudsagt:
Bemærk, at uanset hvor højt vi sætter antallet af nye rækker, der skal indsættes, vil scriptet ovenfor aldrig producere minimal logning på grund af P <=16
test (og P <3
test i FOptimizeInsert
).
Hvis du vælger at køre demoen selv med et større antal rækker, skal du kommentere afsnittet, der viser individuelle transaktionslogposter, ellers venter du meget længe, og SSMS kan gå ned. (For at være retfærdig kan det måske gøre det alligevel, men hvorfor øge risikoen.)
Sider pr. indsat rækkeforhold
Hvis der er 17 eller flere bladsider i det eksisterende indeks, det forrige P <=16
testen mislykkes ikke. Det næste afsnit af logikken omhandler forholdet mellem eksisterende sider til nyligt indsatte rækker . Dette skal også passere for at opnå minimal logning . Som en påmindelse er de relevante betingelser:
- Forhold
R
=P
/I
. - Hvis
R <8
:- Hvis
P> 524
returner sand , ellers falsk .
- Hvis
Vi skal også huske den endelige lagermotortest for mindst 100 rækker:
I>=100
.
Reorganiserer disse forhold lidt, alle af følgende skal være sandt:
P> 524
(eksisterende indekssider)I>=100
(anslået indsatte rækker)P / I <8
(forholdR
)
Der er flere måder at opfylde disse tre betingelser på samtidigt. Lad os vælge de minimale mulige værdier for P
(525) og I
(100) giver en R
værdi på (525 / 100) =5,25. Dette opfylder (R <8
test), så vi forventer, at denne kombination vil resultere i minimal logning :
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)';
INSERT...SELECT
med 100 rækker er faktisk minimalt logget :
Reduktion af det estimerede indsatte rækker til 99 (brydende I>=100
), og/eller reducere antallet af eksisterende indekssider til 524 (bryder P> 524
) resulterer i fuld logning . Vi kunne også foretage ændringer, således at R
er ikke længere mindre end 8 for at producere fuld logning . For eksempel indstilling af P =1000
og I =125
giver R =8
, med følgende resultater:
De 125 indsatte rækker blev fuldstændig logget som forventet. (Dette skyldes ikke fuld logning på første side, da indekset ikke var tomt på forhånd.)
Sideforhold for opdelte indekser
Hvis alle de foregående test mislykkes, kræver den resterende test R>=8
og kan kun være tilfreds, når antallet af partitioner (T
) er større end 1 og der er mere end 250 estimerede indsatte rækker (I
). Husk:
- Hvis
R>=8
:- Hvis
T> 1
ogI> 250
returner sand , ellers falsk .
- Hvis
En finesse:Til opdelt indekser, reglen, der siger, at alle rækker på første side er fuldt logget (for et oprindeligt tomt indeks), gælder pr. partition . For et objekt med 15.000 partitioner betyder det 15.000 fuldt loggede 'første' sider.
Opsummering og endelige tanker
Formlerne og evalueringsrækkefølgen beskrevet i brødteksten er baseret på kodeinspektion ved hjælp af en debugger. De blev præsenteret i en form, der nøje repræsenterer timingen og rækkefølgen, der blev brugt i den rigtige kode.
Det er muligt at omarrangere og forenkle disse betingelser en smule for at producere en mere kortfattet oversigt over de praktiske krav til minimal logning når du indsætter i et b-træ ved hjælp af INSERT...SELECT
. De raffinerede udtryk nedenfor bruger følgende tre parametre:
P
=antal eksisterende indekssider på bladniveau.I
=estimeret antal rækker, der skal indsættes.S
=estimeret indsæt datastørrelse i 8KB sider.
Rowset-massebelastning
- Bruger
sqlmin!RowsetBulk
. - Kræver en tom klynget indeksmål med
TABLOCK
(eller tilsvarende). - Kræver
DMLRequestSort =true
på Clustered Index Insert operatør. DMLRequestSort
er sattrue
hvisI> 250
ogS> 2
.- Alle indsatte rækker er minimalt logget .
- En
Sch-M
lås forhindrer samtidig bordadgang.
Hurtig indlæsningskontekst
- Bruger
sqlmin!FastLoadContext
. - Aktiverer minimalt logget indsætter til b-træ indekser:
- Klyngede eller ikke-klyngede.
- Med eller uden bordlås.
- Målindekset er tomt eller ej.
- Kræver
DMLRequestSort =true
på den tilknyttede Index Insert planoperatør. - Kun rækker skrevet til helt nye sider er bulk-indlæst og minimalt logget .
- Den første side af et tidligere tomt indeks partitionen er altid fuldt logget .
- Absolut minimum på
I>=100
. - Kræver sporingsflag 610 før SQL Server 2016.
- Tilgængelig som standard fra SQL Server 2016 (sporingsflag 692 deaktiverer).
DMLRequestSort
er sat true
til:
- Ethvert indeks (opdelt eller ej) hvis:
I> 250
ogP <3
ogS> 2
; ellerI>=100
ogP> 524
ogP
For kun opdelte indekser (med> 1 partition), DMLRequestSort
er også sat true
hvis:
I> 250
ogP> 16
ogP>=I * 8
Der er et par interessante tilfælde, der stammer fra disse FastLoadContext
betingelser:
- Alle indsætter til en ikke-partitioneret indeks med mellem 3 og 524 (inklusive) eksisterende bladsider vil være fuldt logget uanset antallet og den samlede størrelse af de tilføjede rækker. Dette vil mest mærkbart påvirke store indstik til små (men ikke tomme) tabeller.
- Alle indsætter i en partitioneret indeks med mellem 3 og 16 eksisterende sider bliver fuldstændig logget .
- Store indsatser til store ikke-opdelte indekser er muligvis ikke minimalt logget på grund af uligheden
P . Når
P
er stor, en tilsvarende stor estimeret antal indsatte rækker (I
) er påkrævet. For eksempel kan et indeks med 8 millioner sider ikke understøtte minimal logning når du indsætter 1 million rækker eller færre.
Ikke-klyngede indekser
De samme overvejelser og beregninger, der anvendes på klyngede indekser i demoerne, gælder for ikke-klyngede b-tree-indekser også, så længe indekset vedligeholdes af en dedikeret planoperatør (en bred eller pr. indeks plan). Ikke-klyngede indekser, der vedligeholdes af en basistabeloperator (f.eks. Clustered Index Insert ) er ikke kvalificerede til FastLoadContext
.
Bemærk, at formelparametrene skal evalueres på ny for hver ikke-klyngede indeksoperator — beregnet rækkestørrelse, antal eksisterende indekssider og kardinalitetsestimat.
Generelle bemærkninger
Pas på lave kardinalitetsestimater ved Index Indsæt operatør, da disse vil påvirke I
og S
parametre. Hvis en tærskel ikke nås på grund af en kardinalitetsestimeringsfejl, bliver indsættelsen fuldstændig logget .
Husk at DMLRequestSort
er cachelagret med planen — den evalueres ikke ved hver udførelse af en genbrugt plan. Dette kan introducere en form for det velkendte Parameter Sensitivity Problem (også kendt som "parameter sniffing").
Værdien af P
(indeksbladssider) er ikke opdateret i starten af hver erklæring. Den aktuelle implementering cacher værdien for hele batchen . Dette kan have uventede bivirkninger. For eksempel en TRUNCATE TABLE
i samme batch som en INSERT...SELECT
vil ikke nulstille P
til nul for de beregninger, der er beskrevet i denne artikel - de vil fortsætte med at bruge præ-truncate-værdien, og en rekompilering hjælper ikke. En løsning er at indsende store ændringer i separate batches.
Spor flag
Det er muligt at tvinge FDemandRowsSortedForPerformance
for at returnere sand ved at indstille udokumenteret og ikke understøttet spor flag 2332, som jeg skrev i Optimering af T-SQL-forespørgsler, der ændrer data. Når TF 2332 er aktiv, er antallet af estimerede rækker, der skal indsættes skal stadig være mindst 100 . TF 2332 påvirker den minimale logning beslutning for FastLoadContext
kun (det er effektivt for opdelte heaps så langt som DMLRequestSort
er bekymret, men har ingen effekt på selve heapen, da FastLoadContext
gælder kun for indekser).
Et bredt/pr. indeks planform for ikke-klyngede indeksvedligeholdelse kan tvinges til rowstore-tabeller ved hjælp af sporingsflag 8790 (ikke officielt dokumenteret, men nævnt i en Knowledge Base-artikel såvel som i min artikel som linket til TF2332 lige ovenfor).
Relateret læsning
Alt sammen af Sunil Agarwal fra SQL Server-teamet:
- Hvad er masseimportoptimeringerne?
- Optimeringer af masseimport (minimal logning)
- Minimale logføringsændringer i SQL Server 2008
- Minimale logføringsændringer i SQL Server 2008 (del-2)
- Minimale logføringsændringer i SQL Server 2008 (del-3)