Patrzyłem na artykuł tutaj Tabele tymczasowe vs. zmienne tabel i ich wpływ na wydajność programu SQL Server, a na serwerze SQL Server 2008 udało się odtworzyć wyniki podobne do pokazanych tam w 2005 roku.
Podczas wykonywania procedur przechowywanych (definicje poniżej) z tylko 10 wierszami wersja zmiennej tabeli wykonuje wersję tabeli tymczasowej ponad dwa razy.
Wyczyściłem pamięć podręczną procedur i uruchomiłem obie procedury przechowywane 10 000 razy, a następnie powtórzyłem proces dla kolejnych 4 uruchomień. Wyniki poniżej (czas w ms na partię)
T2_Time V2_Time
----------- -----------
8578 2718
6641 2781
6469 2813
6766 2797
6156 2719
Moje pytanie brzmi: jaka jest przyczyna lepszej wydajności wersji zmiennej tabeli?
Przeprowadziłem dochodzenie. np. Patrząc na liczniki wydajności za pomocą
SELECT cntr_value
from sys.dm_os_performance_counters
where counter_name = 'Temp Tables Creation Rate';
potwierdza, że w obu przypadkach obiekty tymczasowe są buforowane po pierwszym uruchomieniu zgodnie z oczekiwaniami, a nie tworzone od nowa dla każdego wywołania.
Podobnie śledzenie Auto Stats
, SP:Recompile
, SQL:StmtRecompile
wydarzenia w Profiler (zrzut ekranu poniżej) pokazuje, że wydarzenia te występują tylko raz (na pierwszym wezwaniem #temp
tabeli procedury przechowywanej), a pozostałe 9,999 egzekucje nie budzą żadnej z tych wydarzeń. (Wersja zmiennej tabeli nie otrzymuje żadnego z tych zdarzeń)
Nieco większy narzut związany z pierwszym uruchomieniem procedury przechowywanej nie może w żaden sposób uwzględniać dużej ogólnej różnicy, ponieważ nadal zajmuje tylko kilka ms, aby wyczyścić pamięć podręczną procedur i uruchomić obie procedury raz, więc nie wierzę w statystyki ani rekompilacje mogą być przyczyną.
Utwórz wymagane obiekty bazy danych
CREATE DATABASE TESTDB_18Feb2012;
GO
USE TESTDB_18Feb2012;
CREATE TABLE NUM
(
n INT PRIMARY KEY,
s VARCHAR(128)
);
WITH NUMS(N)
AS (SELECT TOP 1000000 ROW_NUMBER() OVER (ORDER BY $/0)
FROM master..spt_values v1,
master..spt_values v2)
INSERT INTO NUM
SELECT N,
'Value: ' + CONVERT(VARCHAR, N)
FROM NUMS
GO
CREATE PROCEDURE [dbo].[T2] @total INT
AS
CREATE TABLE #T
(
n INT PRIMARY KEY,
s VARCHAR(128)
)
INSERT INTO #T
SELECT n,
s
FROM NUM
WHERE n%100 > 0
AND n <= @total
DECLARE @res VARCHAR(128)
SELECT @res = MAX(s)
FROM NUM
WHERE n <= @total
AND NOT EXISTS(SELECT *
FROM #T
WHERE #T.n = NUM.n)
GO
CREATE PROCEDURE [dbo].[V2] @total INT
AS
DECLARE @V TABLE (
n INT PRIMARY KEY,
s VARCHAR(128))
INSERT INTO @V
SELECT n,
s
FROM NUM
WHERE n%100 > 0
AND n <= @total
DECLARE @res VARCHAR(128)
SELECT @res = MAX(s)
FROM NUM
WHERE n <= @total
AND NOT EXISTS(SELECT *
FROM @V V
WHERE V.n = NUM.n)
GO
Skrypt testowy
SET NOCOUNT ON;
DECLARE @T1 DATETIME2,
@T2 DATETIME2,
@T3 DATETIME2,
@Counter INT = 0
SET @T1 = SYSDATETIME()
WHILE ( @Counter < 10000)
BEGIN
EXEC dbo.T2 10
SET @Counter += 1
END
SET @T2 = SYSDATETIME()
SET @Counter = 0
WHILE ( @Counter < 10000)
BEGIN
EXEC dbo.V2 10
SET @Counter += 1
END
SET @T3 = SYSDATETIME()
SELECT DATEDIFF(MILLISECOND,@T1,@T2) AS T2_Time,
DATEDIFF(MILLISECOND,@T2,@T3) AS V2_Time
źródło
#temp
tabeli tylko raz, mimo że zostały wyczyszczone i ponownie wypełnione kolejne 9 999 razy po tym.Odpowiedzi:
Wyjście
SET STATISTICS IO ON
dla obu wygląda podobnieDaje
I jak Aaron zwraca uwagę w komentarzach plan dla wersji zmiennej tabela jest rzeczywiście mniej skuteczny jak podczas gdy oba mają zagnieżdżone pętle PLAN napędzany przez indeks szukać na
dbo.NUM
tych#temp
preform Wersja przedstawić dążą do indeksu na[#T].n = [dbo].[NUM].[n]
z pozostałego orzecznika[#T].[n]<=[@total]
natomiast zmiennej tabeli wersja wykonuje wyszukiwanie indeksu@V.n <= [@total]
z pozostałym predykatem@V.[n]=[dbo].[NUM].[n]
i przetwarza więcej wierszy (dlatego ten plan działa tak słabo dla większej liczby wierszy)Użycie Rozszerzonych zdarzeń do sprawdzenia typów oczekiwania dla określonego pająka daje te wyniki dla 10.000 wykonań
EXEC dbo.T2 10
i te wyniki dla 10.000 egzekucji
EXEC dbo.V2 10
Jest więc jasne, że liczba
PAGELATCH_SH
oczekujących jest znacznie wyższa w#temp
przypadku tabeli. Nie znam żadnego sposobu dodania zasobu oczekiwania do śledzenia zdarzeń rozszerzonych, więc aby to zbadać, uruchomiłemPodczas gdy w innym połączeniu odpytywanie
sys.dm_os_waiting_tasks
Po pozostawieniu go na około 15 sekund uzyskał następujące wyniki
Obie zatrzaśnięte strony należą do (różnych) nieklastrowanych indeksów w
tempdb.sys.sysschobjs
tabeli podstawowej o nazwach'nc1'
i'nc2'
.Zapytanie
tempdb.sys.fn_dblog
podczas przebiegów wskazuje, że liczba rekordów dziennika dodanych przy pierwszym wykonaniu każdej procedury składowanej była nieco zmienna, ale dla kolejnych wykonań liczba dodana przy każdej iteracji była bardzo spójna i przewidywalna. Po buforowaniu planów procedur liczba wpisów w dzienniku jest o połowę mniejsza niż w przypadku#temp
wersji.Patrząc na wpisy dziennika transakcji bardziej szczegółowo dla
#temp
wersji tabelowej SP każde kolejne wywołanie procedury składowanej tworzy trzy transakcje, a zmienna tabelowa tylko dwie.W
INSERT
/TVQUERY
transakcje są identyczne z wyjątkiem nazwy. Zawiera rekordy dziennika dla każdego z 10 wierszy wstawionych do tabeli tymczasowej lub zmiennej tabeli oraz wpisyLOP_BEGIN_XACT
/LOP_COMMIT_XACT
.CREATE TABLE
Transakcja pojawia się tylko w#Temp
wersji i wygląda następująco.FCheckAndCleanupCachedTempTable
Transakcja pojawia się w obu, ale ma 6 dodatkowych wpisów w#temp
wersji. Są to 6 wierszy, które odnoszą sięsys.sysschobjs
i mają dokładnie taki sam wzór jak powyżej.Patrząc na te 6 wierszy w obu transakcjach, odpowiadają one tym samym operacjom. Pierwsza
LOP_MODIFY_ROW, LCX_CLUSTERED
to aktualizacjamodify_date
kolumny wsys.objects
. Pozostałe pięć wierszy dotyczy zmiany nazwy obiektu. Ponieważname
jest to kluczowa kolumna obu dotkniętych NCI (nc1
inc2
), jest to wykonywane jako usuwanie / wstawianie dla tych, a następnie wraca do indeksu klastrowego i również to aktualizuje.Wydaje się, że w przypadku
#temp
wersji tabelowej, gdy procedura przechowywana kończy część czyszczenia przeprowadzonego przezFCheckAndCleanupCachedTempTable
transakcję, jest zmiana nazwy tabeli tymczasowej z czegoś podobnego#T__________________________________________________________________________________________________________________00000000E316
na inną nazwę wewnętrzną, taką jak#2F4A0079
i kiedy jest wprowadzana,CREATE TABLE
transakcja zmienia jej nazwę z powrotem. Ta nazwa flip-flop może być widoczna w jednym połączeniu wykonującym siędbo.T2
w pętli, podczas gdy w innymPrzykładowe wyniki
Jednym z potencjalnych wyjaśnień obserwowanej różnicy wydajności, na które powołał się Alex, jest to, że odpowiedzialna jest ta dodatkowa praca
tempdb
polegająca na utrzymaniu tabel systemowych .Po uruchomieniu obu procedur w pętli profiler programu Visual Studio Code ujawnia następujące informacje
Wersja zmiennej tabeli spędza około 60% czasu na wykonywaniu instrukcji insert i późniejszego wyboru, podczas gdy tabela tymczasowa jest mniejsza o połowę. Jest to zgodne z harmonogramami pokazanymi w PO i powyższym wnioskiem, że różnica w wydajności zależy od czasu poświęconego na wykonywanie pracy pomocniczej, a nie z powodu czasu poświęconego na wykonanie samego zapytania.
Najważniejszymi funkcjami przyczyniającymi się do „braku” 75% w tymczasowej wersji tabeli są
W obu funkcjach tworzenia i zwalniania
CMEDProxyObject::SetName
pokazana jest funkcja z włączającą wartością próbki19.6%
. Z czego wywnioskuję, że 39,2% czasu w przypadku tabeli tymczasowej zajmowane jest przez zmianę nazwy opisaną wcześniej.Największe w wersji zmiennej stołowej, które przyczyniają się do pozostałych 40%, to
Profil stołu tymczasowego
Profil zmiennej tabeli
źródło
Dyskoteka inferno
Ponieważ jest to starsze pytanie, postanowiłem ponownie sprawdzić problem w nowszych wersjach programu SQL Server, aby sprawdzić, czy ten sam profil wydajności nadal istnieje lub czy w ogóle zmieniły się właściwości.
W szczególności dodanie tabel systemowych w pamięci dla SQL Server 2019 wydaje się być dobrą okazją do ponownego przetestowania.
Używam nieco innej uprzęży testowej, ponieważ napotkałem ten problem podczas pracy nad czymś innym.
Testowanie, testowanie
Korzystając z wersji Stack Overflow 2013 , mam ten indeks i te dwie procedury:
Indeks:
Tabela temperatur:
Zmienna tabeli:
Aby zapobiec potencjalnym oczekiwaniom ASYNC_NETWORK_IO , używam procedur otoki.
SQL Server 2017
Ponieważ 2014 i 2016 są w zasadzie RELIKA w tym momencie, zaczynam testowanie od 2017 roku. Ponadto, dla zwięzłości, skaczę od razu do profilowania kodu za pomocą Perfview . W prawdziwym życiu patrzyłem na oczekiwania, zatrzaski, spinlocki, szalone flagi ze śladami i inne rzeczy.
Profilowanie kodu jest jedyną rzeczą, która ujawniła coś interesującego.
Różnica czasu:
Wciąż bardzo wyraźna różnica, co? Ale co teraz uderza w SQL Server?
Patrząc na dwa najwyższe wzrosty w różnych próbkach, widzimy
sqlmin
isqlsqllang!TCacheStore<CacheClockAlgorithm>::GetNextUserDataInHashBucket
jesteśmy dwoma największymi przestępcami.Sądząc po nazwach w stosach wywołań, czyszczenie i zmiana nazw wewnętrznych tabel tymczasowych wydaje się być największym czasem do wyczerpania wywołania tabeli tymczasowej w porównaniu do wywołania zmiennej tabelowej.
Mimo że zmienne tabel są wewnętrznie wspierane przez tabele tymczasowe, nie wydaje się to problemem.
Przeglądanie stosów połączeń dla testu zmiennej tabeli w ogóle nie pokazuje żadnego z głównych przestępców:
SQL Server 2019 (wanilia)
W porządku, więc to nadal jest problem w SQL Server 2017, czy coś nowego w 2019 roku jest gotowe?
Po pierwsze, aby pokazać, że nie ma nic w rękawie:
Różnica czasu:
Obie procedury były różne. Wywołanie tabeli temp było o kilka sekund szybsze, a wywołanie zmiennej tabeli było o około 1,5 sekundy wolniejsze. Spowolnienie zmiennej stołowej można częściowo wyjaśnić kompilacją odroczoną zmiennej stołowej , nowym wyborem optymalizatora w 2019 r.
Patrząc na różnicę w Perfview, to się nieco zmieniło - nie ma już sqlmin - ale
sqllang!TCacheStore<CacheClockAlgorithm>::GetNextUserDataInHashBucket
jest.SQL Server 2019 (tabele systemowe Tempdb w pamięci)
Co z tą nową rzeczą w tabeli systemowej pamięci? Hm? Sup z tym?
Włączmy to!
Zauważ, że wymaga to ponownego uruchomienia SQL Server, aby się uruchomić, więc wybacz mi, gdy ponownie uruchamiam SQL w to piękne piątek po południu.
Teraz sprawy wyglądają inaczej:
Różnica czasu:
Tabele temp były o około 4 sekundy lepsze! To jest coś.
Coś mi się podoba
Tym razem diff Perfview nie jest zbyt interesujący. Obok siebie warto zauważyć, jak blisko są czasy na planszy:
Ciekawym punktem w diff są wywołania
hkengine!
, które mogą wydawać się oczywiste, ponieważ funkcje hekaton-ish są teraz w użyciu.Jeśli chodzi o dwa pierwsze elementy w diff, nie mogę zrobić wiele z
ntoskrnl!?
:Lub
sqltses!CSqlSortManager_80::GetSortKey
, ale są tutaj, aby Smrtr Ppl ™ mógł zobaczyć:Zauważ, że istnieje nieudokumentowana i zdecydowanie nie bezpieczna dla produkcji, więc nie używaj jej flagi śledzenia uruchamiania, której możesz użyć, aby mieć dodatkowe obiekty systemowe tabeli temp (sysrowsets, sysallocunits i sysseobjvalues) zawarte w funkcji w pamięci, ale to w tym przypadku nie zrobiło zauważalnej różnicy w czasach wykonania.
Łapanka
Nawet w nowszych wersjach serwera SQL wywołania wysokiej częstotliwości do zmiennych tabeli są znacznie szybsze niż wywołania wysokiej częstotliwości do tabel temp.
Chociaż kusi, by obwiniać kompilacje, rekompilacje, statystyki automatyczne, zatrzaski, blokady, buforowanie lub inne problemy, problem nadal dotyczy zarządzania czyszczeniem tabeli temp.
Jest to bliższe wywołanie w SQL Server 2019 z włączonymi tabelami systemowymi w pamięci, ale zmienne tabel nadal działają lepiej, gdy częstotliwość połączeń jest wysoka.
Oczywiście jako mędrzec vaping po przemyśleniu: „używaj zmiennych tabeli, gdy wybór planu nie jest problemem”.
źródło