Korzystamy z programu SQL Server 2008 R2 i mamy bardzo dużą tabelę (ponad 100 milionów wierszy) z podstawowym indeksem id oraz datetime
kolumnę z indeksem nieklastrowanym. Obserwujemy bardzo nietypowe zachowanie klient / serwer w oparciu o użycie order by
klauzuli 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 datetime
typ), 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_cursorprepexec
procedurę z pełnej przyczyny problemu. Dodaj do tego fakt, że sp_cursorprepexec
wywoł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 forceseek
spowoduje, ż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:
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.
źródło
select id, test_date from [big table] where serial_number = ..... order by test_date
- Zastanawiam się tylko, czySELECT *
ma to negatywny wpływ na Twoją wydajność. Jeśli masz nieklastrowany indekstest_date
i klastrowego indeksuid
(zakładając, że to, co się nazywa), ta kwerenda powinny być pokryte przez ten indeks nieklastrowany, a zatem powinien wrócić dość szybkosp_executesql
i sprawdź, co się stanie.Odpowiedzi:
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.
źródło
the order
klauzuli? Czy plan nie powinien ograniczać się dowhere
warunkó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?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.
źródło
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.
źródło
order by
uż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.