Zapytanie nie odpowiada podczas dodawania dwóch kolumn

9

Po dodaniu dwóch kolumn do mojego zapytania zapytanie nie odpowiada. Typ kolumny to nvarchar(2000). To trochę niezwykłe.

  • Wersja SQL Server to 2014.
  • Istnieje tylko jeden indeks główny.
  • Całe rekordy to tylko 1000 wierszy.

Oto plan wykonania przed ( XML showplan ):

wprowadź opis zdjęcia tutaj

Plan wykonania po ( XML showplan ):

wprowadź opis zdjęcia tutaj

Oto zapytanie:

select top(100)
  Batch_Tasks_Queue.id,
  btq.id,
  Batch_Tasks_Queue.[Parameters], -- this field
  btq.[Parameters]  -- and this field
from
        Batch_Tasks_Queue with(nolock)
    inner join  Batch_Tasks_Queue btq with(nolock)  on  Batch_Tasks_Queue.Start_Time < btq.Start_Time
                            and btq.Start_Time < Batch_Tasks_Queue.Finish_Time
                            and Batch_Tasks_Queue.id <> btq.id                            
                            and btq.Start_Time is not null
                            and btq.State in (3, 4)                          
where
    Batch_Tasks_Queue.Start_Time is not null      
    and Batch_Tasks_Queue.State in (3, 4)
    and Batch_Tasks_Queue.Operation_Type = btq.Operation_Type
    and Batch_Tasks_Queue.Operation_Type not in (23, 24, 25, 26, 27, 28, 30)

order by
    Batch_Tasks_Queue.Start_Time desc

Cały wynik to 17 wierszy. Brudne dane (wskazówka nolock) nie są ważne.

Oto struktura tabeli:

CREATE TABLE [dbo].[Batch_Tasks_Queue](
    [Id] [int] NOT NULL,
    [OBJ_VERSION] [numeric](8, 0) NOT NULL,
    [Operation_Type] [numeric](2, 0) NULL,
    [Request_Time] [datetime] NOT NULL,
    [Description] [varchar](1000) NULL,
    [State] [numeric](1, 0) NOT NULL,
    [Start_Time] [datetime] NULL,
    [Finish_Time] [datetime] NULL,
    [Parameters] [nvarchar](2000) NULL,
    [Response] [nvarchar](max) NULL,
    [Billing_UserId] [int] NOT NULL,
    [Planned_Start_Time] [datetime] NULL,
    [Input_FileId] [uniqueidentifier] NULL,
    [Output_FileId] [uniqueidentifier] NULL,
    [PRIORITY] [numeric](2, 0) NULL,
    [EXECUTE_SEQ] [numeric](2, 0) NULL,
    [View_Access] [numeric](1, 0) NULL,
    [Seeing] [numeric](1, 0) NULL,
 CONSTRAINT [PKBachTskQ] PRIMARY KEY CLUSTERED 
(
    [Id] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [Batch_Tasks_QueueData]
) ON [Batch_Tasks_QueueData] TEXTIMAGE_ON [Batch_Tasks_QueueData]
GO    
SET ANSI_PADDING OFF
GO
ALTER TABLE [dbo].[Batch_Tasks_Queue]  WITH NOCHECK ADD  CONSTRAINT [FK0_BtchTskQ_BlngUsr] FOREIGN KEY([Billing_UserId])
REFERENCES [dbo].[BILLING_USER] ([ID])
GO
ALTER TABLE [dbo].[Batch_Tasks_Queue] CHECK CONSTRAINT [FK0_BtchTskQ_BlngUsr]
GO
Hamid Fathi
źródło
Dyskusja na temat tego pytania została przeniesiona do tego pokoju czatu .
Paul White 9

Odpowiedzi:

15

Podsumowanie

Główne problemy to:

  • Wybór planu optymalizatora zakłada równomierny rozkład wartości.
  • Brak odpowiednich indeksów oznacza:
    • Skanowanie stołu jest jedyną opcją.
    • Łączenie jest naiwnym łączeniem zagnieżdżonych pętli, a nie łączeniem zagnieżdżonych indeksów . W naiwnym złączeniu predykaty łączenia są oceniane na złączeniu, a nie zepchnięte na wewnętrzną stronę złączenia.

Detale

Te dwa plany są zasadniczo bardzo podobne, chociaż wydajność może się bardzo różnić:

Planuj z dodatkowymi kolumnami

Biorąc tę ​​z dodatkowymi kolumnami, która nie kończy się w rozsądnym czasie:

Powolny plan

Ciekawe funkcje to:

  1. Top w węźle 0 ogranicza liczbę wierszy zwróconych do 100. Ustawia także cel wiersza dla optymalizatora, więc wszystko poniżej planu w planie jest wybierane, aby szybko zwrócić pierwsze 100 wierszy.
  2. Skanowanie w węźle 4 znajduje wiersze z tabeli, w których Start_Timewartość nie jest pusta, Statewynosi 3 lub 4 i Operation_Typejest jedną z wymienionych wartości. Tabela jest w pełni skanowana raz, a każdy wiersz jest testowany pod kątem wspomnianych predykatów. Tylko wiersze, które przejdą wszystkie testy, przechodzą do sortowania. Optymalizator szacuje, że zakwalifikuje się 38 283 wierszy.
  3. Sortuj w węźle 3 zużywa wszystkie wiersze ze skanowania w węźle 4 i sortuje je w kolejności Start_Time DESC. Jest to ostateczna kolejność prezentacji żądana przez zapytanie.
  4. Optymalizator szacuje, że 93 wiersze (w rzeczywistości 93.2791) będą musiały zostać odczytane z sortowania, aby cały plan zwrócił 100 wierszy (uwzględniając oczekiwany efekt sprzężenia).
  5. Oczekuje się, że łączenie zagnieżdżonych pętli w węźle 2 wykona swoje wewnętrzne wejście (dolna gałąź) 94 razy (w rzeczywistości 94,2791). Dodatkowy rząd jest wymagany przez wymianę równoległości stopu w węźle 1 ze względów technicznych.
  6. Skanuj w węźle 5 w pełni skanuje tabelę podczas każdej iteracji. Okazuje się, wiersze którym Start_Timejest różne od zera, a Statejest 3 lub 4. To oszacowanych produkować 400,875 rzędów na każdej iteracji. Ponad 94.2791 iteracji, całkowita liczba wierszy wynosi prawie 38 milionów.
  7. Łączenie zagnieżdżone w węźle 2 stosuje również predykaty łączenia. Sprawdza, czy Operation_Typepasuje, czy Start_Timez węzła 4 jest mniejszy niż Start_Timez węzła 5, czy Start_Timez węzła 5 jest mniejszy niż Finish_Timez węzła 4 i czy te dwie Idwartości się nie zgadzają.
  8. Zbierz strumienie (zatrzymaj wymianę równoległości) w węźle 1 scala uporządkowane strumienie z każdego wątku, aż powstanie 100 wierszy. Zachowujący porządek charakter scalania wielu strumieni jest tym, co wymaga dodatkowego wiersza wspomnianego w kroku 5.

Wielka nieefektywność występuje oczywiście w krokach 6 i 7 powyżej. Pełne skanowanie tabeli w węźle 5 dla każdej iteracji jest nawet nieco rozsądne, jeśli dzieje się to tylko 94 razy, jak przewiduje optymalizator. Zestaw około 38 milionów wierszy porównań w węźle 2 to także duży koszt.

Co istotne, oszacowanie celu rzędu rzędu 93/94 również jest prawdopodobnie błędne, ponieważ zależy od rozkładu wartości. Optymalizator zakłada równomierny rozkład przy braku bardziej szczegółowych informacji. Mówiąc prościej, oznacza to, że jeśli oczekuje się, że kwalifikuje się 1% wierszy w tabeli, optymalizator powoduje, że aby znaleźć 1 pasujący wiersz, musi odczytać 100 wierszy.

Jeśli uruchomisz tę kwerendę do końca (co może zająć bardzo dużo czasu), najprawdopodobniej odkryjesz, że z Sortowania trzeba odczytać wiele więcej niż 93/94 wierszy, aby ostatecznie wygenerować 100 wierszy. W najgorszym przypadku setny rząd zostałby znaleziony przy użyciu ostatniego rzędu z sortowania. Zakładając, że oszacowanie optymalizatora w węźle 4 jest prawidłowe, oznacza to uruchomienie skanowania w węźle 5 38 284 razy, w sumie około 15 miliardów wierszy. Może być więcej, jeśli szacunki skanowania są również wyłączone.

Ten plan wykonania zawiera również ostrzeżenie o braku indeksu:

/*
The Query Processor estimates that implementing the following index
could improve the query cost by 72.7096%.

WARNING: This is only an estimate, and the Query Processor is making
this recommendation based solely upon analysis of this specific query.
It has not considered the resulting index size, or its workload-wide
impact, including its impact on INSERT, UPDATE, DELETE performance.
These factors should be taken into account before creating this index.
*/

CREATE NONCLUSTERED INDEX [<Name of Missing Index, sysname,>]
ON [dbo].[Batch_Tasks_Queue] ([Operation_Type],[State],[Start_Time])
INCLUDE ([Id],[Parameters])

Optymalizator ostrzega o tym, że dodanie indeksu do tabeli poprawiłoby wydajność.

Planuj bez dodatkowych kolumn

Mniej powolny plan

Zasadniczo jest to dokładnie ten sam plan, co poprzedni, z dodaniem szpuli indeksu w węźle 6 i filtra w węźle 5. Ważne różnice to:

  1. Szpula indeksu w węźle 6 jest chętną szpulą. Z niecierpliwością zużywa wynik skanowania poniżej i tworzy tymczasowy indeks z kluczem Operation_Typei Start_Time, z Idkolumną bez klucza.
  2. Łączenie zagnieżdżonych pętli w węźle 2 jest teraz złączeniem indeksu. Nie łączą orzeczniki ocenia się tu zamiast aktualnych wartości dla każdej iteracji, Operation_Type, Start_Time, Finish_Timei Idze skanowania w węźle 4 są przemieszczane do gałęzi wewnętrzna po stronie zewnętrznej, jak odniesienia.
  3. Skanowanie w węźle 7 jest wykonywane tylko raz.
  4. Bufor indeksu w węźle 6 wyszukuje wiersze z indeksu tymczasowego, w którym Operation_Typeodpowiada bieżącej zewnętrznej wartości odniesienia, i Start_Timeznajduje się w zakresie określonym przez odniesienia zewnętrzne Start_Timei Finish_Time.
  5. Filtr w węźle 5 testuje Idwartości z bufora indeksu pod kątem nierówności względem bieżącej zewnętrznej wartości odniesienia Id.

Najważniejsze ulepszenia to:

  • Skanowanie strony wewnętrznej jest wykonywane tylko raz
  • Tymczasowy indeks na ( Operation_Type, Start_Time) z Iddołączoną kolumną pozwala na łączenie zagnieżdżonych pętli indeksu. Indeks służy do wyszukiwania pasujących wierszy na każdej iteracji zamiast skanowania całego stołu za każdym razem.

Tak jak poprzednio, optymalizator zawiera ostrzeżenie o brakującym indeksie:

/*
The Query Processor estimates that implementing the following index
could improve the query cost by 24.1475%.

WARNING: This is only an estimate, and the Query Processor is making
this recommendation based solely upon analysis of this specific query.
It has not considered the resulting index size, or its workload-wide
impact, including its impact on INSERT, UPDATE, DELETE performance.
These factors should be taken into account before creating this index.
*/

CREATE NONCLUSTERED INDEX [<Name of Missing Index, sysname,>]
ON [dbo].[Batch_Tasks_Queue] ([State],[Start_Time])
INCLUDE ([Id],[Operation_Type])
GO

Wniosek

Plan bez dodatkowych kolumn jest szybszy, ponieważ optymalizator postanowił utworzyć dla ciebie tymczasowy indeks.

Plan z dodatkowymi kolumnami spowodowałby, że tymczasowy indeks byłby droższy w budowie. [ParametersKolumny] to nvarchar(2000), które sumują się z 4000 bajtów do każdej linii wskaźnika. Dodatkowy koszt jest wystarczający, aby przekonać optymalizator, że utworzenie tymczasowego indeksu przy każdym wykonaniu nie zwróci się.

Optymalizator ostrzega w obu przypadkach, że lepszym rozwiązaniem byłby indeks stały. Idealna kompozycja indeksu zależy od szerszego obciążenia pracą. W przypadku tego konkretnego zapytania sugerowane indeksy są rozsądnym punktem wyjścia, ale należy zrozumieć korzyści i koszty z tym związane.

Rekomendacje

Dla tego zapytania korzystny byłby szeroki zakres możliwych indeksów. Ważne jest to, że potrzebny jest jakiś indeks nieklastrowany. Na podstawie dostarczonych informacji rozsądnym moim zdaniem byłby:

CREATE NONCLUSTERED INDEX i1
ON dbo.Batch_Tasks_Queue (Start_Time DESC)
INCLUDE (Operation_Type, [State], Finish_Time);

Kusiłbym również, by trochę lepiej zorganizować zapytanie i opóźnić wyszukiwanie szerokich [Parameters]kolumn w indeksie klastrowym, aż do znalezienia 100 pierwszych wierszy (używając Idjako klucza):

SELECT TOP (100)
    BTQ1.id,
    BTQ2.id,
    BTQ3.[Parameters],
    BTQ4.[Parameters]
FROM dbo.Batch_Tasks_Queue AS BTQ1
JOIN dbo.Batch_Tasks_Queue AS BTQ2 WITH (FORCESEEK)
    ON BTQ2.Operation_Type = BTQ1.Operation_Type
    AND BTQ2.Start_Time > BTQ1.Start_Time
    AND BTQ2.Start_Time < BTQ1.Finish_Time
    AND BTQ2.id != BTQ1.id
    -- Look up the [Parameters] values
JOIN dbo.Batch_Tasks_Queue AS BTQ3
    ON BTQ3.Id = BTQ1.Id
JOIN dbo.Batch_Tasks_Queue AS BTQ4
    ON BTQ4.Id = BTQ2.Id
WHERE
    BTQ1.[State] IN (3, 4)
    AND BTQ2.[State] IN (3, 4)
    AND BTQ1.Operation_Type NOT IN (23, 24, 25, 26, 27, 28, 30)
    AND BTQ2.Operation_Type NOT IN (23, 24, 25, 26, 27, 28, 30)
    -- These predicates are not strictly needed
    AND BTQ1.Start_Time IS NOT NULL
    AND BTQ2.Start_Time IS NOT NULL
ORDER BY
    BTQ1.Start_Time DESC;

Tam, gdzie [Parameters]kolumny nie są potrzebne, zapytanie można uprościć:

SELECT TOP (100)
    BTQ1.id,
    BTQ2.id
FROM dbo.Batch_Tasks_Queue AS BTQ1
JOIN dbo.Batch_Tasks_Queue AS BTQ2 WITH (FORCESEEK)
    ON BTQ2.Operation_Type = BTQ1.Operation_Type
    AND BTQ2.Start_Time > BTQ1.Start_Time
    AND BTQ2.Start_Time < BTQ1.Finish_Time
    AND BTQ2.id != BTQ1.id
WHERE
    BTQ1.[State] IN (3, 4)
    AND BTQ2.[State] IN (3, 4)
    AND BTQ1.Operation_Type NOT IN (23, 24, 25, 26, 27, 28, 30)
    AND BTQ2.Operation_Type NOT IN (23, 24, 25, 26, 27, 28, 30)
    AND BTQ1.Start_Time IS NOT NULL
    AND BTQ2.Start_Time IS NOT NULL
ORDER BY
    BTQ1.Start_Time DESC;

FORCESEEKWskazówką jest to, aby pomóc zapewnić optymalizator wybiera indeksowanego zagnieżdżone pętle zaplanować (istnieje pokusa, oparte na kosztach dla optymalizatora, aby wybrać skrót lub (many-many) scalające inaczej, co nie ma tendencję do pracy również z tego typu zapytanie w praktyce. Oba kończą się dużymi resztkami; wiele elementów na wiadro w przypadku skrótu i ​​wiele przewinięć do scalenia).

Alternatywny

Gdyby zapytanie (w tym jego określone wartości) miało szczególne znaczenie dla wydajności odczytu, zamiast tego wziąłbym pod uwagę dwa przefiltrowane indeksy:

CREATE NONCLUSTERED INDEX i1
ON dbo.Batch_Tasks_Queue (Start_Time DESC)
INCLUDE (Operation_Type, [State], Finish_Time)
WHERE 
    Start_Time IS NOT NULL
    AND [State] IN (3, 4)
    AND Operation_Type <> 23
    AND Operation_Type <> 24
    AND Operation_Type <> 25
    AND Operation_Type <> 26
    AND Operation_Type <> 27
    AND Operation_Type <> 28
    AND Operation_Type <> 30;

CREATE NONCLUSTERED INDEX i2
ON dbo.Batch_Tasks_Queue (Operation_Type, [State], Start_Time)
WHERE 
    Start_Time IS NOT NULL
    AND [State] IN (3, 4)
    AND Operation_Type <> 23
    AND Operation_Type <> 24
    AND Operation_Type <> 25
    AND Operation_Type <> 26
    AND Operation_Type <> 27
    AND Operation_Type <> 28
    AND Operation_Type <> 30;

W przypadku zapytania, które nie wymaga [Parameters]kolumny, plan szacunkowy z wykorzystaniem przefiltrowanych indeksów wynosi:

Prosty filtrowany plan indeksu

Skanowanie indeksu automatycznie zwraca wszystkie kwalifikujące się wiersze bez oceny jakichkolwiek dodatkowych predykatów. Dla każdej iteracji łączenia zagnieżdżonych pętli indeksu przeszukiwanie indeksu wykonuje dwie operacje wyszukiwania:

  1. Prefiks wyszukiwania pasuje do Operation_Typei State= 3, a następnie szuka zakresu Start_Timewartości, a predykat resztkowy o Idnierówności.
  2. Prefiks wyszukiwania pasuje do Operation_Typei State= 4, a następnie szuka zakresu Start_Timewartości, a predykat resztkowy na Idnierówności.

Tam, gdzie [Parameters]kolumna jest potrzebna, plan zapytań po prostu dodaje maksymalnie 100 wyszukiwań singletonów dla każdej tabeli:

Filtrowany plan indeksu z dodatkowymi kolumnami

Na koniec należy rozważyć użycie wbudowanych standardowych typów liczb całkowitych zamiast, gdy ma numericto zastosowanie.

Paul White 9
źródło
-2

Utwórz następujący indeks:

create index Batch_Tasks_Queue_ix_Start_Time on Batch_Tasks_Queue(Start_Time);
David Markודו Markovitz
źródło
Próbowałeś tego? Przeprowadziłem kilka testów i to zrobiło ogromną różnicę.
David Markודו Markovitz,