sql >> Database >  >> RDS >> Database

Hekaton met een twist:In-memory TVP's – Deel 2

In mijn laatste bericht heb ik aangetoond dat een voor geheugen geoptimaliseerd TVP bij kleine volumes aanzienlijke prestatievoordelen kan opleveren voor typische querypatronen.

Om op iets hogere schaal te testen, heb ik een kopie gemaakt van de SalesOrderDetailEnlarged tabel, die ik had uitgebreid tot ongeveer 5.000.000 rijen dankzij dit script van Jonathan Kehayias (blog | @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);

Ik heb ook drie in-memory versies van deze tabel gemaakt, elk met een ander aantal emmers (vissen naar een "sweet spot") - 16.384, 131.072 en 1.048.576. (Je kunt rondere getallen gebruiken, maar ze worden toch afgerond naar de volgende macht van 2.) Voorbeeld:

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

Merk op dat ik de maat van de emmer heb gewijzigd ten opzichte van het vorige voorbeeld (256). Bij het samenstellen van de tabel wilt u de "sweet spot" voor de grootte van de bucket kiezen - u wilt de hash-index optimaliseren voor het opzoeken van punten, wat betekent dat u zoveel mogelijk buckets wilt met zo min mogelijk rijen in elke bucket. Als u ~5 miljoen buckets maakt (aangezien in dit geval, misschien niet zo'n goed voorbeeld, er ~5 miljoen unieke combinaties van waarden zijn), zult u natuurlijk wat afwegingen moeten maken tussen geheugengebruik en garbagecollection. Als u echter ~ 5 miljoen unieke waarden in 256 buckets probeert te proppen, zult u ook enkele problemen ondervinden. In ieder geval gaat deze discussie veel verder dan mijn tests voor dit bericht.

Om te testen met de standaardtabel, heb ik soortgelijke opgeslagen procedures gemaakt als in de vorige tests:

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

Dus eerst kijken naar de plannen voor bijvoorbeeld 1.000 rijen die in de tabelvariabelen worden ingevoegd en vervolgens de procedures uitvoeren:

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;

Deze keer zien we dat in beide gevallen de optimizer heeft gekozen voor een geclusterde index-zoekopdracht tegen de basistabel en een geneste lus-join tegen de TVP. Sommige kostenstatistieken zijn anders, maar verder zijn de plannen vrij gelijkaardig:

Vergelijkbare plannen voor in-memory TVP versus klassieke TVP op grotere schaal

Zoek operatorkosten vergelijken – Klassiek aan de linkerkant, In-Memory aan de rechterkant

Door de absolute waarde van de kosten lijkt het alsof de klassieke TVP een stuk minder efficiënt zou zijn dan de In-Memory TVP. Maar ik vroeg me af of dit in de praktijk waar zou zijn (vooral omdat het geschatte aantal executies aan de rechterkant verdacht leek), dus deed ik natuurlijk wat tests. Ik besloot te controleren op 100, 1.000 en 2.000 waarden die naar de procedure moesten worden gestuurd.

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();

De prestatieresultaten laten zien dat, bij grotere aantallen point lookups, het gebruik van een In-Memory TVP leidt tot een licht afnemend rendement, dat elke keer iets langzamer is:


Resultaten van 10.000 uitvoeringen met klassieke en in-memory TVP's

Dus, in tegenstelling tot de indruk die je uit mijn vorige bericht hebt gekregen, is het gebruik van een in-memory TVP niet in alle gevallen noodzakelijk voordelig.

Eerder keek ik ook naar native gecompileerde opgeslagen procedures en in-memory tabellen, in combinatie met in-memory TVP's. Zou dit hier het verschil kunnen maken? Spoiler:absoluut niet. Ik heb drie procedures als volgt gemaakt:

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

Nog een spoiler:ik kon deze 9 tests niet uitvoeren met een iteratietelling van 10.000 - het duurde veel te lang. In plaats daarvan heb ik elke procedure 10 keer doorlopen en uitgevoerd, die reeks tests 10 keer uitgevoerd en het gemiddelde genomen. Dit zijn de resultaten:


Resultaten van 10 uitvoeringen met in-memory TVP's en native gecompileerde opgeslagen procedures

Over het algemeen was dit experiment nogal teleurstellend. Alleen al kijkend naar de enorme omvang van het verschil, met een tabel op de schijf, werd de gemiddelde opgeslagen procedure-aanroep voltooid in gemiddeld 0,0036 seconden. Toen alles echter in-memory-technologieën gebruikte, was de gemiddelde opgeslagen procedure-aanroep 1,1662 seconden. Auw . Het is zeer waarschijnlijk dat ik zojuist een slechte use-case heb gekozen om in het algemeen te demonstreren, maar het leek op dat moment een intuïtieve "eerste poging".

Conclusie

Er valt nog veel meer te testen rond dit scenario en ik heb nog meer blogposts te volgen. Ik heb nog niet de optimale use-case voor in-memory TVP's op grotere schaal geïdentificeerd, maar ik hoop dat dit bericht een herinnering is dat, hoewel een oplossing in één geval optimaal lijkt, het nooit veilig is om aan te nemen dat deze even toepasbaar is naar verschillende scenario's. Dit is precies hoe In-Memory OLTP moet worden benaderd:als een oplossing met een beperkt aantal use-cases die absoluut moeten worden gevalideerd voordat ze in productie worden geïmplementeerd.


  1. Laat je niet voor de gek houden door de streams pool

  2. Hoe kan ik de oorzaak van de Hibernate-uitzondering verhelpen. IllegalArgumentException is opgetreden tijdens het aanroepen van setter?

  3. Problemen bij het openen van een MDF-bestand omdat er SQL-fout 5171 staat? - Een gastpost van Andre Williams

  4. Hoe u het huidige Auto_Increment-volgnummer voor MySQL / MariaDB-tabel kunt krijgen