sql >> Database teknologi >  >> RDS >> Database

Minimal logning med INSERT...SELECT og hurtig indlæsningskontekst

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=trueClustered 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 .
  • Iestimeret 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 R>=8 :
      • Hvis T> 1 og I> 250 returner sand , ellers falsk .

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

  1. Det sikrer, at den tidligere FOptimizeInsert test mislykkedes , fordi den tredje betingelse ikke er opfyldt (P <3 ).
  2. 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 .

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:

  1. P> 524 (eksisterende indekssider)
  2. I>=100 (anslået indsatte rækker)
  3. P / I <8 (forhold R )

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 og I> 250 returner sand , ellers falsk .

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 =trueClustered Index Insert operatør.
  • DMLRequestSort er sat true hvis I> 250 og S> 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 og P <3 og S> 2; eller
    • I>=100 og P> 524 og P

For kun opdelte indekser (med> 1 partition), DMLRequestSort er også sat true hvis:

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

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)

  1. Optimering af Microsoft Access med SQL Server IndyPass – 21/5/19

  2. Vedvarende UUID i PostgreSQL ved hjælp af JPA

  3. MariaDB LENGTHB() Forklaret

  4. mysql_num_rows():det leverede argument er ikke en gyldig MySQL resultatressource