Błąd wydajności indeksu datetime dla SQL Server 2008

11

Korzystamy z programu SQL Server 2008 R2 i mamy bardzo dużą tabelę (ponad 100 milionów wierszy) z podstawowym indeksem id oraz datetimekolumnę z indeksem nieklastrowanym. Obserwujemy bardzo nietypowe zachowanie klient / serwer w oparciu o użycie order byklauzuli konkretnie w indeksowanej kolumnie daty i godziny .

Przeczytałem następujący post: /programming/1716798/sql-server-2008-ordering-by-datetime-is-too-slow, ale z klientem / serwerem dzieje się więcej niż to, co jest zacznij opisywać tutaj.

Jeśli uruchomimy następujące zapytanie (edytowane w celu ochrony niektórych treści):

select * 
from [big table] 
where serial_number = [some number] 
order by test_date desc

Limit czasu zapytania za każdym razem. W SQL Server Profiler wykonane zapytanie wygląda tak:

exec sp_cursorprepexec @p1 output,@p2 output,NULL,N'select * .....

Teraz, jeśli zmodyfikujesz zapytanie, powiedz to:

declare @temp int;
select * from [big table] 
where serial_number = [some number] 
order by test_date desc

SQL Server Profiler pokazuje, że wykonane zapytanie wygląda tak na serwerze i DZIAŁA natychmiast:

exec sp_prepexec @p1 output, NULL, N'declare @temp int;select * from .....

W rzeczywistości możesz nawet wstawić pusty komentarz („-;”) zamiast nieużywanej instrukcji deklaracji i uzyskać ten sam wynik. Tak więc początkowo wskazywaliśmy na preprocesor sp jako główną przyczynę tego problemu, ale jeśli to zrobisz:

select * 
from [big table] 
where serial_number = [some number] 
order by Cast(test_date as smalldatetime) desc

Działa również natychmiastowo (możesz rzucić jak każdy inny datetimetyp), zwracając wynik w milisekundach. Profiler wyświetla żądanie do serwera jako:

exec sp_cursorprepexec @p1 output, @p2 output, NULL, N'select * from .....

To w pewien sposób wyklucza sp_cursorprepexecprocedurę z pełnej przyczyny problemu. Dodaj do tego fakt, że sp_cursorprepexecwywoływany jest również wtedy, gdy nie jest używane polecenie „sortuj według”, a wynik jest natychmiast zwracany.

Rozglądaliśmy się za tym problemem dość często i widzę podobne problemy publikowane przez innych, ale żadne z nich nie rozkłada go na ten poziom.

Czy inni widzieli takie zachowanie? Czy ktoś ma rozwiązanie lepsze niż umieszczanie bezsensownego kodu SQL przed instrukcją select w celu zmiany zachowania? Ponieważ SQL Server powinien wywoływać kolejność po zebraniu danych, z pewnością wygląda na to, że jest to błąd w serwerze, który utrzymuje się przez długi czas. Odkryliśmy, że takie zachowanie jest spójne w wielu naszych dużych tabelach i jest powtarzalne.

Edycje:

Powinienem również dodać, że wstawienie forceseekspowoduje, że problem zniknie.

Powinienem dodać, aby pomóc wyszukiwarkom, zgłoszony błąd przekroczenia limitu czasu ODBC to: [Microsoft] [ODBC SQL Server Driver] Operacja anulowana

Dodano 10/12/2012: Nadal szukam przyczyny źródłowej (wraz z zbudowaniem próbki do przekazania firmie Microsoft, opublikuję tutaj wszelkie wyniki po przesłaniu). Kopie do pliku śledzenia ODBC między działającym zapytaniem (z dodaną instrukcją komentarza / deklaracji) a niedziałającym zapytaniem. Podstawowa różnica w śladach została zamieszczona poniżej. Występuje w wywołaniu wywołania SQLExtendedFetch po zakończeniu wszystkich dyskusji SQLBindCol. Wywołanie kończy się niepowodzeniem z kodem powrotu -1, a następnie wątek nadrzędny wchodzi w SQLCancel. Ponieważ jesteśmy w stanie wyprodukować to zarówno ze sterownikami Native Client, jak i starszymi sterownikami ODBC, nadal wskazuję na pewien problem ze zgodnością po stronie serwera.

(clip)
MSSQLODBCTester 1664-1718   EXIT  SQLBindCol  with return code 0 (SQL_SUCCESS)
        HSTMT               0x001EEA10
        UWORD                       16 
        SWORD                        1 <SQL_C_CHAR>
        PTR                0x03259030
        SQLLEN                    51
        SQLLEN *            0x0326B820 (0)

MSSQLODBCTester 1664-1718   ENTER SQLExtendedFetch 
        HSTMT               0x001EEA10
        UWORD                        1 <SQL_FETCH_NEXT>
        SQLLEN                     1
        SQLULEN *           0x032677C4
        UWORD *             0x032679B0

MSSQLODBCTester 1664-1fd0   ENTER SQLCancel 
        HSTMT               0x001EEA10

MSSQLODBCTester 1664-1718   EXIT  SQLExtendedFetch  with return code -1 (SQL_ERROR)
        HSTMT               0x001EEA10
        UWORD                        1 <SQL_FETCH_NEXT>
        SQLLEN                     1
        SQLULEN *           0x032677C4
        UWORD *             0x032679B0

        DIAG [S1008] [Microsoft][ODBC SQL Server Driver]Operation canceled (0) 

MSSQLODBCTester 1664-1fd0   EXIT  SQLCancel  with return code 0 (SQL_SUCCESS)
        HSTMT               0x001EEA10

MSSQLODBCTester 1664-1718   ENTER SQLErrorW 
        HENV                0x001E7238
        HDBC                0x001E7B30
        HSTMT               0x001EEA10
        WCHAR *             0x08BFFC5C
        SDWORD *            0x08BFFF08
        WCHAR *             0x08BFF85C 
        SWORD                      511 
        SWORD *             0x08BFFEE6

MSSQLODBCTester 1664-1718   EXIT  SQLErrorW  with return code 0 (SQL_SUCCESS)
        HENV                0x001E7238
        HDBC                0x001E7B30
        HSTMT               0x001EEA10
        WCHAR *             0x08BFFC5C [       5] "S1008"
        SDWORD *            0x08BFFF08 (0)
        WCHAR *             0x08BFF85C [      53] "[Microsoft][ODBC SQL Server Driver]Operation canceled"
        SWORD                      511 
        SWORD *             0x08BFFEE6 (53)

MSSQLODBCTester 1664-1718   ENTER SQLErrorW 
        HENV                0x001E7238
        HDBC                0x001E7B30
        HSTMT               0x001EEA10
        WCHAR *             0x08BFFC5C
        SDWORD *            0x08BFFF08
        WCHAR *             0x08BFF85C 
        SWORD                      511 
        SWORD *             0x08BFFEE6

MSSQLODBCTester 1664-1718   EXIT  SQLErrorW  with return code 100 (SQL_NO_DATA_FOUND)
        HENV                0x001E7238
        HDBC                0x001E7B30
        HSTMT               0x001EEA10
        WCHAR *             0x08BFFC5C
        SDWORD *            0x08BFFF08
        WCHAR *             0x08BFF85C 
        SWORD                      511 
        SWORD *             0x08BFFEE6
(clip)

Dodano sprawę Microsoft Connect 10/12/2012:

https://connect.microsoft.com/SQLServer/feedback/details/767196/order-by-datetime-in-odbc-fails-for-clean-sql-statements#details

Powinienem również zauważyć, że sprawdziliśmy plany zapytań zarówno dla zapytań funkcjonujących, jak i niedziałających. Oba są ponownie wykorzystywane odpowiednio na podstawie liczby wykonania. Opróżnianie planów w pamięci podręcznej i ponowne uruchamianie nie zmienia powodzenia zapytania.

DBtheDBA
źródło
Co się stanie, jeśli spróbujesz select id, test_date from [big table] where serial_number = ..... order by test_date- Zastanawiam się tylko, czy SELECT *ma to negatywny wpływ na Twoją wydajność. Jeśli masz nieklastrowany indeks test_datei klastrowego indeksu id(zakładając, że to, co się nazywa), ta kwerenda powinny być pokryte przez ten indeks nieklastrowany, a zatem powinien wrócić dość szybko
marc_s
Przepraszam, dobra uwaga. Powinienem był dodać, że próbowaliśmy mocno zmodyfikować wybraną przestrzeń kolumny (usuwając „*” itp.) Za pomocą różnych kombinacji. Opisane powyżej zachowanie utrzymywało się przez te zmiany.
DBtheDBA,
Połączyłem teraz moje konta z tą witryną. Jeśli moderator chce przenieść post na tę stronę, nie mam nic przeciwko. Jeden z moich programistów wskazał mi tę witrynę po opublikowaniu tutaj.
DBtheDBA,
Który stos klienta jest tutaj używany? Bez całego tekstu śledzenia wydaje się to problemem. Spróbuj owinąć oryginalne połączenie sp_executesqli sprawdź, co się stanie.
Jon Seigel,
1
Jak wygląda plan powolnego wykonywania? Wąchanie parametrów?
Martin Smith

Odpowiedzi:

6

Nie ma tajemnicy, dostajesz dobry (er) lub (naprawdę) zły plan w zasadzie losowy, ponieważ nie ma wyraźnego wyboru do użycia dla indeksu. Przekonując do klauzuli ORDER BY i tym samym unikając sortowania, indeks nieklastrowany w kolumnie datetime jest bardzo złym wyborem dla tego zapytania. Tym, co byłoby znacznie lepszym indeksem dla tego zapytania, byłby jeden (serial_number, test_date). Co więcej, byłby to bardzo dobry kandydat na klastrowany klucz indeksu.

Z reguły kciuki szeregów czasowych powinny być grupowane według kolumny czasu, ponieważ przeważająca większość żądań jest zainteresowana konkretnymi przedziałami czasowymi. Jeśli dane są z natury podzielone na partycje w kolumnie o niskiej selektywności, tak jak w przypadku numeru_seryjnego, kolumna ta powinna zostać dodana jako skrajnie lewa w definicji klucza klastrowego.

Remus Rusanu
źródło
Jestem trochę zdezorientowany. Dlaczego plan miałby być oparty na the orderklauzuli? Czy plan nie powinien ograniczać się do wherewarunków, ponieważ porządkowanie powinno nastąpić dopiero po pobraniu wierszy? Dlaczego serwer miałby próbować sortować rekordy przed ustawieniem całego zestawu wyników?
DBtheDBA,
5
Nie wyjaśnia to również, dlaczego dodanie komentarza na początku zapytania wpływa na czas trwania uruchomienia.
cfradenburg,
Ponadto nasze tabele są prawie zawsze sprawdzane według numeru seryjnego, a nie test_date. Mamy indeksy nieklastrowe na obu i klastrowane tylko na kolumnie id w tabeli. Jest to operacyjny magazyn danych, a dodanie indeksów klastrowych do innych kolumn spowodowałoby tylko podziały stron i gorszą wydajność.
DBtheDBA,
1
@DBtheDBA: jeśli chcesz zgłosić roszczenie dotyczące „błędu”, musisz przeprowadzić odpowiednie dochodzenie i ujawnić informacje. Dokładny schemat tabeli i wywożone statystykach, wykonaj Jak wygenerować skrypt niezbędnego metadane bazy danych, aby utworzyć bazę danych statystyki tylko dla SQL Server 2005 i SQL Server 2008 , a konkretnie wszystkie ważne Script Statystyki : Script Statystyki i histogramy . Dodaj je do informacji o poście wraz ze wskazówkami opisującymi problem.
Remus Rusanu,
1
Czytamy to wcześniej podczas naszych wyszukiwań i rozumiem, co mówisz, ale istnieje zasadnicza wada w działaniu serwera. Odbudowaliśmy tabelę i indeksy i odtworzyliśmy ją na nowej tabeli. Opcja ponownej kompilacji nie rozwiązuje problemu, co stanowi dużą wskazówkę, że coś jest nie tak. Nie wątpię, że umieszczenie indeksów klastrowych na wszystkim może potencjalnie rozwiązać ten problem, ale nie jest to rozwiązanie podstawowej przyczyny, jest to obejście i kosztowne na dużym stole.
DBtheDBA
0

Udokumentuj szczegóły dotyczące sposobu odtworzenia błędu i prześlij go na connect.microsoft.com. Sprawdziłem i nie widziałem już nic, co by się z tym wiązało.

cfradenburg
źródło
Poproszę mojego DBA o napisanie skryptu jutro, aby stworzyć środowisko do odtwarzania. Nie sądzę, że to takie trudne. Zamieszczę go również tutaj, jeśli ktoś będzie zainteresowany wypróbowaniem go samodzielnie.
DBtheDBA,
Opublikuj element Connect również, gdy zostanie otwarty. W ten sposób, jeśli ktoś ma ten problem, trafia do niego. I każdy, kto ogląda to pytanie, może chcieć zagłosować nad tym elementem, aby Microsoft był bardziej skłonny zwrócić na to uwagę.
cfradenburg
0

Moja hipoteza jest taka, że ​​korzystasz z pamięci podręcznej planu zapytań. (Remus może mówić to samo co ja, ale w inny sposób.)

Oto mnóstwo szczegółów na temat planowania buforowania przez SQL .

Przeglądanie szczegółów: ktoś uruchomił to zapytanie wcześniej, dla konkretnego [jakiejś liczby]. SQL sprawdził podaną wartość, indeksy i statystyki dla odpowiedniej tabeli / kolumn itp. I zbudował plan, który działał dobrze dla tej konkretnej [pewnej liczby]. Następnie zbuforował plan, uruchomił go i zwrócił wyniki dzwoniącemu.

Później ktoś inny uruchamia to samo zapytanie o inną wartość [jakaś liczba]. Ta konkretna wartość powoduje niesamowicie różną liczbę wierszy wyników, a silnik powinien utworzyć inny plan dla tego wystąpienia zapytania. Ale to nie działa w ten sposób. Zamiast tego SQL bierze zapytanie i (mniej więcej) wyszukuje wielkość liter w pamięci podręcznej zapytania, szukając wcześniej istniejącej wersji zapytania. Kiedy znajdzie ten wcześniejszy, po prostu korzysta z tego planu.

Chodzi o to, że oszczędza czas potrzebny na podjęcie decyzji w sprawie planu i jego zbudowanie. Luka w tym pomyśle polega na uruchomieniu tego samego zapytania z wartościami, które dają bardzo różne wyniki. Powinny mieć inne plany, ale nie mają. Ktokolwiek uruchomił kwerendę jako pierwszy, pomaga ustawić zachowanie każdego, kto ją następnie uruchomi.

Szybki przykład: wybierz * z [osób] gdzie nazwisko = „SMITH” - bardzo popularne nazwisko w USA GO wybierz * z [osób] gdzie nazwisko = „BONAPARTE” - NIE popularne nazwisko w USA

Po uruchomieniu zapytania dla BONAPARTE plan utworzony dla SMITH zostanie ponownie użyty. Jeśli SMITH spowodował skanowanie tabeli (co może być dobre , jeśli wiersze w tabeli wynoszą 99% SMITH), wówczas BONAPARTE również otrzyma skanowanie tabeli. Jeśli BONAPARTE został uruchomiony przed SMITH, plan wykorzystujący indeks może zostać zbudowany i użyty, a następnie użyty ponownie dla SMITH (co może być lepsze w przypadku skanowania tabeli). Ludzie mogą nie zauważyć, że wydajność SMITH jest niska, ponieważ oczekują słabej wydajności, ponieważ cała tabela musi zostać odczytana, a odczyt indeksu i przeskakiwanie do tabeli nie jest bezpośrednio zauważane.

W odniesieniu do twoich zmian, które powinny zmienić wszystko, podejrzewam, że SQL po prostu postrzega to jako zupełnie inne zapytanie i buduje nowy plan, specyficzny dla twojej wartości [jakiejś liczby].

Aby to przetestować, dokonaj bezsensownej zmiany w zapytaniu, na przykład dodaj spacje między FOR a nazwą tabeli lub dodaj komentarz na końcu. Czy to jest szybkie Jeśli tak, to dlatego, że to zapytanie różni się nieco od tego, co znajduje się w pamięci podręcznej, więc SQL zrobił to, co robi dla „nowych” zapytań.

Aby znaleźć rozwiązanie, przyjrzałbym się trzem rzeczom. Po pierwsze, upewnij się, że twoje statystyki są aktualne. To naprawdę powinna być pierwsza rzecz, którą zrobisz, gdy zapytanie wydaje się dziwne lub losowe. Twój DBA powinien to robić, ale coś się dzieje. Zwykłym sposobem na zapewnienie aktualności statystyk jest ponowne indeksowanie tabel, co niekoniecznie jest lekką rzeczą, ale istnieją również opcje aktualizacji statystyk.

Drugą rzeczą do przemyślenia jest dodanie indeksów zgodnie z sugestiami Remusa. Przy lepszym / innym indeksie jedna wartość w stosunku do drugiej może być bardziej stabilna i nie zmieniać się tak gwałtownie.

Jeśli to nie pomoże, trzecią rzeczą do wypróbowania jest wymuszenie nowego planu za każdym razem, gdy uruchamiasz instrukcję, używając słowa kluczowego RECOMPILE:

wybierz * z [dużej tabeli] gdzie numer_seryjny = [jakaś liczba] uporządkuj według test_date desc OPCJA (RECOMPILE)

Jest tutaj artykuł opisujący podobną sytuację . Szczerze mówiąc, widziałem wcześniej RECOMPILE stosowane do procedur przechowywanych, ale wydaje się, że działa z „normalnymi” instrukcjami SELECT dla. Kimberly Tripp nigdy mnie źle nie poprowadziła.

Możesz także przyjrzeć się funkcji zwanej „ przewodnikami po planach ”, ale jest ona bardziej złożona i może być nadmierna.

cieśnina Darina
źródło
Aby uwzględnić niektóre z tych problemów: 1. Statystyki zostały zaktualizowane, są aktualizowane. 2. Próbowaliśmy indeksować na kilka sposobów (obejmujących indeksy itp.), Ale problem wydaje się być bardziej związany z order byużyciem w stosunku do indeksu daty / godziny. 3. Właśnie wypróbowałem swój pomysł z opcją RECOMPILE, ale i tak się nie udało, co mnie trochę zaskoczyło, miałem nadzieję, że zadziała, chociaż nie wiem, czy to rozwiązanie dla produkcji.
DBtheDBA,