Database
 sql >> Baza danych >  >> RDS >> Database

Hekaton z niespodzianką:In-memory TVP – część 2

W moim ostatnim poście pokazałem, że przy małych ilościach TVP zoptymalizowany pod kątem pamięci może zapewnić znaczne korzyści w zakresie wydajności typowych wzorców zapytań.

Aby przetestować na nieco większą skalę, wykonałem kopię SalesOrderDetailEnlarged tabeli, którą rozszerzyłem do około 5 000 000 wierszy dzięki temu skryptowi autorstwa Jonathana Kehayiasa (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);

Stworzyłem również trzy wersje tego stołu w pamięci, z których każda ma inną liczbę wiader (łowienie „sweet spot”) – 16 384, 131 072 i 1 048 576. (Możesz użyć zaokrąglonych liczb, ale i tak zostaną one zaokrąglone do następnej potęgi 2.) Przykład:

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

Zauważ, że zmieniłem rozmiar wiadra z poprzedniego przykładu (256). Tworząc tabelę, chcesz wybrać „najlepsze miejsce” dla rozmiaru segmentu — chcesz zoptymalizować indeks skrótu dla wyszukiwania punktów, co oznacza, że ​​chcesz mieć jak najwięcej segmentów z jak najmniejszą liczbą wierszy w każdym segmencie. Oczywiście, jeśli utworzysz ~5 milionów wiader (ponieważ w tym przypadku może to niezbyt dobry przykład, istnieje ~5 milionów unikalnych kombinacji wartości), będziesz miał do czynienia z pewnymi kompromisami związanymi z wykorzystaniem pamięci i wyrzucaniem śmieci. Jeśli jednak spróbujesz upchnąć ~5 milionów unikalnych wartości w 256 segmentach, również napotkasz pewne problemy. W każdym razie ta dyskusja wykracza daleko poza zakres moich testów w tym poście.

Aby przetestować tabelę standardową, wykonałem podobne procedury składowane, jak w poprzednich testach:

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

Więc najpierw przyjrzyjmy się planom dla, powiedzmy, 1000 wierszy wstawianych do zmiennych tabeli, a następnie uruchamianiu procedur:

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;

Tym razem widzimy, że w obu przypadkach optymalizator wybrał wyszukiwanie indeksu klastrowego względem tabeli bazowej i sprzężenie zagnieżdżonych pętli względem TVP. Niektóre wskaźniki kosztów są różne, ale poza tym plany są dość podobne:

Podobne plany dla TVP w pamięci i klasycznej TVP na większą skalę

Porównanie kosztów operatora wyszukiwania – po lewej klasyczny, po prawej w pamięci

Bezwzględna wartość kosztów sprawia, że ​​wydaje się, iż klasyczna TVP byłaby dużo mniej wydajna niż In-Memory TVP. Ale zastanawiałem się, czy byłoby to prawdą w praktyce (zwłaszcza, że ​​szacowana liczba egzekucji po prawej stronie wydawała się podejrzana), więc oczywiście przeprowadziłem kilka testów. Postanowiłem sprawdzić 100, 1000 i 2000 wartości, które mają zostać wysłane do procedury.

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

Wyniki wydajności pokazują, że przy większej liczbie wyszukiwań punktów użycie In-Memory TVP prowadzi do nieco mniejszych zwrotów, za każdym razem nieco wolniej:


Wyniki 10 000 egzekucji przy użyciu programów TVP klasycznych i zapisanych w pamięci

Tak więc, wbrew wrażeniu, jakie można było odnieść z mojego poprzedniego postu, korzystanie z TVP w pamięci niekoniecznie jest korzystne we wszystkich przypadkach.

Wcześniej przyjrzałem się również natywnie skompilowanym procedurom składowanym i tabelom w pamięci, w połączeniu z programami TVP w pamięci. Czy to może tu coś zmienić? Spoiler:absolutnie nie. Stworzyłem trzy takie procedury:

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

Kolejny spoiler:nie byłem w stanie przeprowadzić tych 9 testów z liczbą iteracji 10 000 – trwało to zdecydowanie za długo. Zamiast tego zapętliłem się i uruchomiłem każdą procedurę 10 razy, wykonałem ten zestaw testów 10 razy i wziąłem średnią. Oto wyniki:


Wyniki 10 wykonań przy użyciu programów TVP w pamięci i natywnie skompilowanych procedury

Ogólnie rzecz biorąc, ten eksperyment był raczej rozczarowujący. Patrząc na samą wielkość różnicy, w przypadku tabeli na dysku, średnie wywołanie procedury składowanej zostało wykonane w ciągu średnio 0,0036 sekundy. Jednak gdy wszystko korzystało z technologii w pamięci, średnie wywołanie procedury składowanej trwało 1,1662 sekundy. Ała . Jest bardzo prawdopodobne, że wybrałem właśnie słaby przypadek użycia do ogólnej demonstracji, ale w tamtym czasie wydawało się to intuicyjną „pierwszą próbą”.

Wniosek

Jest o wiele więcej do przetestowania wokół tego scenariusza i mam więcej wpisów na blogu do śledzenia. Nie zidentyfikowałem jeszcze optymalnego przypadku użycia dla TVP in-memory na większą skalę, ale mam nadzieję, że ten post służy jako przypomnienie, że nawet jeśli rozwiązanie wydaje się optymalne w jednym przypadku, nigdy nie można bezpiecznie założyć, że jest ono równie przydatne do różnych scenariuszy. Dokładnie tak należy podchodzić do OLTP w pamięci:jako rozwiązanie z wąskim zestawem przypadków użycia, które bezwzględnie muszą zostać zweryfikowane przed wdrożeniem w środowisku produkcyjnym.


  1. Database
  2.   
  3. Mysql
  4.   
  5. Oracle
  6.   
  7. Sqlserver
  8.   
  9. PostgreSQL
  10.   
  11. Access
  12.   
  13. SQLite
  14.   
  15. MariaDB
  1. Śledź wydajność bazy danych dzięki Uptime Infrastructure Monitor

  2. Jak zamaskować Cassandrę za pomocą IRI FieldShield

  3. Wykrywanie i klasyfikacja danych SQL

  4. Upuść vs Obcinaj w SQL

  5. Co robią wirtualne statystyki plików, a czego nie, informują o opóźnieniach we/wy