Optymalizacja łączenia na dużym stole


10

Próbuję zwiększyć wydajność dzięki zapytaniu, które uzyskuje dostęp do tabeli zawierającej ~ 250 milionów rekordów. Z mojego lektury faktycznego (nieocenionego) planu wykonania pierwsze wąskie gardło to zapytanie, które wygląda następująco:

select
    b.stuff,
    a.added,
    a.value
from
    dbo.hugetable a
    inner join
    #smalltable b on a.fk = b.pk
where
    a.added between @start and @end;

Zobacz dalsze definicje tabel i indeksów.

Plan wykonania wskazuje, że na #smalltable używana jest zagnieżdżona pętla, a skanowanie indeksu na hugetable jest wykonywane 480 razy (dla każdego wiersza w #smalltable). Wydaje mi się to odwrócone, więc próbowałem wymusić użycie połączenia scalającego:

select
    b.stuff,
    a.added,
    a.value
from
    dbo.hugetable a with(index = ix_hugetable)
    inner merge join
    #smalltable b with(index(1)) on a.fk = b.pk
where
    a.added between @start and @end;

Indeks, o którym mowa (pełna definicja znajduje się poniżej), obejmuje kolumny fk (predykat złączenia), dodane (używane w klauzuli where) i id (bezużyteczne) w porządku rosnącym oraz zawiera wartość .

Kiedy to robię, zapytanie przepływa z 2 1/2 minuty do ponad 9. Miałem nadzieję, że podpowiedzi wymuszą bardziej wydajne łączenie, które wykonuje tylko jedno przejście nad każdą tabelą, ale najwyraźniej nie.

Wszelkie wskazówki są mile widziane. W razie potrzeby dostarczone dodatkowe informacje.

Aktualizacja (2011/06/02)

Po reorganizacji indeksowania na stole dokonałem znacznego wzrostu wydajności, jednak natrafiłem na nową przeszkodę, jeśli chodzi o podsumowanie danych w ogromnej tabeli. Wynikiem jest podsumowanie według miesięcy, które obecnie wygląda następująco:

select
    b.stuff,
    datediff(month, 0, a.added),
    count(a.value),
    sum(case when a.value > 0 else 1 end) -- this triples the running time!
from
    dbo.hugetable a
    inner join
    #smalltable b on a.fk = b.pk
group by
    b.stuff,
    datediff(month, 0, a.added);

Obecnie hugetable ma indeks klastrowany pk_hugetable (added, fk)(klucz podstawowy), a indeks nieklastrowany działa w drugą stronę ix_hugetable (fk, added).

Bez czwartej kolumny powyżej optymalizator używa zagnieżdżonego łączenia pętli, jak poprzednio, używając #smalltable jako wejścia zewnętrznego, a nieklastrowany indeks szuka jako pętli wewnętrznej (wykonując ponownie 480 razy). To, co mnie niepokoi, to rozbieżność między rzędami szacowanymi (12 958,4) a rzeczywistymi (74 668 468). Względny koszt tych poszukiwań wynosi 45%. Czas trwania jest jednak krótszy niż minuta.

Czwarta kolumna skraca czas działania do 4 minut. Tym razem szuka indeksu klastrowego (2 wykonania) dla tego samego kosztu względnego (45%), agreguje poprzez dopasowanie hash (30%), a następnie łączy hash na #smalltable (0%).

Nie jestem pewien, co do mojego następnego sposobu działania. Obawiam się, że ani wyszukiwanie zakresu dat, ani predykat łączenia nie są gwarantowane, a nawet wszystko, co może drastycznie zmniejszyć zestaw wyników. Zakres dat w większości przypadków przycina tylko 10-15% rekordów, a wewnętrzne sprzężenie na fk może odfiltrować może 20-30%.


Zgodnie z życzeniem Willa A wyniki sp_spaceused:

name      | rows      | reserved    | data        | index_size  | unused
hugetable | 261774373 | 93552920 KB | 18373816 KB | 75167432 KB | 11672 KB

#smalltable jest zdefiniowany jako:

create table #endpoints (
    pk uniqueidentifier primary key clustered,
    stuff varchar(6) null
);

Podczas gdy dbo.hugetable jest zdefiniowany jako:

create table dbo.hugetable (
    id uniqueidentifier not null,
    fk uniqueidentifier not null,
    added datetime not null,
    value decimal(13, 3) not null,

    constraint pk_hugetable primary key clustered (
        fk asc,
        added asc,
        id asc
    )
    with (
        pad_index = off, statistics_norecompute = off,
        ignore_dup_key = off, allow_row_locks = on,
        allow_page_locks = on
    )
    on [primary]
)
on [primary];

Ze zdefiniowanym następującym indeksem:

create nonclustered index ix_hugetable on dbo.hugetable (
    fk asc, added asc, id asc
) include(value) with (
    pad_index = off, statistics_norecompute = off,
    sort_in_tempdb = off, ignore_dup_key = off,
    drop_existing = off, online = off,
    allow_row_locks = on, allow_page_locks = on
)
on [primary];

Pole id jest redundantne, artefakt z poprzedniego DBA, który nalegał, aby wszystkie tabele wszędzie miały GUID, bez wyjątków.


Czy możesz podać wynik sp_spaceused 'dbo.hugetable'?
Czy

Gotowe, dodane tuż nad początkiem definicji tabeli.
Szybki Joe Smith,

Z pewnością tak jest. Jego niedorzeczny rozmiar jest powodem, dla którego się tym zajmuję.
Szybki Joe Smith,

Odpowiedzi:


5

Twój ix_hugetablewygląd jest zupełnie bezużyteczny, ponieważ:

  • to jest współczynnik klastrowy (PK)
  • INCLUDE nie robi różnicy, ponieważ indeks klastrowany ZAWIERA wszystkie kolumny niekluczowe (wartości niekluczowe przy najniższym liściu = INCLUDEd = czym jest indeks klastrowany)

Ponadto: - dodane lub fk powinno być pierwsze - ID jest pierwsze = mało użyteczne

Spróbuj zmienić klucz klastrowany na (added, fk, id)i upuść ix_hugetable. Już próbowałeś (fk, added, id). Jeśli nic więcej, zaoszczędzisz dużo miejsca na dysku i utrzymanie indeksu

Inną opcją może być wypróbowanie podpowiedzi FORCE ORDER z kolejnością tabel, bez wskazówek JOIN / INDEX. Staram się nie używać osobno wskazówek JOIN / INDEX, ponieważ usuwasz opcje optymalizatora. Wiele lat temu powiedziano mi (seminarium z guru SQL), że wskazówka FORCE ORDER może pomóc, gdy masz ogromny stół DOŁĄCZ mały stolik: YMMV 7 lat później ...

Aha, i daj nam znać, gdzie mieszka DBA, abyśmy mogli zorganizować dostosowanie perkusji

Edytuj, po aktualizacji 02 czerwca

Czwarta kolumna nie jest częścią indeksu nieklastrowanego, więc używa indeksu klastrowanego.

Spróbuj zmienić indeks NC na INCLUDE kolumna wartości, aby nie musiała uzyskiwać dostępu do kolumny wartości dla indeksu klastrowego

create nonclustered index ix_hugetable on dbo.hugetable (
    fk asc, added asc
) include(value)

Uwaga: Jeśli wartość nie jest zerowalna, to jest taka sama jak COUNT(*)semantycznie. Ale dla SUMA potrzebuje rzeczywistej wartości, a nie istnienia .

Na przykład, jeśli zmienisz COUNT(value)na COUNT(DISTINCT value) bez zmiany indeksu, powinien on ponownie przerwać zapytanie, ponieważ musi przetwarzać wartość jako wartość, a nie jako istnienie.

Kwerenda wymaga 3 kolumn: dodanej, fk, wartości. Pierwsze 2 są filtrowane / łączone, podobnie jak kluczowe kolumny. wartość jest właśnie używana, więc można ją uwzględnić. Klasyczne zastosowanie indeksu pokrycia.


Hah, miałem w głowie, że indeksy klastrowane i nieklastrowane miały fk i dodawane w różnej kolejności. Nie mogę uwierzyć, że tego nie zauważyłem, prawie tak samo, jak nie mogę uwierzyć, że zostało to ustawione w ten sposób. Jutro zmienię indeks klastrowy, a potem pójdę ulicą na kawę, gdy się odbuduje.
Szybki Joe Smith

Zmieniłem indeksowanie i uderzyłem z FORCE ORDER, próbując zmniejszyć liczbę wyszukiwań na dużym stole, ale bezskutecznie. Moje pytanie zostało zaktualizowane.
Szybki Joe Smith

@Quick Joe Smith: zaktualizowane moją odpowiedź
gbn

Tak, próbowałem tego niedługo potem. Ponieważ przebudowywanie indeksu trwa tak długo, zapomniałem o tym i początkowo myślałem, że przyspieszyłem, robiąc coś zupełnie niezwiązanego.
Szybki Joe Smith

2

Zdefiniuj indeks hugetabletylko na addedkolumnie.

Bazy danych będą korzystać z indeksu wieloczęściowego (wielokolumnowego) tylko z prawej strony listy kolumn, ponieważ ma wartości liczone od lewej. Twoje zapytanie nie określa fkklauzuli where pierwszego zapytania, więc ignoruje indeks.


Plan wykonania pokazuje, że szukany jest indeks (ix_hugetable) . A może mówisz, że ten indeks nie jest odpowiedni dla zapytania?
Szybki Joe Smith,

Indeks nie jest odpowiedni. Kto wie, jak to jest „używać indeksu”. Doświadczenie mówi mi, że to twój problem. Spróbuj i powiedz nam, jak to działa.
Bohemian

@ Szybki Joe Smith - czy próbowałeś sugestii @ Bohemian? Jakie są wyniki?
Lieven Keersmaekers

2
Nie zgadzam się: klauzula ON jest najpierw logicznie przetwarzana i w praktyce jest GDZIE, więc OP musi najpierw wypróbować obie kolumny. Brak indeksowania na fk w ogóle = skanowanie indeksu klastrowego lub wyszukiwanie klucza w celu uzyskania wartości fk dla JOIN. Czy możesz również dodać odniesienia do opisanego zachowania? Zwłaszcza dla SQL Server, biorąc pod uwagę, że masz niewiele wcześniejszych odpowiedzi na ten RDBMS. Właściwie -1 z perspektywy czasu jako a,
piszę

2

Plan wykonania wskazuje, że na #smalltable używana jest zagnieżdżona pętla, a skanowanie indeksu na hugetable jest wykonywane 480 razy (dla każdego wiersza w #smalltable).

Jest to kolejność, jakiej spodziewałbym się po optymalizatorze zapytań, przy założeniu, że pętla łączy się z właściwym wyborem. Alternatywą jest zapętlenie 250 milionów razy i każdorazowe przeszukiwanie tablicy #temp - co może zająć godziny / dni.

Indeks, który zmuszasz do użycia w złączeniu MERGE, to prawie 250 milionów wierszy * „rozmiar każdego wiersza” - niezbyt mały, przynajmniej kilka GB. Sądząc z danych sp_spaceusedwyjściowych „kilka GB” może być dość mało powiedziane - połączenie MERGE wymaga przejrzenia indeksu, który będzie bardzo intensywny we / wy.


Rozumiem, że istnieją 3 rodzaje algorytmów łączenia i że połączenie scalające ma najlepszą wydajność, gdy oba dane wejściowe są uporządkowane według predykatu łączenia. Słusznie czy nie, to jest wynik, który staram się uzyskać.
Szybki Joe Smith

2
Ale jest w tym coś więcej. Jeśli #smalltable ma dużą liczbę wierszy, połączenie scalania może być odpowiednie. Jeśli, jak sama nazwa wskazuje, ma niewielką liczbę wierszy, połączenie pętli może być właściwym wyborem. Wyobraź sobie, że #smalltable miał jeden lub dwa rzędy i pasował do garstki rzędów z drugiego stołu - trudno byłoby uzasadnić połączenie tutaj.
Czy

Uznałem, że było w tym coś więcej; Po prostu nie wiedziałam, co to może być. Optymalizacja bazy danych nie jest moją mocną stroną, jak zapewne już się domyślacie.
Szybki Joe Smith,

@ Szybki Joe Smith - dzięki za sp_spaceused. 75 GB indeksu i 18 GB danych - czy ix_hugetable nie jest jedynym indeksem na stole?
Czy

1
+1 do woli. Planista robi obecnie właściwą rzecz. Problem polega na losowym wyszukiwaniu dysku ze względu na sposób klastrowania tabel.
Denis de Bernardy

1

Twój indeks jest niepoprawny. Zobacz indeksy dos i donts .

W tej chwili twoim jedynym przydatnym indeksem jest klucz podstawowy małego stolika. Jedynym rozsądnym planem jest zatem skanowanie sekwencji małego stolika i zagnieżdżenie bałaganu przy dużym.

Spróbuj dodać indeks klastrowany hugetable(added, fk). Powinno to zmusić planistę do wyszukania odpowiednich rzędów z ogromnego stołu, a pętla gniazdowa lub scalanie łączą je z małym stołem.


Dzięki za ten link. Spróbuję tego, kiedy jutro będę w pracy.
Szybki Joe Smith
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.