sql >> Databasteknik >  >> RDS >> Database

Hekaton med en twist:In-memory TVPs – Del 2

I mitt förra inlägg visade jag att en minnesoptimerad TVP vid små volymer kan ge betydande prestandafördelar för typiska frågemönster.

För att testa i lite högre skala gjorde jag en kopia av SalesOrderDetailEnlarged tabell, som jag hade utökat till ungefär 5 000 000 rader tack vare detta skript av Jonathan Kehayias (blogg | @SQLPoolBoy)).

DROP TABLE dbo.SalesOrderDetailEnlarged;
GO
 
SELECT * INTO dbo.SalesOrderDetailEnlarged 
  FROM AdventureWorks2012.Sales.SalesOrderDetailEnlarged; -- 4,973,997 rows
 
CREATE CLUSTERED INDEX PK_SODE 
  ON dbo.SalesOrderDetailEnlarged(SalesOrderID, SalesOrderDetailID);

Jag skapade också tre in-memory-versioner av denna tabell, var och en med olika bucket count (fiskar efter en "sweet spot") – 16 384, 131 072 och 1 048 576. (Du kan använda rundare tal, men de avrundas uppåt till nästa potens av 2 ändå.) Exempel:

CREATE TABLE [dbo].[SalesOrderDetailEnlarged_InMem_16K] -- and _131K and _1MM
(
	[SalesOrderID] [int] NOT NULL,
	[SalesOrderDetailID] [int] NOT NULL,
	[CarrierTrackingNumber] [nvarchar](25) COLLATE SQL_Latin1_General_CP1_CI_AS NULL,
	[OrderQty] [smallint] NOT NULL,
	[ProductID] [int] NOT NULL,
	[SpecialOfferID] [int] NOT NULL,
	[UnitPrice] [money] NOT NULL,
	[UnitPriceDiscount] [money] NOT NULL,
	[LineTotal] [numeric](38, 6) NOT NULL,
	[rowguid] [uniqueidentifier] NOT NULL,
	[ModifiedDate] [datetime] NOT NULL
 PRIMARY KEY NONCLUSTERED HASH 
 (
	[SalesOrderID],
	[SalesOrderDetailID]
 ) WITH ( BUCKET_COUNT = 16384) -- and 131072 and 1048576
) WITH ( MEMORY_OPTIMIZED = ON , DURABILITY = SCHEMA_AND_DATA );
GO
 
INSERT dbo.SalesOrderDetailEnlarged_InMem_16K
  SELECT * FROM dbo.SalesOrderDetailEnlarged;
 
INSERT dbo.SalesOrderDetailEnlarged_InMem_131K
  SELECT * FROM dbo.SalesOrderDetailEnlarged;
 
INSERT dbo.SalesOrderDetailEnlarged_InMem_1MM
  SELECT * FROM dbo.SalesOrderDetailEnlarged;
GO

Observera att jag ändrade skopstorleken från föregående exempel (256). När du bygger tabellen vill du välja "sweet spot" för hinkstorlek – du vill optimera hashindexet för punktuppslag, vilket innebär att du vill ha så många hinkar som möjligt med så få rader i varje hink som möjligt. Naturligtvis om du skapar ~5 miljoner hinkar (eftersom i det här fallet, kanske inte ett särskilt bra exempel, det finns ~5 miljoner unika kombinationer av värden), kommer du att ha en del minnesanvändning och avvägningar för sophämtning att hantera. Men om du försöker stoppa in ~5 miljoner unika värden i 256 hinkar, kommer du också att få problem. I vilket fall som helst går den här diskussionen långt utanför omfattningen av mina tester för det här inlägget.

För att testa mot standardtabellen gjorde jag liknande lagrade procedurer som i de tidigare testerna:

CREATE PROCEDURE dbo.SODE_InMemory
  @InMemory dbo.InMemoryTVP READONLY
AS
BEGIN
  SET NOCOUNT ON;
 
  DECLARE @tn NVARCHAR(25);
 
  SELECT @tn = CarrierTrackingNumber
    FROM dbo.SalesOrderDetailEnlarged AS sode
    WHERE EXISTS (SELECT 1 FROM @InMemory AS t
    WHERE sode.SalesOrderID = t.Item);
END
GO
 
CREATE PROCEDURE dbo.SODE_Classic
  @Classic dbo.ClassicTVP READONLY
AS
BEGIN
  SET NOCOUNT ON;
 
  DECLARE @tn NVARCHAR(25);
 
  SELECT @tn = CarrierTrackingNumber
    FROM dbo.SalesOrderDetailEnlarged AS sode
    WHERE EXISTS (SELECT 1 FROM @Classic AS t
    WHERE sode.SalesOrderID = t.Item);
END
GO

Så först, för att titta på planerna för, säg, 1 000 rader som infogas i tabellvariablerna, och sedan köra procedurerna:

DECLARE @InMemory dbo.InMemoryTVP;
INSERT @InMemory SELECT TOP (1000) SalesOrderID
  FROM dbo.SalesOrderDetailEnlarged
  GROUP BY SalesOrderID ORDER BY NEWID();
 
DECLARE @Classic dbo.ClassicTVP;
INSERT @Classic SELECT Item FROM @InMemory;
 
EXEC dbo.SODE_Classic  @Classic  = @Classic;
EXEC dbo.SODE_InMemory @InMemory = @InMemory;

Den här gången ser vi att i båda fallen har optimeraren valt en klustrad indexsökning mot bastabellen och en kapslad loop förenas mot TVP. Vissa kostnadsmått är olika, men annars är planerna ganska lika:

Liknande planer för in-memory TVP kontra klassisk TVP i högre skala

Jämföra sökoperatörskostnader – Classic till vänster, In-Memory till höger

Det absoluta värdet av kostnaderna får det att verka som att den klassiska TVP skulle vara mycket mindre effektiv än In-Memory TVP. Men jag undrade om detta skulle vara sant i praktiken (särskilt eftersom siffran för det uppskattade antalet avrättningar till höger verkade misstänkt), så naturligtvis körde jag några tester. Jag bestämde mig för att kontrollera mot 100, 1 000 och 2 000 värden som skulle skickas till proceduren.

DECLARE @values INT = 100; -- 1000, 2000
 
DECLARE @Classic dbo.ClassicTVP;
DECLARE @InMemory dbo.InMemoryTVP;
 
INSERT @Classic(Item) 
SELECT TOP (@values) SalesOrderID
  FROM dbo.SalesOrderDetailEnlarged
  GROUP BY SalesOrderID ORDER BY NEWID();
 
INSERT @InMemory(Item) SELECT Item FROM @Classic;
 
DECLARE @i INT = 1;
 
SELECT SYSDATETIME();
 
WHILE @i <= 10000
BEGIN
  EXEC dbo.SODE_Classic  @Classic  = @Classic;
  SET @i += 1;
END
 
SELECT SYSDATETIME();
 
SET @i = 1;
 
WHILE @i <= 10000
BEGIN
  EXEC dbo.SODE_InMemory @InMemory = @InMemory;
  SET @i += 1;
END
 
SELECT SYSDATETIME();

Prestandaresultaten visar att vid ett större antal punktuppslagningar leder användning av en In-Memory TVP till något minskande avkastning, och blir något långsammare varje gång:


Resultat av 10 000 avrättningar med klassiska TVP och in-memory TVP em>

Så, i motsats till det intryck du kan ha fått från mitt tidigare inlägg, är det inte nödvändigtvis fördelaktigt att använda en TVP i minnet i alla fall.

Tidigare har jag också tittat på inbyggt kompilerade lagrade procedurer och tabeller i minnet, i kombination med minnes-TVP. Kan detta göra skillnad här? Spoiler:absolut inte. Jag skapade tre procedurer så här:

CREATE PROCEDURE [dbo].[SODE_Native_InMem_16K] -- and _131K and _1MM
  @InMemory dbo.InMemoryTVP READONLY
WITH NATIVE_COMPILATION, SCHEMABINDING, EXECUTE AS OWNER 
AS 
  BEGIN ATOMIC WITH (TRANSACTION ISOLATION LEVEL = SNAPSHOT, LANGUAGE = N'us_english');
 
  DECLARE @tn NVARCHAR(25);
 
  SELECT @tn = CarrierTrackingNumber
    FROM dbo.SalesOrderDetailEnlarged_InMem_16K AS sode -- and _131K and _1MM
    INNER JOIN @InMemory AS t -- no EXISTS allowed here
    ON sode.SalesOrderID = t.Item;
END
GO

En annan spoiler:Jag kunde inte köra dessa 9 tester med ett iterationsantal på 10 000 – det tog alldeles för lång tid. Istället gick jag igenom och körde varje procedur 10 gånger, körde den uppsättningen av tester 10 gånger och tog genomsnittet. Här är resultaten:


Resultat av 10 körningar med TVP:er i minnet och inbyggt kompilerade lagrade procedurer

Sammantaget var detta experiment ganska nedslående. Bara att titta på hur stor skillnaden är, med en tabell på disken, slutfördes det genomsnittliga lagrade proceduranropet på i genomsnitt 0,0036 sekunder. Men när allt använde in-memory-teknik var det genomsnittliga lagrade proceduranropet 1,1662 sekunder. Aj . Det är högst troligt att jag precis har valt ett dåligt användningsfall för att demonstrera överlag, men det verkade då vara ett intuitivt "första försök."

Slutsats

Det finns mycket mer att testa kring detta scenario, och jag har fler blogginlägg att följa. Jag har ännu inte identifierat det optimala användningsfallet för TVP:er i minnet i större skala, men hoppas att det här inlägget fungerar som en påminnelse om att även om en lösning verkar optimal i ett fall, är det aldrig säkert att anta att den är lika tillämplig till olika scenarier. Det är precis så här In-Memory OLTP bör närma sig:som en lösning med en smal uppsättning användningsfall som absolut måste valideras innan de implementeras i produktionen.


  1. Android anpassad kalender och påminnelse

  2. Hur man uppdaterar befintliga data med SQLite

  3. Oracle-skärm mer än 24 timmar

  4. 12c Adaptiva planer i SQL-utvecklare