Wyślę odpowiedź, aby zacząć. Moja pierwsza myśl była taka, że powinna istnieć możliwość zachowania porządku zagnieżdżonego łączenia pętli wraz z kilkoma tabelami pomocniczymi, które mają jeden wiersz dla każdej litery. Trudna część miała być zapętlona w taki sposób, aby wyniki były uporządkowane według długości, a także aby uniknąć duplikatów. Na przykład, łącząc krzyżowo CTE, które zawiera wszystkie 26 wielkich liter wraz z „”, możesz wygenerować 'A' + '' + 'A'
i '' + 'A' + 'A'
który jest oczywiście tym samym ciągiem.
Pierwszą decyzją było miejsce przechowywania danych pomocnika. Próbowałem użyć tabeli tymczasowej, ale miało to zaskakująco negatywny wpływ na wydajność, mimo że dane mieszczą się na jednej stronie. Tabela temp zawierała następujące dane:
SELECT 'A'
UNION ALL SELECT 'B'
...
UNION ALL SELECT 'Y'
UNION ALL SELECT 'Z'
W porównaniu z użyciem CTE, zapytanie zajęło 3 razy więcej w przypadku tabeli klastrowej i 4 razy dłużej w przypadku sterty. Nie sądzę, że problemem jest to, że dane są na dysku. Należy go wczytać do pamięci jako pojedynczą stronę i przetworzyć w pamięci dla całego planu. Być może SQL Server może pracować z danymi od operatora Constant Scan wydajniej niż z danymi przechowywanymi na typowych stronach magazynu.
Co ciekawe, SQL Server wybiera umieszczenie uporządkowanych wyników z jednostronicowej tabeli tempdb z uporządkowanymi danymi w buforze tabeli:
SQL Server często umieszcza wyniki dla wewnętrznej tabeli krzyżowego łączenia w buforze tabeli, nawet jeśli wydaje się to nonsensowne. Myślę, że optymalizator wymaga trochę pracy w tym obszarze. Uruchomiłem kwerendę, NO_PERFORMANCE_SPOOL
aby uniknąć spadku wydajności.
Jednym z problemów z użyciem CTE do przechowywania danych pomocnika jest to, że nie ma gwarancji, że dane zostaną zamówione. Nie mogę wymyślić, dlaczego optymalizator postanowiłby tego nie zamawiać, a we wszystkich moich testach dane były przetwarzane w kolejności, w której napisałem CTE:
Najlepiej jednak nie ryzykować, zwłaszcza jeśli istnieje sposób, aby to zrobić bez dużego obciążenia wydajności. Możliwe jest uporządkowanie danych w tabeli pochodnej poprzez dodanie zbędnego TOP
operatora. Na przykład:
(SELECT TOP (26) CHR FROM FIRST_CHAR ORDER BY CHR)
To dodanie do zapytania powinno gwarantować, że wyniki zostaną zwrócone we właściwej kolejności. Spodziewałem się, że wszystkie rodzaje będą miały duży negatywny wpływ na wydajność. Optymalizator zapytań również tego oczekiwał na podstawie szacunkowych kosztów:
Bardzo zaskakujące, że nie zauważyłem żadnej statystycznie istotnej różnicy w czasie procesora lub czasie działania z wyraźnym zamówieniem lub bez niego. Jeśli cokolwiek, zapytanie wydawało się działać szybciej z ORDER BY
! Nie mam wytłumaczenia tego zachowania.
Problem polegał na tym, aby dowiedzieć się, jak wstawić puste znaki we właściwe miejsca. Jak wspomniano wcześniej, prosty CROSS JOIN
spowodowałby zduplikowanie danych. Wiemy, że 100000000-ty ciąg będzie miał długość sześciu znaków, ponieważ:
26 + 26 ^ 2 + 26 ^ 3 + 26 ^ 4 + 26 ^ 5 = 914654 <100000000
ale
26 + 26 ^ 2 + 26 ^ 3 + 26 ^ 4 + 26 ^ 5 + 26 ^ 6 = 321272406> 100000000
Dlatego musimy dołączyć się do litery CTE tylko sześć razy. Załóżmy, że dołączamy do CTE sześć razy, bierzemy jedną literę z każdego CTE i łączymy je wszystkie razem. Załóżmy, że pierwsza lewa litera nie jest pusta. Jeśli którakolwiek z kolejnych liter jest pusta, oznacza to, że łańcuch ma mniej niż sześć znaków, więc jest duplikatem. Dlatego możemy zapobiec duplikatom, znajdując pierwszy niepusty znak i wymagając, aby wszystkie znaki po nim również nie były puste. Wybrałem śledzenie tego, przypisując FLAG
kolumnę do jednego z CTE i dodając zaznaczenie do WHERE
klauzuli. Powinno to być bardziej zrozumiałe po spojrzeniu na zapytanie. Ostatnie zapytanie jest następujące:
WITH FIRST_CHAR (CHR) AS
(
SELECT 'A'
UNION ALL SELECT 'B'
UNION ALL SELECT 'C'
UNION ALL SELECT 'D'
UNION ALL SELECT 'E'
UNION ALL SELECT 'F'
UNION ALL SELECT 'G'
UNION ALL SELECT 'H'
UNION ALL SELECT 'I'
UNION ALL SELECT 'J'
UNION ALL SELECT 'K'
UNION ALL SELECT 'L'
UNION ALL SELECT 'M'
UNION ALL SELECT 'N'
UNION ALL SELECT 'O'
UNION ALL SELECT 'P'
UNION ALL SELECT 'Q'
UNION ALL SELECT 'R'
UNION ALL SELECT 'S'
UNION ALL SELECT 'T'
UNION ALL SELECT 'U'
UNION ALL SELECT 'V'
UNION ALL SELECT 'W'
UNION ALL SELECT 'X'
UNION ALL SELECT 'Y'
UNION ALL SELECT 'Z'
)
, ALL_CHAR (CHR, FLAG) AS
(
SELECT '', 0 CHR
UNION ALL SELECT 'A', 1
UNION ALL SELECT 'B', 1
UNION ALL SELECT 'C', 1
UNION ALL SELECT 'D', 1
UNION ALL SELECT 'E', 1
UNION ALL SELECT 'F', 1
UNION ALL SELECT 'G', 1
UNION ALL SELECT 'H', 1
UNION ALL SELECT 'I', 1
UNION ALL SELECT 'J', 1
UNION ALL SELECT 'K', 1
UNION ALL SELECT 'L', 1
UNION ALL SELECT 'M', 1
UNION ALL SELECT 'N', 1
UNION ALL SELECT 'O', 1
UNION ALL SELECT 'P', 1
UNION ALL SELECT 'Q', 1
UNION ALL SELECT 'R', 1
UNION ALL SELECT 'S', 1
UNION ALL SELECT 'T', 1
UNION ALL SELECT 'U', 1
UNION ALL SELECT 'V', 1
UNION ALL SELECT 'W', 1
UNION ALL SELECT 'X', 1
UNION ALL SELECT 'Y', 1
UNION ALL SELECT 'Z', 1
)
SELECT TOP (100000000)
d6.CHR + d5.CHR + d4.CHR + d3.CHR + d2.CHR + d1.CHR
FROM (SELECT TOP (27) FLAG, CHR FROM ALL_CHAR ORDER BY CHR) d6
CROSS JOIN (SELECT TOP (27) FLAG, CHR FROM ALL_CHAR ORDER BY CHR) d5
CROSS JOIN (SELECT TOP (27) FLAG, CHR FROM ALL_CHAR ORDER BY CHR) d4
CROSS JOIN (SELECT TOP (27) FLAG, CHR FROM ALL_CHAR ORDER BY CHR) d3
CROSS JOIN (SELECT TOP (27) FLAG, CHR FROM ALL_CHAR ORDER BY CHR) d2
CROSS JOIN (SELECT TOP (26) CHR FROM FIRST_CHAR ORDER BY CHR) d1
WHERE (d2.FLAG + d3.FLAG + d4.FLAG + d5.FLAG + d6.FLAG) =
CASE
WHEN d6.FLAG = 1 THEN 5
WHEN d5.FLAG = 1 THEN 4
WHEN d4.FLAG = 1 THEN 3
WHEN d3.FLAG = 1 THEN 2
WHEN d2.FLAG = 1 THEN 1
ELSE 0 END
OPTION (MAXDOP 1, FORCE ORDER, LOOP JOIN, NO_PERFORMANCE_SPOOL);
Wartości CTE są takie, jak opisano powyżej. ALL_CHAR
jest łączony pięciokrotnie, ponieważ zawiera wiersz pustego znaku. Ostateczna postać w ciągu nigdy nie powinien być pusty, dzięki czemu osobny CTE jest zdefiniowany dla niego FIRST_CHAR
. Dodatkowa kolumna flagi w ALL_CHAR
służy do zapobiegania duplikatom, jak opisano powyżej. Może istnieć bardziej skuteczny sposób na wykonanie tej kontroli, ale są zdecydowanie bardziej nieefektywne metody. Jedna próba mnie LEN()
i POWER()
wykonany przebieg zapytań sześć razy wolniej niż w obecnej wersji.
MAXDOP 1
I FORCE ORDER
wskazówki są niezbędne, aby upewnić się, że kolejność jest zachowana w zapytaniu. Plan szacunkowy z adnotacjami może być pomocny w ustaleniu, dlaczego połączenia są w ich bieżącej kolejności:
Plany zapytań są często odczytywane od prawej do lewej, ale żądania wierszy zdarzają się od lewej do prawej. Idealnie byłoby, gdyby SQL Server zażądał dokładnie 100 milionów wierszy od d1
stałego operatora skanowania. Podczas przechodzenia od lewej do prawej oczekuję, że od każdego operatora będzie wymagane mniej wierszy. Widzimy to w rzeczywistym planie wykonania . Ponadto poniżej znajduje się zrzut ekranu z eksploratora SQL Sentry Plan Explorer:
Mamy dokładnie 100 milionów wierszy z d1, co jest dobrą rzeczą. Zauważ, że stosunek rzędów między d2 i d3 wynosi prawie dokładnie 27: 1 (165336 * 27 = 4464072), co ma sens, jeśli pomyślisz o tym, jak będzie działać połączenie krzyżowe. Stosunek rzędów między d1 i d2 wynosi 22,4, co oznacza trochę zmarnowanej pracy. Wierzę, że dodatkowe wiersze pochodzą z duplikatów (z powodu pustych znaków na środku ciągów), które nie przechodzą obok zagnieżdżonego operatora łączenia pętli, który wykonuje filtrowanie.
LOOP JOIN
Wskazówką jest to technicznie konieczne, ponieważ CROSS JOIN
mogą być realizowane tylko jako pętla dołączyć w SQL Server. Ma NO_PERFORMANCE_SPOOL
to na celu zapobieganie zbędnemu buforowaniu tabeli. Pominięcie wskazówki dotyczącej szpuli spowodowało, że zapytanie zajęło 3 razy więcej czasu na mojej maszynie.
Ostatnie zapytanie ma czas procesora około 17 sekund i całkowity czas, który upłynął 18 sekund. Tak było podczas uruchamiania zapytania przez SSMS i odrzucania zestawu wyników. Jestem bardzo zainteresowany widzeniem innych metod generowania danych.