sql >> Databasteknik >  >> RDS >> Database

Minimal loggning med INSERT...SELECT och snabbladdningskontext

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=trueClustered 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 .
  • Iuppskattad 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 R>=8 :
      • Om T> 1 och I> 250 returnera true , annars falskt .

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; och
  • I>=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 ):

  1. Det säkerställer att föregående FOptimizeInsert testet misslyckas , eftersom det tredje villkoret inte är uppfyllt (P <3 ).
  2. 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 .

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:

  1. P> 524 (befintliga indexsidor)
  2. I>=100 (uppskattade infogade rader)
  3. P / I <8 (förhållande R )

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 och I> 250 returnera true , annars falskt .

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 =trueClustered Index Insert operatör.
  • DMLRequestSort är satt till true om I> 250 och S> 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 och P <3 och S> 2; eller
    • I>=100 och P> 524 och P

Endast för partitionerade index (med> 1 partition), DMLRequestSort är också satt till true om:

  • I> 250 och P> 16 och P>=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).

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)

  1. Hur får man poster slumpmässigt från Oracle-databasen?

  2. Hur man skapar en riktig en-till-en-relation i SQL Server

  3. MySQL-ordning efter bästa matchning

  4. Oracle Shutdown-fel ORA-01033