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

Sjovt med (columnstore) komprimering på et meget stort bord – del 3

[ Del 1 | Del 2 | Del 3 ]

I del 1 af denne serie prøvede jeg et par måder at komprimere et 1TB bord på. Mens jeg fik anstændige resultater i mit første forsøg, ville jeg se, om jeg kunne forbedre ydeevnen i del 2. Der skitserede jeg et par af de ting, jeg troede kunne være præstationsproblemer, og lagde ud, hvordan jeg bedre kunne partitionere destinationstabellen for optimal kolonnelagerkomprimering. Jeg har allerede:

  • opdelte tabellen i 8 partitioner (én pr. kerne);
  • sæt hver partitions datafil i sin egen filgruppe; og,
  • indstil arkivkomprimering på alle undtagen den "aktive" partition.

Jeg mangler stadig at gøre det, så hver planlægger udelukkende skriver til sin egen partition.

Først skal jeg foretage ændringer i den batch-tabel, jeg oprettede. Jeg har brug for en kolonne til at gemme antallet af tilføjede rækker pr. batch (en slags selvrevisionskontrol) og start-/sluttider for at måle fremskridt.

ALTER TABLE dbo.BatchQueue ADD 
  RowsAdded int,
  StartTime datetime2, 
  EndTime   datetime2;

Dernæst skal jeg oprette en tabel for at give affinitet – vi vil aldrig have mere end én proces, der kører på en planlægger, selvom det betyder, at man mister noget tid til at prøve logik igen. Så vi har brug for en tabel, der holder styr på enhver session på en specifik skemalægger og forhindrer stabling:

CREATE TABLE dbo.OpAffinity
(
  SchedulerID int NOT NULL,
  SessionID   int NULL,
  CONSTRAINT  PK_OpAffinity PRIMARY KEY CLUSTERED (SchedulerID)
);

Ideen er, at jeg vil have otte forekomster af en applikation (SQLQueryStress), der hver vil køre på en dedikeret planlægger, der kun håndterer de data, der er bestemt til en specifik partition/filgruppe/datafil, ~100 millioner rækker ad gangen (klik for at forstørre) :

App 1 får skemalægger 0 og skriver til partition 1 på filgruppe 1, og så videre …

Dernæst har vi brug for en lagret procedure, der gør det muligt for hver instans af applikationen at reservere tid på en enkelt skemalægger. Som jeg nævnte i et tidligere indlæg, er dette ikke min oprindelige idé (og jeg ville aldrig have fundet det i den guide, hvis ikke for Joe Obbish). Her er den procedure, jeg oprettede i Utility :

CREATE PROCEDURE dbo.DoMyBatch
  @PartitionID   int,    -- pass in 1 through 8
  @BatchID       int     -- pass in 1 through 4
AS
BEGIN
  DECLARE @BatchSize       bigint, 
          @MinID           bigint, 
          @MaxID           bigint, 
          @rc              bigint,
          @ThisSchedulerID int = 
          (
            SELECT scheduler_id 
	      FROM sys.dm_exec_requests 
    	      WHERE session_id = @@SPID
          );
 
  -- try to get the requested scheduler, 0-based
  IF @ThisSchedulerID <> @PartitionID - 1 
  BEGIN
    -- surface the scheduler we got to the application, but force a delay
    RAISERROR('Got wrong scheduler %d.', 11, 1, @ThisSchedulerID);
    WAITFOR DELAY '00:00:05';
    RETURN -3;
  END
  ELSE
  BEGIN
    -- we are on our scheduler, now serializibly make sure we're exclusive
    INSERT Utility.dbo.OpAffinity(SchedulerID, SessionID)
      SELECT @ThisSchedulerID, @@SPID
        WHERE NOT EXISTS 
        (
          SELECT 1 FROM Utility.dbo.OpAffinity WITH (TABLOCKX) 
            WHERE SchedulerID = @ThisSchedulerID
        );
 
    -- if someone is already using this scheduler, raise roar:
    IF @@ROWCOUNT <> 1
    BEGIN
      RAISERROR('Wrong scheduler %d, try again.',11,1,@ThisSchedulerID) WITH NOWAIT;
      RETURN @ThisSchedulerID;
    END
 
    -- checkpoint twice to clear log
    EXEC OCopy.sys.sp_executesql N'CHECKPOINT; CHECKPOINT;';
 
    -- get our range of rows for the current batch
    SELECT @MinID = MinID, @MaxID = MaxID
      FROM Utility.dbo.BatchQueue 
      WHERE PartitionID = @PartitionID
        AND BatchID = @BatchID
        AND StartTime IS NULL;
 
    -- if we couldn't get a row here, must already be done:
    IF @@ROWCOUNT <> 1
    BEGIN
      RAISERROR('Already done.', 11, 1) WITH NOWAIT;
      RETURN -1;
    END
 
    -- update the BatchQueue table to indicate we've started:
    UPDATE msdb.dbo.BatchQueue 
      SET StartTime = sysdatetime(), EndTime = NULL
      WHERE PartitionID = @PartitionID
        AND BatchID = @BatchID;
 
    -- do the work - copy from Original to Partitioned
    INSERT OCopy.dbo.tblPartitionedCCI 
      SELECT * FROM OCopy.dbo.tblOriginal AS o
        WHERE o.CostID >= @MinID AND o.CostID <= @MaxID
        OPTION (MAXDOP 1); -- don't want parallelism here!
 
    /*
        You might think, don't I want a TABLOCK hint on the insert, 
        to benefit from minimal logging? I thought so too, but while 
        this leads to a BULK UPDATE lock on rowstore tables, it is a 
        TABLOCKX with columnstore. This isn't going to work well if 
        we want to have multiple processes inserting into separate 
        partitions simultaneously. We need a PARTITIONLOCK hint!
    */
 
    SET @rc = @@ROWCOUNT;
 
    -- update BatchQueue that we've finished and how many rows:
    UPDATE Utility.dbo.BatchQueue 
      SET EndTime = sysdatetime(), RowsAdded = @rc
      WHERE PartitionID = @PartitionID
        AND BatchID = @BatchID;
 
    -- remove our lock to this scheduler:
    DELETE Utility.dbo.OpAffinity 
      WHERE SchedulerID = @ThisSchedulerID 
        AND SessionID = @@SPID;
  END
END

Simpelt, ikke? Start 8 forekomster af SQLQueryStress, og sæt denne batch i hver:

EXEC dbo.DoMyBatch @PartitionID = /* PartitionID - 1 through 8 */, @BatchID = 1;
EXEC dbo.DoMyBatch @PartitionID = /* PartitionID - 1 through 8 */, @BatchID = 2;
EXEC dbo.DoMyBatch @PartitionID = /* PartitionID - 1 through 8 */, @BatchID = 3;
EXEC dbo.DoMyBatch @PartitionID = /* PartitionID - 1 through 8 */, @BatchID = 4;

Fattig mands parallelitet

Bortset fra, at det ikke er så enkelt, da planlægningsopgave er lidt som en æske chokolade. Det tog mange forsøg at få hver forekomst af appen på den forventede planlægger; Jeg ville inspicere undtagelserne på en given forekomst af appen og ændre PartitionID at matche. Det er derfor, jeg brugte mere end én iteration (men jeg ville stadig kun have én tråd pr. instans). Som et eksempel forventede denne forekomst af appen at være på skemalægger 3, men den fik skemalægger 4:

Hvis du først ikke lykkes...

Jeg ændrede 3'erne i forespørgselsvinduet til 4'er og prøvede igen. Hvis jeg var hurtig, var planlægningsopgaven "klæbende" nok til at den ville tage den op og begynde at tude væk. Men jeg var ikke altid hurtig nok, så det var lidt som en muldvarp at komme afsted. Jeg kunne nok have udtænkt en bedre genforsøg/sløjfe-rutine for at gøre arbejdet mindre manuelt her, og forkorte forsinkelsen, så jeg vidste med det samme, om det virkede eller ej, men dette var godt nok til mine behov. Det sørgede også for en utilsigtet forveksling af starttider for hver proces, endnu et råd fra hr. Obbish.

Overvågning

Mens den affiniterede kopi kører, kan jeg få et tip om den aktuelle status med følgende to forespørgsler:

SELECT r.session_id, r.[status], r.scheduler_id, partition_id = o.SchedulerID + 1, 
  r.logical_reads, r.total_elapsed_time, r.last_wait_type, longest_wait_type = 
  (
    SELECT TOP (1) wait_type 
      FROM sys.dm_exec_session_wait_stats
      WHERE session_id = r.session_id AND wait_type <> 'WAITFOR' 
      ORDER BY wait_time_ms - signal_wait_time_ms DESC
  )
  FROM sys.dm_exec_requests AS r 
  INNER JOIN Utility.dbo.OpAffinity AS o
      ON o.SessionID = r.session_id
  WHERE r.command = N'INSERT'
  ORDER BY r.scheduler_id;
 
SELECT SchedulerID = PartitionID - 1, Duration = DATEDIFF(SECOND, StartTime, EndTime), *
  FROM Utility.dbo.BatchQueue WITH (NOLOCK) 
  WHERE StartTime IS NOT NULL -- AND EndTime IS NULL
  ORDER BY PartitionID;

Hvis jeg gjorde alt rigtigt, ville begge forespørgsler returnere 8 rækker og vise stigende logiske læsninger og varighed. Ventetyper vil vende rundt mellem PAGEIOLATCH_SH , SOS_SCHEDULER_YIELD , og lejlighedsvis RESERVED_MEMORY_ALLOCATION_EXT. Når en batch var færdig (jeg kunne gennemgå disse ved at fjerne kommentarer til -- AND EndTime IS NULL , vil jeg bekræfte, at RowsAdded = RowsInRange .

Når alle 8 forekomster af SQLQueryStress var gennemført, kunne jeg bare udføre en SELECT INTO <newtable> FROM dbo.BatchQueue for at logge de endelige resultater til senere analyse.

Anden test

Ud over at kopiere dataene til det partitionerede clustered columnstore-indeks, der allerede eksisterede, ved at bruge affinitet, ville jeg også prøve et par andre ting:

  • Kopiering af data til den nye tabel uden at forsøge at kontrollere affinitet. Jeg tog affinitetslogikken ud af proceduren og overlod bare hele "håber-du-får-den-rigtige-planlægger"-ting til tilfældighederne. Dette tog længere tid, fordi planlægningsstabling gjorde det forekomme. For eksempel, på dette specifikke tidspunkt kørte skemalægger 3 to processer, mens skemalægger 0 holdt frokostpause:

    Hvor er du, skemalægger nummer 0?

  • Anvender side eller række komprimering (både online/offline) til kilden før den affiniterede kopi (offline), for at se, om komprimering af dataene først kunne fremskynde destinationen. Bemærk, at kopien også kunne laves online, men ligesom Andy Mallons int til bigint omstilling, kræver det noget gymnastik. Bemærk, at vi i dette tilfælde ikke kan drage fordel af CPU-affinitet (selvom vi kunne, hvis kildetabellen allerede var partitioneret). Jeg var smart og tog en sikkerhedskopi af den originale kilde og lavede en procedure for at vende databasen tilbage til dens oprindelige tilstand. Meget hurtigere og nemmere end at forsøge at vende tilbage til en bestemt tilstand manuelt.

    -- refresh source, then do page online:
    ALTER TABLE dbo.tblOriginal REBUILD WITH (DATA_COMPRESSION = PAGE, ONLINE = ON);
    -- then run SQLQueryStress
     
    -- refresh source, then do page offline:
    ALTER TABLE dbo.tblOriginal REBUILD WITH (DATA_COMPRESSION = PAGE, ONLINE = OFF);
    -- then run SQLQueryStress
     
    -- refresh source, then do row online:
    ALTER TABLE dbo.tblOriginal REBUILD WITH (DATA_COMPRESSION = ROW, ONLINE = ON);
    -- then run SQLQueryStress
     
    -- refresh source, then do row offline:
    ALTER TABLE dbo.tblOriginal REBUILD WITH (DATA_COMPRESSION = ROW, ONLINE = OFF);
    -- then run SQLQueryStress
  • Og til sidst skal du først genopbygge det klyngede indeks på partitionsskemaet, og derefter bygge det klyngede kolonnelagerindeks ovenpå. Ulempen ved sidstnævnte er, at du i SQL Server 2017 ikke kan køre dette online... men du vil være i stand til det i 2019.

    Her skal vi først droppe PK-begrænsningen; du kan ikke bruge DROP_EXISTING , da den oprindelige unikke begrænsning ikke kan håndhæves af det klyngede kolonnelagerindeks, og du kan ikke erstatte et unikt klynget indeks med et ikke-entydigt klynget indeks.

    Meddelelse 1907, niveau 16, tilstand 1
    Kan ikke genskabe indeks 'pk_tblOriginal'. Den nye indeksdefinition matcher ikke den begrænsning, der håndhæves af det eksisterende indeks.

    Alle disse detaljer gør dette til en tre-trins proces, kun det andet trin online. Det første trin testede jeg kun udtrykkeligt OFFLINE; der kørte på tre minutter, mens ONLINE Jeg stoppede efter 15 minutter. En af de ting, der måske ikke burde være en datastørrelsesoperation i begge tilfælde, men det lader jeg stå til en anden dag.

    ALTER TABLE dbo.tblOriginal DROP CONSTRAINT PK_tblOriginal WITH (ONLINE = OFF);
    GO
     
    CREATE CLUSTERED INDEX CCI_tblOriginal -- yes, a bad name, but only temporarily
      ON dbo.tblOriginal(OID)
      WITH (ONLINE = ON)
      ON PS_OID (OID); -- this moves the data
     
     
    CREATE CLUSTERED COLUMNSTORE INDEX CCI_tblOriginal
      ON dbo.tblOriginal
      WITH                 
      (
        DROP_EXISTING = ON,
        DATA_COMPRESSION = COLUMNSTORE_ARCHIVE ON PARTITIONS (1 TO 7),
        DATA_COMPRESSION = COLUMNSTORE ON PARTITIONS (8)
        -- in 2019, CCI can be ONLINE = ON as well
      )
      ON PS_OID (OID);
    GO

Resultater

Timings og kompressionshastigheder:

Nogle muligheder er bedre end andre

Bemærk, at jeg rundede af til GB, fordi der ville være mindre forskelle i den endelige størrelse efter hvert løb, selv ved at bruge den samme teknik. Timingerne for affinitetsmetoderne var også baseret på gennemsnittet individuel skemalægger/batch runtime, da nogle skemalæggere blev færdige hurtigere end andre.

Det er svært at forestille sig et nøjagtigt billede fra regnearket som vist, fordi nogle opgaver har afhængigheder, så jeg vil forsøge at vise informationen som en tidslinje og vise, hvor meget komprimering du får i forhold til den brugte tid:

Tidsforbrug (minutter) vs. kompressionshastighed

Et par observationer fra resultaterne, med det forbehold, at dine data kan komprimere anderledes (og at online operationer kun gælder for dig, hvis du bruger Enterprise Edition):

  • Hvis din prioritet er at spare lidt plads så hurtigt som muligt , er dit bedste bud at anvende rækkekompression på plads. Hvis du vil minimere forstyrrelser, så brug online; hvis du vil optimere hastigheden, skal du bruge offline.
  • Hvis du vil maksimere komprimeringen uden afbrydelse , kan du nærme dig 90 % lagerreduktion uden nogen afbrydelse overhovedet ved at bruge sidekomprimering online.
  • Hvis du vil maksimere komprimering og afbrydelse er okay , kopier dataene til en ny, opdelt version af tabellen med et klynget kolonnelagerindeks, og brug affinitetsprocessen beskrevet ovenfor til at migrere dataene. (Og igen, du kan eliminere denne forstyrrelse, hvis du er en bedre planlægger end mig.)

Den sidste mulighed fungerede bedst for mit scenarie, selvom vi stadig bliver nødt til at sparke dækkene på arbejdsbelastningen (ja, flertal). Bemærk også, at i SQL Server 2019 virker denne teknik muligvis ikke så godt, men du kan bygge klyngede kolonnelagerindekser online der, så det betyder måske ikke så meget.

Nogle af disse tilgange kan være mere eller mindre acceptable for dig, fordi du måske foretrækker "at forblive tilgængelig" frem for "færdiggøre så hurtigt som muligt" eller "minimere diskforbrug" frem for "at forblive tilgængelig" eller bare balancere læseydelse og skriveoverhead .

Hvis du vil have flere detaljer om et eller andet aspekt af dette, så spørg bare. Jeg trimmede noget af fedtet for at balancere detaljer med fordøjelighed, og jeg har før taget fejl af den balance. En afskedstanke er, at jeg er nysgerrig efter, hvor lineært dette er – vi har et andet bord med en lignende struktur, der er over 25 TB, og jeg er spændt på, om vi kan få en lignende effekt der. Indtil da, glad komprimering!

[ Del 1 | Del 2 | Del 3 ]


  1. SQRT() Eksempler i SQL Server

  2. SQL Server kumulativ sum efter gruppe

  3. I Rails, kunne ikke oprette database for {adapter=>postgresql,

  4. Nulstille en kumulativ sum?