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

Kan ColumnStore hjælpe med sideinddelingsarbejdsbelastninger?

For næsten et år siden i dag postede jeg min løsning til paginering i SQL Server, som indebar at bruge en CTE til kun at finde nøgleværdierne for det pågældende sæt af rækker og derefter slutte tilbage fra CTE til kildetabellen for at hente de andre kolonner for netop den "side" af rækker. Dette viste sig at være mest fordelagtigt, når der var et smalt indeks, der understøttede den bestilling, som brugeren havde anmodet om, eller når bestillingen var baseret på clustering-nøglen, men endda klarede sig lidt bedre uden et indeks til at understøtte den påkrævede sortering.

Siden da har jeg spekuleret på, om ColumnStore-indekser (både klyngede og ikke-klyngede) kunne hjælpe nogen af ​​disse scenarier. TL;DR :Baseret på dette eksperiment isoleret set er svaret på titlen på dette indlæg et rungende NEJ . Hvis du ikke ønsker at se testopsætningen, koden, udførelsesplanerne eller graferne, er du velkommen til at springe til mit resumé, mens du husker på, at min analyse er baseret på en meget specifik use case.

Opsætning

På en ny VM med SQL Server 2016 CTP 3.2 (13.0.900.73) installeret, kørte jeg gennem nogenlunde samme opsætning som før, kun denne gang med tre tabeller. Først en traditionel tabel med en smal klyngenøgle og flere understøttende indekser:

CREATE TABLE [dbo].[Customers]
(
	[CustomerID] [int] NOT NULL,
	[FirstName] [nvarchar](64) NOT NULL,
	[LastName] [nvarchar](64) NOT NULL,
	[EMail] [nvarchar](320) NOT NULL UNIQUE,
	[Active] [bit] NOT NULL DEFAULT 1,
	[Created] [datetime] NOT NULL DEFAULT SYSDATETIME(),
	[Updated] [datetime] NULL,
  CONSTRAINT [PK_Customers] PRIMARY KEY CLUSTERED ([CustomerID])
);
 
CREATE NONCLUSTERED INDEX [Active_Customers] 
ON [dbo].[Customers]([FirstName],[LastName],[EMail])
WHERE ([Active]=1);
 
-- to support "PhoneBook" sorting (order by Last,First)
CREATE NONCLUSTERED INDEX [PhoneBook_Customers] 
ON [dbo].[Customers]([LastName],[FirstName])
INCLUDE ([EMail]);

Dernæst en tabel med et klynget ColumnStore-indeks:

CREATE TABLE [dbo].[Customers_CCI]
(
	[CustomerID] [int] NOT NULL,
	[FirstName] [nvarchar](64) NOT NULL,
	[LastName] [nvarchar](64) NOT NULL,
	[EMail] [nvarchar](320) NOT NULL UNIQUE,
	[Active] [bit] NOT NULL DEFAULT 1,
	[Created] [datetime] NOT NULL DEFAULT SYSDATETIME(),
	[Updated] [datetime] NULL,
  CONSTRAINT [PK_CustomersCCI] PRIMARY KEY NONCLUSTERED ([CustomerID])
);
 
CREATE CLUSTERED COLUMNSTORE INDEX [Customers_CCI] 
ON [dbo].[Customers_CCI];

Og endelig en tabel med et ikke-klynget ColumnStore-indeks, der dækker alle kolonnerne:

CREATE TABLE [dbo].[Customers_NCCI]
(
	[CustomerID] [int] NOT NULL,
	[FirstName] [nvarchar](64) NOT NULL,
	[LastName] [nvarchar](64) NOT NULL,
	[EMail] [nvarchar](320) NOT NULL UNIQUE,
	[Active] [bit] NOT NULL DEFAULT 1,
	[Created] [datetime] NOT NULL DEFAULT SYSDATETIME(),
	[Updated] [datetime] NULL,
  CONSTRAINT [PK_CustomersNCCI] PRIMARY KEY CLUSTERED 
  ([CustomerID])
);
 
CREATE NONCLUSTERED COLUMNSTORE INDEX [Customers_NCCI] 
ON [dbo].[Customers_NCCI]
(
        [CustomerID],
	[FirstName],
	[LastName],
	[EMail],
	[Active],
        [Created],
        [Updated]
);

Bemærk, at for begge tabeller med ColumnStore-indekser udelod jeg det indeks, der ville understøtte hurtigere søgninger på "Telefonbog"-sorteringen (efternavn, fornavn).

Testdata

Jeg udfyldte derefter den første tabel med 1.000.000 tilfældige rækker, baseret på et script, jeg har genbrugt fra tidligere indlæg:

INSERT dbo.Customers WITH (TABLOCKX) 
  (CustomerID, FirstName, LastName, EMail, [Active])
SELECT rn = ROW_NUMBER() OVER (ORDER BY n), fn, ln, em, a
FROM 
(
  SELECT TOP (1000000) fn, ln, em, a = MAX(a), n = MAX(NEWID())
  FROM
  (
    SELECT fn, ln, em, a, r = ROW_NUMBER() OVER (PARTITION BY em ORDER BY em)
    FROM
    (
      SELECT TOP (2000000)
        fn = LEFT(o.name, 64), 
        ln = LEFT(c.name, 64), 
        em = LEFT(o.name, LEN(c.name)%5+1) + '.' 
             + LEFT(c.name, LEN(o.name)%5+2) + '@' 
             + RIGHT(c.name, LEN(o.name+c.name)%12 + 1) 
             + LEFT(RTRIM(CHECKSUM(NEWID())),3) + '.com', 
        a  = CASE WHEN c.name LIKE '%y%' THEN 0 ELSE 1 END
      FROM sys.all_objects AS o CROSS JOIN sys.all_columns AS c 
      ORDER BY NEWID()
    ) AS x
  ) AS y WHERE r = 1 
  GROUP BY fn, ln, em 
  ORDER BY n
) AS z 
ORDER BY rn;

Så brugte jeg den tabel til at udfylde de to andre med nøjagtig de samme data, og genopbyggede alle indekserne:

INSERT dbo.Customers_CCI WITH (TABLOCKX)
  (CustomerID, FirstName, LastName, EMail, [Active])
SELECT CustomerID, FirstName, LastName, EMail, [Active]
FROM dbo.Customers;
 
INSERT dbo.Customers_NCCI WITH (TABLOCKX)
  (CustomerID, FirstName, LastName, EMail, [Active])
SELECT CustomerID, FirstName, LastName, EMail, [Active]
FROM dbo.Customers;
 
ALTER INDEX ALL ON dbo.Customers      REBUILD;
ALTER INDEX ALL ON dbo.Customers_CCI  REBUILD;
ALTER INDEX ALL ON dbo.Customers_NCCI REBUILD;

Den samlede størrelse af hver tabel:

Tabel Reserveret Data Indeks
Kunder 463.200 KB 154.344 KB 308.576 KB
Customers_CCI 117.280 KB 30.288 KB 86.536 KB
Customers_NCCI 349.480 KB 154.344 KB 194.976 KB


Og rækkeantallet/sidetallet for de relevante indekser (det unikke indeks på e-mail var der mere for mig at passe mit eget datagenereringsscript end noget andet):

Tabel Indeks Rækker Sider
Kunder PK_Kunder 1.000.000 19.377
Kunder Telefonbog_Kunder 1.000.000 17.209
Kunder Active_Customers 808.012 13.977
Customers_CCI PK_CustomersCCI 1.000.000 2.737
Customers_CCI Customers_CCI 1.000.000 3.826
Customers_NCCI PK_CustomersNCCI 1.000.000 19.377
Customers_NCCI Customers_NCCI 1.000.000 16.971

Procedurer

Så, for at se, om ColumnStore-indekserne ville slå ind og gøre nogen af ​​scenarierne bedre, kørte jeg det samme sæt forespørgsler som før, men nu mod alle tre tabeller. Jeg blev i det mindste en lille smule klogere og lavede to lagrede procedurer med dynamisk SQL for at acceptere tabelkilden og sorteringsrækkefølgen. (Jeg er godt klar over SQL-injektion; det er ikke, hvad jeg ville gøre i produktionen, hvis disse strenge kom fra en slutbruger, så tag det ikke som en anbefaling at gøre det. Jeg stoler lige nok på mig selv i min lukket miljø, at det ikke er et problem for disse tests.)

CREATE PROCEDURE dbo.P_Old
  @PageNumber  INT = 1,
  @PageSize    INT = 100,
  @Table       SYSNAME,
  @Sort        VARCHAR(32)
AS
BEGIN
  SET NOCOUNT ON;
 
  DECLARE @sql NVARCHAR(MAX) = N'
 
  SELECT CustomerID, FirstName, LastName,
      EMail, Active, Created, Updated
    FROM dbo.' + QUOTENAME(@Table) + N'
    ORDER BY ' + CASE @Sort 
	  WHEN 'Key'         THEN N'CustomerID'
	  WHEN 'PhoneBook'   THEN N'LastName, FirstName'
	  WHEN 'Unsupported' THEN N'FirstName DESC, EMail'
	END
	+ N'
    OFFSET @PageSize * (@PageNumber - 1) ROWS
    FETCH NEXT @PageSize ROWS ONLY OPTION (RECOMPILE);';
 
  EXEC sys.sp_executesql @sql, N'@PageSize INT, @PageNumber INT', @PageSize, @PageNumber;
END
GO
 
CREATE PROCEDURE dbo.P_CTE
  @PageNumber  INT = 1,
  @PageSize    INT = 100,
  @Table       SYSNAME,
  @Sort        VARCHAR(32)
AS
BEGIN
  SET NOCOUNT ON;
 
  DECLARE @sql NVARCHAR(MAX) = N'
 
  ;WITH pg AS
  (
    SELECT CustomerID
      FROM dbo.' + QUOTENAME(@Table) + N'
      ORDER BY ' + CASE @Sort 
	  WHEN 'Key'         THEN N'CustomerID'
	  WHEN 'PhoneBook'   THEN N'LastName, FirstName'
	  WHEN 'Unsupported' THEN N'FirstName DESC, EMail'
	END
	+ N' OFFSET @PageSize * (@PageNumber - 1) ROWS
      FETCH NEXT @PageSize ROWS ONLY
  )
  SELECT c.CustomerID, c.FirstName, c.LastName,
      c.EMail, c.Active, c.Created, c.Updated
  FROM dbo.' + QUOTENAME(@Table) + N' AS c
  WHERE EXISTS (SELECT 1 FROM pg WHERE pg.CustomerID = c.CustomerID)
  ORDER BY ' + CASE @Sort 
	  WHEN 'Key'         THEN N'CustomerID'
	  WHEN 'PhoneBook'   THEN N'LastName, FirstName'
	  WHEN 'Unsupported' THEN N'FirstName DESC, EMail'
	END
	+ N' OPTION (RECOMPILE);';
 
  EXEC sys.sp_executesql @sql, N'@PageSize INT, @PageNumber INT', @PageSize, @PageNumber;
END
GO

Så lavede jeg noget mere dynamisk SQL for at generere alle de kombinationer af opkald, jeg skulle foretage for at kalde både de gamle og nye lagrede procedurer, i alle tre af de ønskede sorteringsrækkefølger og på forskellige sidetal (for at simulere behov en side nær begyndelsen, midten og slutningen af ​​sorteringsrækkefølgen). Så jeg kunne kopiere PRINT output og indsæt det i SQL Sentry Plan Explorer for at få runtime-metrics, jeg kørte denne batch to gange, én gang med procedures CTE ved hjælp af P_Old , og derefter igen ved at bruge P_CTE .

DECLARE @sql NVARCHAR(MAX) = N'';
 
;WITH [tables](name) AS 
(
  SELECT N'Customers' UNION ALL SELECT N'Customers_CCI' 
  UNION ALL SELECT N'Customers_NCCI'
),
sorts(sort) AS
(
  SELECT 'Key' UNION ALL SELECT 'PhoneBook' UNION ALL SELECT 'Unsupported'
),
pages(pagenumber) AS
(
  SELECT 1 UNION ALL SELECT 500 UNION ALL SELECT 5000 UNION ALL SELECT 9999
),
procedures(name) AS
(
  SELECT N'P_CTE' -- N'P_Old'
)
SELECT @sql += N'
  EXEC dbo.' + p.name
  + N' @Table = N' + CHAR(39) + t.name
  + CHAR(39) + N', @Sort = N' + CHAR(39)
  + s.sort + CHAR(39) + N', @PageNumber = ' 
  + CONVERT(NVARCHAR(11), pg.pagenumber) + N';'
FROM tables AS t
  CROSS JOIN sorts AS s
  CROSS JOIN pages AS pg
  CROSS JOIN procedures AS p
  ORDER BY t.name, s.sort, pg.pagenumber;
 
PRINT @sql;

Dette producerede output som dette (36 kalder i alt for den gamle metode (P_Old ), og 36 kalder på den nye metode (P_CTE )):

  EXEC dbo.P_CTE @Table = N'Customers', @Sort = N'Key', @PageNumber = 1;
  EXEC dbo.P_CTE @Table = N'Customers', @Sort = N'Key', @PageNumber = 500;
  EXEC dbo.P_CTE @Table = N'Customers', @Sort = N'Key', @PageNumber = 5000;
  EXEC dbo.P_CTE @Table = N'Customers', @Sort = N'Key', @PageNumber = 9999;
  EXEC dbo.P_CTE @Table = N'Customers', @Sort = N'PhoneBook', @PageNumber = 1;
  ...
  EXEC dbo.P_CTE @Table = N'Customers', @Sort = N'PhoneBook', @PageNumber = 9999;
  EXEC dbo.P_CTE @Table = N'Customers', @Sort = N'Unsupported', @PageNumber = 1;
  ...
  EXEC dbo.P_CTE @Table = N'Customers', @Sort = N'Unsupported', @PageNumber = 9999;
  EXEC dbo.P_CTE @Table = N'Customers_CCI', @Sort = N'Key', @PageNumber = 1;
  ...
  EXEC dbo.P_CTE @Table = N'Customers_CCI', @Sort = N'Unsupported', @PageNumber = 9999;
  EXEC dbo.P_CTE @Table = N'Customers_NCCI', @Sort = N'Key', @PageNumber = 1;
  ...
  EXEC dbo.P_CTE @Table = N'Customers_NCCI', @Sort = N'Unsupported', @PageNumber = 9999;

Jeg ved, det hele er meget besværligt; vi kommer snart til punchline, det lover jeg.

Resultater

Jeg tog disse to sæt med 36 sætninger og startede to nye sessioner i Plan Explorer, hvor jeg kørte hvert sæt flere gange for at sikre, at vi fik data fra en varm cache og tog gennemsnit (jeg kunne også sammenligne kold og varm cache, men jeg tror, ​​der er nok variable her).

Jeg kan umiddelbart fortælle dig et par simple fakta uden overhovedet at vise dig understøttende grafer eller planer:

  • I intet tilfælde slog den "gamle" metode den nye CTE-metode Jeg promoverede i mit tidligere indlæg, uanset hvilken type indekser der var til stede. Så det gør det nemt næsten at ignorere halvdelen af ​​resultaterne, i det mindste med hensyn til varighed (som er den metriske slutbrugere bekymrer sig mest om).
  • Intet ColumnStore-indeks klarede sig godt, når man bladrede mod slutningen af ​​resultatet – de gav kun fordele i begyndelsen, og kun i et par tilfælde.
  • Når du sorterer efter den primære nøgle (grupperet eller ej), tilstedeværelsen af ​​ColumnStore-indekser hjalp ikke – igen med hensyn til varighed.

Med disse resuméer af vejen, lad os tage et kig på nogle få tværsnit af varighedsdataene. Først resultaterne af forespørgslen sorteret efter fornavn faldende, derefter e-mail, uden håb om at bruge et eksisterende indeks til sortering. Som du kan se i diagrammet, var ydeevnen inkonsekvent – ​​ved lavere sidetal klarede den ikke-klyngede ColumnStore sig bedst; ved højere sidetal vandt det traditionelle indeks altid:

Varighed (millisekunder) for forskellige sidetal og forskellige indekstyper

Og så de tre planer, der repræsenterer de tre forskellige typer indekser (med gråtoner tilføjet af Photoshop for at fremhæve de store forskelle mellem planerne):

Plan for traditionelt indeks

Plan for klynget ColumnStore-indeks

Plan for ikke-klyngede ColumnStore-indeks

Et scenarie, jeg var mere interesseret i, selv før jeg begyndte at teste, var tilgangen til telefonbogssortering (efternavn, fornavn). I dette tilfælde var ColumnStore-indekserne faktisk ret skadelige for resultatets ydeevne:

ColumnStore-planerne her er næsten spejlbilleder til de to ColumnStore-planer vist ovenfor for den ikke-understøttede sortering. Årsagen er den samme i begge tilfælde:dyre scanninger eller sorteringer på grund af mangel på et sorteringsunderstøttende indeks.

Så dernæst oprettede jeg understøttende "Telefonbog"-indekser på tabellerne med ColumnStore-indekserne også for at se, om jeg kunne lokke en anden plan og/eller hurtigere eksekveringstider i nogen af ​​disse scenarier. Jeg oprettede disse to indekser og genopbyggede derefter igen:

CREATE NONCLUSTERED INDEX [PhoneBook_CustomersCCI] 
ON [dbo].[Customers_CCI]([LastName],[FirstName])
INCLUDE ([EMail]);
 
ALTER INDEX ALL ON dbo.Customers_CCI  REBUILD;
 
CREATE NONCLUSTERED INDEX [PhoneBook_CustomersNCCI] 
ON [dbo].[Customers_NCCI]([LastName],[FirstName])
INCLUDE ([EMail]);
 
ALTER INDEX ALL ON dbo.Customers_NCCI REBUILD;

Her var de nye varigheder:

Det mest interessante her er, at nu ser sidesøgningsforespørgslen mod tabellen med det ikke-klyngede ColumnStore-indeks ud til at holde trit med det traditionelle indeks, indtil vi kommer ud over midten af ​​tabellen. Ser vi på planerne, kan vi se, at på side 5.000 bruges en traditionel indeksscanning, og ColumnStore-indekset ignoreres fuldstændigt:

Telefonbogsplan ignorerer det ikke-klyngede ColumnStore-indeks

Men et sted mellem midtpunktet på 5.000 sider og "slutningen" af tabellen på 9.999 sider, har optimeringsværktøjet ramt en slags vippepunkt og vælger – for nøjagtig samme forespørgsel – nu at scanne det ikke-klyngede ColumnStore-indeks :

Tip til telefonbogsplan og bruger ColumnStore-indekset

Dette viser sig at være en knap så god beslutning af optimeringsværktøjet, primært på grund af omkostningerne ved sorteringsoperationen. Du kan se, hvor meget bedre varigheden bliver, hvis du antyder det almindelige indeks:

-- ...
;WITH pg AS
  (
    SELECT CustomerID
      FROM dbo.[Customers_NCCI] WITH (INDEX(PhoneBook_CustomersNCCI)) -- hint here
      ORDER BY LastName, FirstName OFFSET @PageSize * (@PageNumber - 1) ROWS
      FETCH NEXT @PageSize ROWS ONLY
  )
-- ...

Dette giver følgende plan, næsten identisk med den første plan ovenfor (en lidt højere pris for scanningen, dog simpelthen fordi der er mere output):

Telefonbogsplan med antydet indeks

Du kan opnå det samme ved at bruge OPTION (IGNORE_NONCLUSTERED_COLUMNSTORE_INDEX) i stedet for det eksplicitte indekstip. Bare husk på, at dette er det samme som ikke at have ColumnStore-indekset der i første omgang.

Konklusion

Selvom der er et par kantsager ovenfor, hvor et ColumnStore-indeks (næppe) kan betale sig, forekommer det mig ikke, at de passer godt til dette specifikke pagineringsscenarie. Jeg tror, ​​vigtigst af alt, mens ColumnStore viser betydelige pladsbesparelser på grund af komprimering, er runtime-ydeevnen ikke fantastisk på grund af sorteringskravene (selvom disse sorteringer anslås at køre i batch-tilstand, en ny optimering til SQL Server 2016).

Generelt kunne dette gøre med en hel del mere tid brugt på forskning og test; i piggy-backing off af tidligere artikler, jeg ønskede at ændre så lidt som muligt. Jeg ville elske at finde det vendepunkt, for eksempel, og jeg vil også gerne erkende, at disse ikke ligefrem er massive tests (på grund af VM-størrelse og hukommelsesbegrænsninger), og at jeg lod dig gætte på en masse runtime-metrikkene (mest for kortheds skyld, men jeg ved ikke, at et diagram over læsninger, der ikke altid er proportionale med varigheden, virkelig ville fortælle dig). Disse tests forudsætter også luksusen ved SSD'er, tilstrækkelig hukommelse, en altid varm cache og et enkeltbrugermiljø. Jeg vil virkelig gerne udføre et større batteri af tests mod flere data, på større servere med langsommere diske og instanser med mindre hukommelse, hele tiden med simuleret samtidighed.

Når det er sagt, kan dette også bare være et scenarie, som ColumnStore ikke er designet til at hjælpe med at løse i første omgang, da den underliggende løsning med traditionelle indekser allerede er ret effektiv til at trække et smalt sæt rækker ud – ikke ligefrem ColumnStores styrehus. Måske er en anden variabel, der skal tilføjes til matricen, sidestørrelsen – alle testene ovenfor trækker 100 rækker ad gangen, men hvad nu hvis vi er efter 10.000 eller 100.000 rækker ad gangen, uanset hvor stor den underliggende tabel er?

Har du en situation, hvor din OLTP-arbejdsbyrde blev forbedret blot ved at tilføje ColumnStore-indekser? Jeg ved godt, at de er designet til datavarehus-lignende arbejdsbelastninger, men hvis du har set fordele andre steder, vil jeg meget gerne høre om dit scenarie og se, om jeg kan inkorporere nogen differentiatorer i min testrig.


  1. Søg efter hele ord match i MySQL

  2. Vælg værdier fra XML-feltet i SQL Server 2008

  3. Sådan opretter du historik ELLER reviderer visninger fra Change Data Capture (CDC) tabeller i SQL Server - SQL Server Tutorial

  4. LocalDB-implementering på klient-pc