W tym artykule poruszymy temat wydajności zmiennych tabelowych. W SQL Server możemy tworzyć zmienne, które będą działać jako kompletne tabele. Być może inne bazy danych mają takie same możliwości, jednak ja takich zmiennych użyłem tylko w MS SQL Serwerze.
W ten sposób możesz napisać:
declare @t as table (int value)
Tutaj deklarujemy zmienną @t jako tabelę, która będzie zawierać pojedynczą kolumnę Value typu Integer. Możliwe jest tworzenie bardziej złożonych tabel, jednak w naszym przykładzie jedna kolumna wystarczy do zbadania optymalizacji.
Teraz możemy użyć tej zmiennej w naszych zapytaniach. Możemy dodać do niego dużo danych i pobrać dane z tej zmiennej:
insert into @t select UserID from User or select * from @t
Zauważyłem, że zmienne tabeli są używane, gdy trzeba pobrać dane dla dużego wyboru. Na przykład w kodzie znajduje się zapytanie, które zwraca użytkowników witryny. Teraz zbierasz identyfikatory wszystkich użytkowników, dodajesz je do zmiennej tabeli i możesz wyszukiwać adresy tych użytkowników. Być może ktoś zapyta, dlaczego nie wykonujemy jednego zapytania na bazie danych i od razu wszystko dostajemy? Mam prosty przykład.
Załóżmy, że użytkownicy pochodzą z serwisu WWW, a ich adresy są przechowywane w Twojej bazie danych. W takim przypadku nie ma wyjścia. Otrzymaliśmy z usługi kilka identyfikatorów użytkowników i aby uniknąć wysyłania zapytań do bazy danych, ktoś decyduje, że łatwiej jest dodać wszystkie identyfikatory do parametru zapytania jako zmienną tabeli, a zapytanie będzie wyglądało ładnie:
select * from @t as users join Address a on a.UserID = users.UserID os
Wszystko to działa poprawnie. W kodzie C# możesz szybko połączyć wyniki obu tablic danych w jeden obiekt za pomocą LINQ. Jednak wydajność zapytania może ucierpieć.
Faktem jest, że zmienne tabeli nie zostały zaprojektowane do przetwarzania dużych ilości danych. Jeśli się nie mylę, optymalizator zapytań będzie zawsze używał metody wykonania LOOP. W związku z tym dla każdego identyfikatora z @t nastąpi wyszukiwanie w tabeli adresów. Jeśli w @t jest 1000 rekordów, serwer przeskanuje adres 1000 razy.
Pod względem wykonania, ze względu na szaloną liczbę skanowań, serwer po prostu przestaje próbować znaleźć dane.
O wiele bardziej efektywne jest przeskanowanie całej tabeli adresów i jednoczesne znalezienie wszystkich użytkowników. Ta metoda nazywa się MERGE. Jednak SQL Server wybiera go, gdy jest dużo posortowanych danych. W tym przypadku optymalizator nie wie ile i jakie dane zostaną dodane do zmiennej i czy jest sortowanie, bo taka zmienna nie zawiera indeksów.
Jeśli w zmiennej tabeli jest mało danych i nie wstawiasz do niej tysięcy wierszy, wszystko jest w porządku. Jeśli jednak chcesz używać takich zmiennych i dodawać do nich ogromne ilości danych, musisz kontynuować czytanie.
Nawet jeśli zamienisz zmienną tabeli na SQL, znacznie przyspieszy to wykonanie zapytania:
select * from ( Select 10377 as UserID Union all Select 73736 Union all Select 7474748 …. ) as users join Address a on a.UserID = users.UserID
Takich instrukcji SELECT może być tysiąc, a tekst zapytania będzie ogromny, ale zostanie wykonany tysiące razy szybciej w przypadku dużej ilości danych, ponieważ SQL Server może wybrać efektywny plan wykonania.
To zapytanie nie wygląda świetnie. Jednak jego plan wykonania nie może być zbuforowany, ponieważ zmiana tylko jednego identyfikatora zmieni również cały tekst zapytania i nie będzie można użyć parametrów.
Myślę, że Microsoft nie spodziewał się, że użytkownicy będą używać zmiennych tabelarycznych w ten sposób, ale istnieje dobre obejście.
Istnieje kilka sposobów rozwiązania tego problemu. Jednak moim zdaniem najskuteczniejsze pod względem wydajności jest dodanie OPCJI (RECOMPILE) na końcu zapytania:
select * from @t as users join Address a on a.UserID = users.UserID OPTION (RECOMPILE)
Ta opcja jest dodawana raz na samym końcu zapytania, nawet po ORDER BY. Celem tej opcji jest ponowne skompilowanie zapytania SQL Server przy każdym wykonaniu.
Jeśli później zmierzymy wydajność zapytania, najprawdopodobniej skróci się czas na wykonanie wyszukiwania. Przy dużych ilościach danych poprawa wydajności może być znacząca, od kilkudziesięciu minut do sekund. Teraz serwer kompiluje swój kod przed uruchomieniem każdego zapytania i nie korzysta z planu wykonania z pamięci podręcznej, ale generuje nowy, w zależności od ilości danych w zmiennej, a to zwykle bardzo pomaga.
Wadą jest to, że plan wykonania nie jest przechowywany i serwer musi każdorazowo kompilować zapytanie i szukać efektywnego planu wykonania. Jednak nie widziałem zapytań, w których proces ten trwał dłużej niż 100 ms.
Czy używanie zmiennych tabeli to zły pomysł? Nie, nie jest. Pamiętaj tylko, że nie zostały stworzone z myślą o dużych ilościach danych. Czasami lepiej jest utworzyć tabelę tymczasową, jeśli jest dużo danych, i wstawić dane do tej tabeli, a nawet utworzyć indeks w locie. Musiałem to zrobić z raportami, ale tylko raz. Wtedy skróciłem czas generowania jednego raportu z 3 godzin do 20 minut.
Wolę używać jednego dużego zapytania zamiast dzielić go na kilka zapytań i przechowywać wyniki w zmiennych. Pozwól, aby SQL Server dostroił wydajność dużego zapytania i nie zawiedzie Cię. Pamiętaj, że powinieneś odwoływać się do zmiennych tabeli tylko w skrajnych przypadkach, gdy naprawdę dostrzegasz ich zalety.