Wybór między zmiennymi tabeli a tabelami tymczasowymi (ST011, ST012)

Ludzie mogą i robią wiele argumentów na temat względnych zalet zmiennych tabel i tabel tymczasowych. Czasami, tak jak podczas pisania funkcji, nie masz wyboru; ale kiedy już to zrobisz, okaże się, że oba mają swoje zastosowania i łatwo jest znaleźć przykłady, w których jedno jest szybsze. W tym artykule wyjaśnię główne czynniki związane z wyborem jednego lub drugiego oraz przedstawię kilka prostych „zasad”, aby uzyskać najlepszą wydajność.

Zakładając, że przestrzegasz podstawowych zasad zaangażowania , wtedy należy rozważyć zmienne tabeli jako pierwszy wybór podczas pracy ze stosunkowo małymi zestawami danych. Są łatwiejsze w użyciu i powodują mniejszą liczbę ponownych kompilacji w procedurach, w których są używane, w porównaniu do korzystania z tabel tymczasowych. Zmienne tabel również wymagają mniej zasobów blokujących, ponieważ są „prywatne” dla procesu i partii, która je utworzyła. SQL Prompt implementuje tę rekomendację jako regułę analizy kodu, ST011 – Rozważ użycie zmiennej tabeli zamiast tabeli tymczasowej.

Jeśli wykonujesz bardziej złożone przetwarzanie danych tymczasowych lub potrzebujesz więcej niż rozsądnie małych ilości danych w nich, wtedy prawdopodobnie lepszym wyborem będą lokalne tabele tymczasowe. SQL Code Guard zawiera regułę analizy kodu, opartą na jego zaleceniach, ST012 – Rozważ użycie tabeli tymczasowej zamiast zmiennej tabeli, ale obecnie nie jest ona zaimplementowana w SQL Prompt.

Zalety i wady zmiennych tabel i tabel tymczasowych

Zmienne tabeli mają tendencję do uzyskiwania „złej prasy”, ponieważ zapytania, które ich używają, czasami skutkują bardzo nieefektywnymi planami wykonania. Jeśli jednak zastosujesz się do kilku prostych reguł, są one dobrym wyborem dla pośrednich tabel „roboczych” oraz do przekazywania wyników między procedurami, w których zbiory danych są małe, a wymagane przetwarzanie jest stosunkowo proste.

Zmienne tabeli są bardzo proste w użyciu, głównie dlatego, że nie wymagają konserwacji. Są ograniczone do partii lub procedury, w której zostały utworzone, i są automatycznie usuwane po zakończeniu wykonywania, a więc używanie ich w ramach długotrwałego połączenia nie ryzykuje problemów związanych z „uciążaniem zasobów” w tempdb. Jeśli zmienna tabeli jest zadeklarowana w procedurze składowanej, jest lokalna dla tej procedury składowanej i nie można się do niej odwoływać w procedurze zagnieżdżonej. nie możesz ALTER jednego, więc procedury, które ich używają, zwykle powodują mniejszą liczbę ponownych kompilacji niż te, które używają tabel tymczasowych. Nie są też w pełni rejestrowane, więc ich tworzenie i wypełnianie jest wymaga mniej miejsca w t dziennik okupu. Gdy są używane w procedurach składowanych, rywalizacja o tabele systemowe jest mniejsza w warunkach wysokiej współbieżności. Krótko mówiąc, łatwiej jest utrzymać porządek i porządek.

Podczas pracy ze stosunkowo małymi zestawami danych są one szybsze niż porównywalna tabela tymczasowa. Jednak wraz ze wzrostem liczby wierszy, przekraczających około 15 tys. Wierszy, ale różniących się w zależności od kontekstu, mogą wystąpić trudności, głównie z powodu braku obsługi statystyk. Nawet indeksy, które narzucają PRIMARY KEY i UNIQUE ograniczenia dla zmiennych tabeli, nie mają statystyk . Dlatego optymalizator użyje zakodowanego na stałe oszacowania 1 wiersza zwróconego ze zmiennej tabeli, a więc będzie miał tendencję do wybierania operatorów optymalnych do pracy z małymi zestawami danych (takimi jak operator zagnieżdżonych pętli dla złączeń). Im więcej wierszy w zmiennej tabeli, tym większe rozbieżności między oszacowaniem a rzeczywistością i tym mniej efektywne stają się wybory planu optymalizatora. Wynikowy plan jest czasem przerażający.

Doświadczony programista lub administrator będzie szukał tego rodzaju problemu i będzie gotowy do dodania OPTION (RECOMPILE) podpowiedź zapytania do instrukcji używającej zmiennej tabeli. Kiedy przesyłamy pakiet zawierający zmienną tabelaryczną, optymalizator najpierw kompiluje wsad, w którym to momencie zmienna tabeli jest pusta. Gdy partia zacznie się wykonywać, wskazówka spowoduje rekompilację tylko tej pojedynczej instrukcji, w którym to momencie zmienna tabeli zostanie zapełniona, a optymalizator może użyć rzeczywistej liczby wierszy do skompilowania nowego planu dla tej instrukcji. Czasami, ale rzadko, nawet to nie pomaga. Ponadto nadmierne poleganie na tej wskazówce zniweczy w pewnym stopniu zaletę, jaką mają zmienne tabelowe, ponieważ powoduje mniej rekompilacji niż tabele tymczasowe.

Po drugie, pewne ograniczenia indeksu dotyczące zmiennych tabel stają się bardziej istotne w przypadku duże zbiory danych. Chociaż można teraz używać składni tworzenia indeksu wbudowanego do tworzenia indeksów nieklastrowych w zmiennej tabeli, istnieją pewne ograniczenia i nadal nie ma powiązanych statystyk.

Nawet przy stosunkowo niewielkiej liczbie wierszy możesz napotkać problemy z wydajnością zapytania, jeśli spróbujesz wykonać zapytanie będące złączeniem i zapomnisz zdefiniować PRIMARY KEY lub UNIQUE kolumna używana do łączenia. Bez dostarczonych przez siebie metadanych optymalizator nie ma wiedzy o logicznej kolejności danych ani o tym, czy dane w kolumnie łączenia zawierają zduplikowane wartości i prawdopodobnie wybierze nieefektywne operacje łączenia, co spowoduje powolne zapytania. Jeśli pracujesz ze stertą zmiennych tabeli, możesz użyć tylko prostej listy, która prawdopodobnie zostanie przetworzona jednym haustem (skanowanie tabeli). Jeśli połączysz użycie OPTION (RECOMPILE) wskazówki, aby uzyskać dokładne szacunki liczności, oraz klucza w kolumnie łączenia, aby optymalizator był użyteczny metadanych, wówczas w przypadku mniejszych zestawów danych często można osiągnąć prędkości zapytań podobne lub lepsze niż przy użyciu lokalnej tabeli tymczasowej.

Gdy liczba wierszy wzrośnie poza strefę komfortu zmiennej tabeli lub musisz wykonać bardziej złożone dane przetwarzania, najlepiej przełączyć się na tabele tymczasowe. Tutaj masz wszystkie dostępne opcje indeksowania, a optymalizator będzie miał luksus korzystania ze statystyk dla każdego z tych indeksów. Oczywiście wadą jest to, że tymczasowe stoły wiążą się z wyższymi kosztami utrzymania. Musisz oczyścić się po sobie, aby uniknąć przeciążenia tempdb. Jeśli zmienisz tabelę tymczasową lub zmodyfikujesz zawarte w niej dane, możesz ponieść konieczność ponownej kompilacji procedury nadrzędnej.

Tabele tymczasowe są lepsze, gdy istnieje potrzeba dużej liczby operacji usuwania i wstawiania (udostępnianie zestawu wierszy ). Jest to szczególnie ważne, jeśli dane muszą zostać całkowicie usunięte z tabeli, ponieważ tylko tabele tymczasowe obsługują obcinanie. Kompromisy w projektowaniu zmiennych tabel, takie jak brak statystyk i rekompilacji, działają przeciwko nim, jeśli dane są ulotne.

Kiedy opłaca się używać zmiennych tabeli.

Zacznę od przykładu, w którym zmienna tabeli jest idealna i zapewnia lepszą wydajność. Stworzymy listę pracowników Adventureworks, w którym dziale pracują i na jakich zmianach pracują. Mamy do czynienia z małym zestawem danych (291 wierszy).

Wyniki umieścimy w drugiej tabeli tymczasowej, tak jakbyśmy przekazywali wynik do następnej partii. Listing 1 przedstawia kod.

A oto typowy wynik na mojej powolnej maszynie testowej:

Używanie tymczasowej tabeli jest konsekwentnie wolniejsze, chociaż poszczególne przebiegi mogą się znacznie różnić.

Problemy ze skalowaniem i zapomnienie o podaniu klucza lub wskazówki

Jaka będzie wydajność, jeśli połączymy dwie zmienne tabeli? Wypróbujmy to. W tym przykładzie potrzebujemy dwóch prostych tabel, jednej ze wszystkimi popularnymi słowami w języku angielskim (CommonWords), a drugiej z listą wszystkich słów w „Dracula” Brama Stokera (WordsInDracula). Pobieranie TestTVsAndTTs zawiera skrypt do tworzenia tych dwóch tabel i zapełniania każdej z nich z powiązanego pliku tekstowego. Istnieje 60 000 popularnych słów, ale Bram Stoker użył ich tylko 10 000. Pierwsza z nich znajduje się znacznie poza punktem rentowności, w którym zaczyna się preferować tabele tymczasowe.

Użyjemy czterech prostych zapytań z łączeniem zewnętrznym, testując wynik dla NULL wartości, aby znaleźć popularne słowa, których nie ma w Draculi, pospolite słowa, które są w Draculi, słowa w Draculi, które są rzadkie, i wreszcie kolejne zapytanie, aby znaleźć wspólne słowa w Draculi, ale łączące się w przeciwnym kierunku. Zapytania zobaczysz wkrótce, kiedy pokażę kod stanowiska testowego.

Poniżej przedstawiono wyniki początkowych przebiegów testowych. W pierwszym uruchomieniu obie zmienne tabeli mają klucze podstawowe, aw drugim obie są stertami, tylko po to, żeby zobaczyć, czy wyolbrzymiam problemy związane z nie zadeklarowaniem indeksu w zmiennej tabeli. Na koniec wykonujemy te same zapytania z tymczasowymi tabelami. Wszystkie testy zostały wykonane celowo na wolnym serwerze programistycznym, w celach ilustracyjnych; otrzymasz bardzo różne wyniki z serwerem produkcyjnym.

Wyniki pokazują, że gdy zmienne tabeli są stertami, istnieje ryzyko, że zapytanie będzie działać przez dziesięć minut, a nie przez 100 milisekund. To świetny przykład upiornej wydajności, jakiej możesz doświadczyć, jeśli nie znasz zasad. Jednak nawet gdy używamy kluczy podstawowych, liczba wierszy, z którymi mamy do czynienia, oznacza, że używanie tabel tymczasowych jest teraz dwa razy szybsze.

Nie będę zagłębiać się w szczegóły planów wykonania za nimi wskaźniki wydajności, inne niż podanie kilku ogólnych wyjaśnień głównych różnic. W przypadku zapytań tabeli tymczasowej optymalizator, uzbrojony w pełną wiedzę o liczności i metadanych z ograniczeń klucza podstawowego, wybiera wydajnego operatora łączenia scalającego do wykonania operacji łączenia.W przypadku zmiennej tabelarycznej z kluczami podstawowymi optymalizator zna kolejność wierszy w kolumnie łączenia i że nie zawierają one żadnych duplikatów, ale zakłada, że dotyczy tylko jednego wiersza, dlatego zamiast tego wybiera złączenie zagnieżdżonych pętli. Tutaj skanuje jedną tabelę, a następnie dla każdego zwróconego wiersza wykonuje indywidualne wyszukiwania drugiej tabeli. Staje się to mniej wydajne wraz z większymi zbiorami danych i jest szczególnie złe w przypadkach, gdy skanuje zmienną tabeli CommonWords, ponieważ powoduje ponad 60 tys. Wyszukiwań Dracula zmienna tabeli. Łączenie zagnieżdżonych pętli osiąga „szczytową nieefektywność” dla dwóch dziesięciominutowych zapytań wykorzystujących stosy zmiennych tabeli, ponieważ pociąga za sobą tysiące skanów tabeli CommonWords. Co ciekawe, dwa zapytania „typowe słowa w Draculi” działają znacznie lepiej, a to dlatego, że dla tych dwóch optymalizator wybrał zamiast tego łączenie Hash Match.

Ogólnie rzecz biorąc, tabele tymczasowe wydają się być najlepszym wyborem , ale to jeszcze nie koniec! Dodajmy OPTION (RECOMPILE) wskazówkę do zapytań, które używają zmiennych tabeli z kluczami głównymi i ponownie uruchom testy dla tych zapytań i oryginalnych zapytań przy użyciu tabel tymczasowych. Na razie pomijamy kiepskie stosy.

Jak widać, przewaga wynikowa tabeli tymczasowej znika. prawidłowe liczenie wierszy i uporządkowane dane wejściowe, optymalizator wybiera znacznie wydajniejsze łączenie scalające.

Zastanawiasz się, co by się stało, gdybyś dał tym kiepskim stosom OPTION (RECOMPILE) też podpowiedź? A więc historia zmienia się dla nich tak, że wszystkie trzy czasy są znacznie bliższe.

Co ciekawe, dwa „popularne słowa w Draculi” są byli szybko nawet na stertach są teraz znacznie wolniejsze. Uzbrojony w prawidłowe liczniki wierszy, optymalizator zmienia swoją strategię, ale ponieważ nadal nie ma żadnych użytecznych metadanych dostępnych podczas definiowania ograniczeń i kluczy, dokonuje złego wyboru. Skanuje stertę CommonWords, a następnie podejmuje próbę „częściowej agregacji”, szacując, że nastąpi agregacja od 60 tys. Wierszy do kilkuset. Nie wie, że nie ma duplikatów, więc w rzeczywistości nie gromadzi się w ogóle, a agregacja i późniejsze połączenia przechodzą do tempdb.

Stanowisko testowe

Należy pamiętać, że jest to stanowisko testowe w ostatecznej formie pokazuje mniej więcej taką samą wydajność dla trzech różnych typów tabel. Aby wrócić do oryginału, musisz usunąć OPTION (RECOMPILE) wskazówki.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136

>

137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189

UŻYJ PhilFactor;
– utwórz tabelę roboczą ze wszystkimi słowami z Draculi
DECLARE @WordsInDracula TABLE
(word VARCHAR ( 40) NOT NULL PODSTAWOWY KLASTER KLASTERÓW);
INSERT INTO @WordsInDracula (słowo) SELECT WordsInDracula.word FROM dbo.WordsInDracula;
– utwórz drugą tabelę roboczą ze wszystkimi popularnymi słowami
DECLARE @CommonWords TABLE
(word VARCHAR ( 40) NOT NULL PODSTAWOWY KLASTER KLASTERÓW);
INSERT INTO @CommonWords (word) SELECT commonwords.word FROM dbo.commonwords;
–create a timing log
DECLARE @log TABLE
(TheOrder INT IDENTITY (1, 1),
WhatHappened VARCHAR (200),
WhenItDid DATETIME2 DEFAULT GetDate ());
—- początek czasu (nigdy nie raportowany)
INSERT INTO @log (WhatHappened) SELECT „Starting My_Section_of_code”;
– miejsce na początku
————— sekcja kodu wykorzystująca zmienne tabelaryczne
– pierwsza czasowa sekcja kodu wykorzystująca zmienne tabeli
SELECT Count (*) AS
FROM @CommonWords AS c
LEFT OUTER JOIN @WordsInDracula AS d
ON d.word = c.word
WHERE d.word IS NULL
OPTION (RECOMPILE);
INSERT INTO @log (WhatHappened)
SELECT „popularne słowa nie w języku Dracula: obie zmienne tabeli z kluczami głównymi”;
– gdzie kończy się procedura, która ma zostać zakończona
– Druga sekcja czasowa kodu wykorzystująca zmienne tabeli
SELECT Count (*) AS
FROM @CommonWords AS c
LEFT OUTER JOIN @WordsInDracula AS d
ON d.word = c.word
WHERE d.słowo NIE JEST NULL
OPCJA (REKOMPILACJA);
INSERT INTO @log (WhatHappened)
SELECT „popularne słowa w Draculi: Obie zmienne tabeli z kluczami głównymi”;
– gdzie kończy się procedura, którą chcesz zakończyć
– trzecia sekcja czasowa kodu wykorzystująca zmienne tabelowe
SELECT Count (*) AS
FROM @WordsInDracula AS d
LEFT OUTER JOIN @CommonWords AS c
ON d.word = c.word
WHERE c.word IS NULL
OPTION (RECOMPILE);
INSERT INTO @log (WhatHappened)
SELECT „niezwykłe słowa w Draculi: Obie zmienne tabeli z kluczami głównymi”;
– gdzie kończy się procedura, którą chcesz zakończyć
– ostatnia sekcja kodu wykorzystująca zmienne tabelaryczne
SELECT Count (*) AS
FROM @WordsInDracula AS d
LEFT OUTER JOIN @CommonWords AS c
ON d.word = c.word
GDZIE c.word NIE JEST NULL
OPCJA (RECOMPILE);
INSERT INTO @log (WhatHappened)
SELECT „bardziej popularne słowa w Draculi: Obie zmienne tabeli z kluczami głównymi”;
– gdzie kończy się procedura, którą chcesz zakończyć
————— sekcja kodu wykorzystująca zmienne sterty
ZADEKLARUJ @WordsInDraculaHeap TABLE (słowo VARCHAR (40) NOT NULL);
INSERT INTO @WordsInDraculaHeap (word) SELECT WordsInDracula.word FROM dbo.WordsInDracula;
DECLARE @CommonWordsHeap TABLE (słowo VARCHAR (40) NOT NULL);
INSERT INTO @CommonWordsHeap (word) SELECT commonwords.word FROM dbo.commonwords;
INSERT INTO @log (WhatHappened) SELECT „Test Rig Setup”;
– gdzie kończy się procedura, która ma zostać zakończona
– pierwsza czasowa sekcja kodu wykorzystująca zmienne sterty
SELECT Count (*) AS
Z @CommonWordsHeap AS c
LEFT OUTER JOIN @WordsInDraculaHeap AS d
ON d.word = c.word
GDZIE d.word JEST NULL
OPCJA (RECOMPILE);
INSERT INTO @log (WhatHappened) SELECT „typowe słowa nie w języku Dracula: Both Heaps”;
– gdzie kończy się procedura, którą chcesz zakończyć
– sekundowa sekcja kodu wykorzystująca zmienne sterty
SELECT Count (*) AS
Z @CommonWordsHeap AS c
LEFT OUTER JOIN @WordsInDraculaHeap AS d
ON d.word = c.word
GDZIE d.word NIE JEST NULL
OPCJA (RECOMPILE);
INSERT INTO @log (WhatHappened) SELECT „typowe słowa w Draculi: Both Heaps”;
– gdzie kończy się procedura, którą chcesz zakończyć
– trzecia sekcja czasowa kodu używająca zmiennych sterty
SELECT Count (*) AS
FROM @WordsInDraculaHeap AS d
LEFT OUTER JOIN @CommonWordsHeap AS c
ON d.word = c.word
WHERE c.word IS NULL
OPTION (RECOMPILE);
INSERT INTO @log (WhatHappened) SELECT „niezwykłe słowa w Draculi: Both Heaps”;
– gdzie procedura, którą chcesz zakończyć,
– ostatnia sekcja kodu wykorzystująca zmienne sterty
SELECT Count (*) AS
FROM @WordsInDraculaHeap AS d
LEFT OUTER JOIN @CommonWordsHeap AS c
ON d.word = c.word
GDZIE c.word NIE JEST NULL
OPCJA (RECOMPILE);
INSERT INTO @log (WhatHappened) SELECT „popularne słowa w Draculi: Both Heaps”;
– gdzie kończy się procedura, którą chcesz zakończyć
————— sekcja kodu wykorzystująca Tabele tymczasowe
TWORZENIE TABELI #WordsInDracula (słowo VARCHAR (40) NOT NULL PRIMARY KEY);
INSERT INTO #WordsInDracula (słowo) SELECT WordsInDracula.word FROM dbo.WordsInDracula;
CREATE TABLE #CommonWords (słowo VARCHAR (40) NOT NULL PRIMARY KEY);
INSERT INTO #CommonWords (word) SELECT commonwords.word FROM dbo.commonwords;
INSERT INTO @log (WhatHappened) SELECT „Temp Table Test Rig Setup”;
– gdzie kończy się procedura, którą chcesz zakończyć
– pierwsza sekcja kodu wykorzystująca tabele tymczasowe
SELECT Count (*) AS
Z #CommonWords AS c
LEFT OUTER JOIN #WordsInDracula AS d
ON d.word = c.word
GDZIE d.word IS NULL;
INSERT INTO @log (WhatHappened) SELECT „typowe słowa nie w języku Dracula: obie tabele tymczasowe”;
– gdzie kończy się procedura, którą chcesz zakończyć
– Druga sekcja czasowa kodu wykorzystująca tabele tymczasowe
SELECT Count (*) AS
Z #CommonWords AS c
LEFT OUTER JOIN #WordsInDracula AS d
ON d.word = c.word
WHERE d.słowo NIE JEST NULL;
INSERT INTO @log (WhatHappened) SELECT „popularne słowa w Draculi: obie tabele tymczasowe”;
– gdzie kończy się procedura, którą chcesz zakończyć
– trzecia sekcja kodu wykorzystująca tabele tymczasowe
SELECT Count (*) AS
FROM #WordsInDracula AS d
LEFT OUTER JOIN #CommonWords AS c
ON d.word = c.word
WHERE c.word IS NULL;
INSERT INTO @log (WhatHappened) SELECT „nietypowe słowa w Draculi: obie tabele tymczasowe”;
– gdzie procedura, którą chcesz zakończyć,
– ostatnia sekcja kodu wykorzystująca tabele tymczasowe
SELECT Count (*) AS
FROM #WordsInDracula AS d
LEFT OUTER JOIN #CommonWords AS c
ON d.word = c.word
GDZIE c.word NIE JEST NULL;
INSERT INTO @log (WhatHappened) SELECT „popularne słowa w Draculi: obie tabele tymczasowe”; – gdzie procedura, której czas chcesz zakończyć,
DROP TABLE #WordsInDracula;
DROP TABLE #CommonWords;
Koniec SELECT. WhatHappened AS,
DateDiff (ms, start.WhenItDid, zakończenie.WhenItDid) AS
FROM @log AS zaczynający się
INNER JOIN @log AS kończący się
ON kończący się .Order = start.Order + 1;
– wypisz wszystkie czasy

Listing 2

Wnioski

Nie ma nic lekkomyślnego w używaniu zmiennych tabeli. Dają lepszą wydajność, gdy są używane do celów, do których zostały przeznaczone, i samodzielnie czyszczą. W pewnym momencie kompromisy, które zapewniają im lepszą wydajność (brak wyzwalania ponownej kompilacji, brak dostarczania statystyk, brak wycofywania, brak równoległości) stają się ich upadkiem.

Często ekspert SQL Server udziela mądrych rad na temat rozmiar wyniku, który spowoduje problemy dla zmiennej tabeli. Wyniki, które przedstawiłem w tym artykule, sugerują, że to zbytnio upraszcza problemy. Są dwa ważne czynniki: jeśli wynik przekroczy, powiedzmy, 1000 wierszy (a liczba ta zależy od kontekstu), to musisz mieć PRIMARY KEY lub UNIQUE dla wszelkich zapytań, które łączą się ze zmienną tabeli. W pewnym momencie będziesz musiał również uruchomić rekompilację, aby uzyskać przyzwoity plan wykonania, który ma swój własny narzut.

Nawet wtedy wydajność może znacznie ucierpieć, szczególnie jeśli wykonujesz bardziej złożone przetwarzanie , ponieważ optymalizator nadal nie ma dostępu do statystyk, a więc nie ma wiedzy na temat selektywności dowolnego predykatu zapytania. W takich przypadkach musisz przełączyć się na używanie tabel tymczasowych.

Dalsze czytanie

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *