Wydajność tabeli w pamięci jest gorsza niż tabeli opartej na dysku

10

Mam tabelę w programie SQL Server 2014, która wygląda następująco:

CREATE TABLE dbo.MyTable
(
[id1] [bigint] NOT NULL,
[id2] [bigint] NOT NULL,
[col1] [int] NOT NULL default(0),
[col2] [int] NOT NULL default(0)
)

gdzie (id1, id2) jest PK. Zasadniczo id1 to identyfikator grupujący zestaw wyników (id2, col1, col2), którego pk to id2.

Próbuję użyć tabeli w pamięci, aby pozbyć się istniejącej tabeli dyskowej, która jest moim wąskim gardłem.

  • Dane w tabeli są zapisywane -> czytane -> usuwane jeden raz.
  • Każda wartość id1 ma kilka (dziesiątki / setki) tysięcy id2.
  • Dane są przechowywane w tabeli przez bardzo krótki czas, np. 20 sekund.

Kwerendy wykonane w tej tabeli są następujące:

-- INSERT (can vary from 10s to 10,000s of records):
INSERT INTO MyTable
  SELECT @fixedValue, id2, col1, col2 FROM AnotherTable

-- READ:
SELECT id2, col1
FROM MyTable INNER JOIN OtherTbl ON MyTable.id2 = OtherTbl.pk
WHERE id1 = @value
ORDER BY col1

-- DELETE:
DELETE FROM MyTable WHERE id1 = @value

Oto obecna definicja, której użyłem dla tabeli:

CREATE TABLE dbo.SearchItems
(
  [id1] [bigint] NOT NULL,
  [id2] [bigint] NOT NULL,
  [col1] [int] NOT NULL default(0),
  [col2] [int] NOT NULL default(0)

  CONSTRAINT PK_Mem PRIMARY KEY NONCLUSTERED (id1,id2),
  INDEX idx_Mem HASH (id1,id2) WITH (BUCKET_COUNT = 131072)
) WITH (MEMORY_OPTIMIZED = ON, DURABILITY = SCHEMA_ONLY)

Niestety definicja ta powoduje pogorszenie wydajności w stosunku do poprzedniej sytuacji z tabelą dyskową. Rząd wielkości jest mniej więcej o 10% wyższy (w niektórych przypadkach osiąga 100%, więc podwójny czas).

Przede wszystkim spodziewałem się przewagi w scenariuszach o wysokiej współbieżności, biorąc pod uwagę architekturę bez blokady reklamowaną przez Microsoft. Zamiast tego najgorsze wyniki występują dokładnie wtedy, gdy kilku równoczesnych użytkowników uruchamia kilka zapytań w tabeli.

Pytania:

  • jaki jest właściwy BUCKET_COUNT do ustawienia?
  • jakiego rodzaju indeksu powinienem użyć?
  • dlaczego wydajność jest gorsza niż w przypadku tabeli opartej na dysku?

Zapytanie sys.dm_db_xtp_hash_index_stats zwraca:

total_bucket_count = 131072
empty_bucket_count = 0
avg_chain_len = 873
max_chain_length = 1009

Zmieniłem liczbę segmentów, więc dane wyjściowe z sys.dm_db_xtp_hash_index_stats to:

total_bucket_count = 134217728
empty_bucket_count = 131664087
avg_chain_len = 1
max_chain_length = 3

Mimo to wyniki są prawie takie same, jeśli nie gorsze.

Cristiano Ghersi
źródło
Czy na pewno nie napotykasz wąchania parametrów? Czy próbowałeś już uruchamiać zapytania OPTION(OPTIMIZE FOR UNKNOWN)(patrz Wskazówki do tabeli )?
TT.
Domyślam się, że masz problemy z łańcuchem wierszy. Czy możesz podać nam wynik select * from sys.dm_db_xtp_hash_index_stats ? Link ten powinien również odpowiedzieć na większość / wszystkie pytania: msdn.microsoft.com/en-us/library/…
Sean Gallardy
4
Indeks skrótu jest użyteczny tylko dla predykatów w obu uwzględnionych kolumnach. Próbowałeś bez indeksu skrótu na stole?
Mikael Eriksson,
Przekonałem się, że najlepszą poprawę wydajności dzięki technologii in-memory można osiągnąć tylko przy użyciu natywnie skompilowanych procedur przechowywanych .
Daniel Hutmacher
@DanielHutmacher FWIW Widziałem kontrprzykłady, w których cała korzyść polegała na usunięciu zatrzasku i dodaniu natywnie skompilowanych procedur dających zerową lub nieznaczną poprawę. Nie sądzę, żeby było miejsce na ogólne oświadczenie (chociaż może masz rację w tym przypadku, nawet nie spojrzałem na szczegóły).
Aaron Bertrand

Odpowiedzi:

7

Chociaż ten post nie będzie kompletną odpowiedzią z powodu braku informacji, powinien być w stanie skierować Cię we właściwym kierunku lub w inny sposób uzyskać wgląd, który możesz później udostępnić społeczności.

Niestety definicja ta powoduje pogorszenie wydajności w stosunku do poprzedniej sytuacji z tabelą dyskową. Rząd wielkości jest mniej więcej o 10% wyższy (w niektórych przypadkach osiąga 100%, więc podwójny czas).

Przede wszystkim spodziewałem się przewagi w scenariuszach o wysokiej współbieżności, biorąc pod uwagę architekturę bez blokady reklamowaną przez Microsoft. Zamiast tego najgorsze wyniki występują dokładnie wtedy, gdy kilku równoczesnych użytkowników uruchamia kilka zapytań w tabeli.

Jest to niepokojące, ponieważ zdecydowanie nie powinno tak być. Niektóre obciążenia nie są w tabelach pamięci (SQL 2014), a niektóre obciążenia się do tego nadają. W większości sytuacji może wystąpić minimalny wzrost wydajności po prostu przez migrację i wybranie odpowiednich indeksów.

Początkowo myślałem bardzo wąsko o twoich pytaniach dotyczących tego:

Pytania:

  • jaki jest właściwy BUCKET_COUNT do ustawienia?
  • jakiego rodzaju indeksu powinienem użyć?
  • dlaczego wydajność jest gorsza niż w przypadku tabeli opartej na dysku?

Początkowo uważałem, że występuje problem z faktyczną tabelą pamięci i indeksami, które nie są optymalne. Chociaż istnieją pewne problemy z definicją indeksu skrótu zoptymalizowaną pod kątem pamięci, uważam, że prawdziwy problem dotyczy wykorzystywanych zapytań.

-- INSERT (can vary from 10s to 10,000s of records):
INSERT INTO MyTable
  SELECT @fixedValue, id2, col1, col2 FROM AnotherTable

Ta wstawka powinna być wyjątkowo szybka, gdyby obejmowała tylko tabelę pamięci. Dotyczy to jednak również tabeli opartej na dysku i podlega wszystkim związanym z tym blokowaniu i blokowaniu. Zatem marnotrawstwo w czasie rzeczywistym dotyczy tabeli opartej na dysku.

Kiedy po załadowaniu danych do pamięci zrobiłem szybki test z wstawieniem 100 000 wierszy z tabeli opartej na dysku - czasy reakcji były poniżej sekundy. Jednak większość danych jest przechowywana tylko przez bardzo krótki czas, mniej niż 20 sekund. To nie daje dużo czasu, aby naprawdę żyć w pamięci podręcznej. Ponadto nie jestem pewien, jak duży AnotherTablejest naprawdę i nie wiem, czy wartości są odczytywane z dysku, czy nie. Na tych odpowiedziach musimy polegać.

Za pomocą zapytania Wybierz:

SELECT id2, col1
FROM MyTable INNER JOIN OtherTbl ON MyTable.id2 = OtherTbl.pk
WHERE id1 = @value
ORDER BY col1

Ponownie jesteśmy na łasce wydajności tabeli opartej na dyskach interop +. Ponadto sortowanie nie jest tanie w indeksach HASH i należy stosować indeks nieklastrowany. Zostało to przywołane w Przewodniku po indeksie, który zamieściłem w komentarzach.

Aby podać pewne fakty oparte na badaniach, załadowałem SearchItemstabelę pamięci do 10 milionów wierszy i AnotherTable100 000, ponieważ nie znałem jej rzeczywistej wielkości ani statystyk. Następnie wykonałem powyższe zapytanie select. Dodatkowo utworzyłem sesję zdarzeń rozszerzonych na wait_completed i umieściłem ją w buforze pierścieniowym. Został wyczyszczony po każdym uruchomieniu. Pobiegłem również, DBCC DROPCLEANBUFFERSaby zasymulować środowisko, w którym wszystkie dane mogą nie znajdować się w pamięci.

Wyniki nie były niczym spektakularnym, gdy patrzy się na nie w próżni. Ponieważ laptop, na którym testuję, korzysta z dysku SSD wyższej jakości, sztucznie obniżyłem wydajność dysku dla używanej maszyny wirtualnej.

Wyniki pojawiły się bez informacji o czekaniu po 5 uruchomieniach zapytania tylko w tabeli opartej na pamięci (usunięcie sprzężenia i brak podpytań). Jest to w zasadzie zgodne z oczekiwaniami.

Jednak przy użyciu oryginalnego zapytania czekałem. W tym przypadku to PAGEIOLATCH_SH ma sens, ponieważ dane są odczytywane z dysku. Ponieważ jestem jedynym użytkownikiem w tym systemie i nie spędziłem czasu na tworzeniu ogromnego środowiska testowego dla wstawek, aktualizacji, usuwania w stosunku do połączonej tabeli, nie spodziewałem się, że nastąpi jakiekolwiek blokowanie lub blokowanie.

W tym przypadku po raz kolejny znaczna część czasu została poświęcona tabeli opartej na dysku.

Na koniec usuń zapytanie. Znalezienie wierszy na podstawie tylko ID1 nie jest wyjątkowo wydajne dzięki indeksowi. Chociaż prawdą jest, że predykaty równości są właściwe dla indeksów mieszających, segment, do którego wpadają dane, jest oparty na całych kolumnach mieszanych. Zatem id1, id2 gdzie id1 = 1, id2 = 2, a id1 = 1, id2 = 3 będzie mieszał różne wiadra, ponieważ hash będzie w poprzek (1,2) i (1,3). Nie będzie to prosty skan zakresu B-Tree, ponieważ indeksy skrótów nie mają takiej samej struktury. Spodziewałbym się wtedy, że nie będzie to idealny wskaźnik dla tej operacji, ale nie spodziewałbym się, że zajmie to rzędy wielkości dłużej niż doświadczenie. Byłbym zainteresowany zobaczeniem wait_info na ten temat.

Przede wszystkim spodziewałem się przewagi w scenariuszach o wysokiej współbieżności, biorąc pod uwagę architekturę bez blokady reklamowaną przez Microsoft. Zamiast tego najgorsze wyniki występują dokładnie wtedy, gdy kilku równoczesnych użytkowników uruchamia kilka zapytań w tabeli.

Chociaż prawdą jest, że zamki są używane dla logicznej spójności, operacje muszą nadal być atomowe. Odbywa się to za pomocą specjalnego operatora porównywania opartego na procesorze (dlatego In-Memory działa tylko z niektórymi [choć prawie wszystkimi procesorami produkowanymi w ciągu ostatnich 4 lat]). Dlatego nie dostajemy wszystkiego za darmo, będzie jeszcze trochę czasu na dokończenie tych operacji.

Kolejną kwestią, o której należy wspomnieć, jest fakt, że w prawie wszystkich zapytaniach wykorzystano interfejs T-SQL (a nie natywnie skompilowane SPROC), które dotykają co najmniej jednej tabeli opartej na dysku. Właśnie dlatego uważam, że ostatecznie nie mamy żadnej zwiększonej wydajności, ponieważ nadal jesteśmy ograniczeni wydajnością tabel opartych na dyskach.

Kontynuacja:

  1. Utwórz rozszerzoną sesję zdarzeń dla wait_completed i podaj znany identyfikator SPID. Uruchom zapytanie i podaj nam dane wyjściowe lub zużyj je wewnętrznie.

  2. Daj nam aktualizację danych wyjściowych z nr 1.

  3. Nie ma magicznej liczby do określenia liczby segmentów dla indeksów skrótów. Zasadniczo, dopóki łyżki nie zostaną całkowicie wypełnione, a łańcuchy rzędów pozostaną poniżej 3 lub 4, wydajność powinna pozostać do zaakceptowania. To trochę jak pytanie: „Na co powinienem ustawić mój plik dziennika?” - będzie zależeć od procesu, bazy danych i typu użycia.

Sean Gallardy
źródło