Die Abfrage reagiert nicht, wenn zwei Spalten hinzugefügt werden

9

Wenn ich meiner Auswahl zwei Spalten hinzufüge, antwortet die Abfrage nicht. Der Spaltentyp ist nvarchar(2000). Es ist ein bisschen ungewöhnlich.

  • Die SQL Server-Version ist 2014.
  • Es gibt nur einen Primärindex.
  • Der gesamte Datensatz besteht nur aus 1000 Zeilen.

Hier ist der Ausführungsplan vor ( XML-Showplan ):

Geben Sie hier die Bildbeschreibung ein

Ausführungsplan nach ( XML-Showplan ):

Geben Sie hier die Bildbeschreibung ein

Hier ist die Abfrage:

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

Die Gesamtzahl der Ergebnisse beträgt 17 Zeilen. Die schmutzigen Daten (Nolock-Hinweis) sind nicht wichtig.

Hier ist die Tabellenstruktur:

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
quelle
Die Diskussion zu dieser Frage wurde in diesen Chatraum verschoben .
Paul White 9

Antworten:

15

Zusammenfassung

Die Hauptprobleme sind:

  • Die Planauswahl des Optimierers setzt eine gleichmäßige Werteverteilung voraus.
  • Ein Mangel an geeigneten Indizes bedeutet:
    • Das Scannen der Tabelle ist die einzige Option.
    • Der Join ist ein naiver Join für verschachtelte Schleifen und kein Join für verschachtelte Indexschleifen . Bei einem naiven Join werden die Join-Prädikate beim Join ausgewertet, anstatt auf die Innenseite des Joins gedrückt zu werden.

Einzelheiten

Die beiden Pläne sind grundsätzlich ziemlich ähnlich, obwohl die Leistung sehr unterschiedlich sein kann:

Planen Sie mit den zusätzlichen Spalten

Nehmen Sie zuerst die mit den zusätzlichen Spalten, die nicht in angemessener Zeit abgeschlossen sind:

Langsamer Plan

Die interessanten Funktionen sind:

  1. Die Spitze am Knoten 0 begrenzt die zurückgegebenen Zeilen auf 100. Außerdem wird ein Zeilenziel für das Optimierungsprogramm festgelegt, sodass alles darunter im Plan ausgewählt wird, um die ersten 100 Zeilen schnell zurückzugeben.
  2. Der Scan am Knoten 4 findet Zeilen aus der Tabelle, in denen der Wert Start_Timenicht null, State3 oder 4 ist und Operation_Typeeiner der aufgelisteten Werte ist. Die Tabelle wird einmal vollständig gescannt, wobei jede Zeile gegen die genannten Prädikate getestet wird. Nur Zeilen, die alle Tests bestehen, werden an die Sortierung weitergeleitet. Der Optimierer schätzt, dass 38.283 Zeilen qualifiziert sind.
  3. Die Sortierung an Knoten 3 belegt alle Zeilen aus dem Scan an Knoten 4 und sortiert sie in der Reihenfolge von Start_Time DESC. Dies ist die endgültige Präsentationsreihenfolge, die von der Abfrage angefordert wird.
  4. Der Optimierer schätzt, dass 93 Zeilen (tatsächlich 93,2791) aus der Sortierung gelesen werden müssen, damit der gesamte Plan 100 Zeilen zurückgibt (unter Berücksichtigung des erwarteten Effekts des Joins).
  5. Es wird erwartet, dass der Nested Loops-Join am Knoten 2 seine innere Eingabe (den unteren Zweig) 94 Mal ausführt (tatsächlich 94.2791). Die zusätzliche Zeile wird aus technischen Gründen für den Stopp-Parallelitätsaustausch am Knoten 1 benötigt.
  6. Der Scan am Knoten 5 scannt die Tabelle bei jeder Iteration vollständig. Es werden Zeilen gefunden, die Start_Timenicht null sind und State3 oder 4 betragen. Es wird geschätzt, dass bei jeder Iteration 400.875 Zeilen erzeugt werden. Über 94.2791 Iterationen beträgt die Gesamtzahl der Zeilen fast 38 Millionen.
  7. Der Join für verschachtelte Schleifen am Knoten 2 wendet auch die Join-Prädikate an. Es wird überprüft, ob Operation_TypeÜbereinstimmungen vorliegen, ob der Start_Timevon Knoten 4 kleiner als der Start_Timevon Knoten 5 ist, ob der Start_Timevon Knoten 5 kleiner als der Finish_Timevon Knoten 4 ist und ob die beiden IdWerte nicht übereinstimmen.
  8. Die Gather Streams (Stop Parallelism Exchange) am Knoten 1 führen die geordneten Streams von jedem Thread zusammen, bis 100 Zeilen erzeugt wurden. Die auftragserhaltende Natur der Zusammenführung über mehrere Streams erfordert die in Schritt 5 erwähnte zusätzliche Zeile.

Die große Ineffizienz liegt offensichtlich in den obigen Schritten 6 und 7. Das vollständige Scannen der Tabelle am Knoten 5 für jede Iteration ist nur geringfügig sinnvoll, wenn dies nur 94 Mal geschieht, wie es der Optimierer vorhersagt. Die ~ 38 Millionen Vergleiche pro Zeile am Knoten 2 sind ebenfalls mit hohen Kosten verbunden.

Entscheidend ist auch, dass die Schätzung des Ziels der Zeilenreihe 93/94 wahrscheinlich falsch ist, da sie von der Verteilung der Werte abhängt. Der Optimierer geht von einer gleichmäßigen Verteilung aus, wenn keine detaillierteren Informationen vorliegen. In einfachen Worten bedeutet dies, dass, wenn erwartet wird, dass 1% der Zeilen in der Tabelle qualifiziert sind, der Optimierer Gründe dafür hat, dass er 100 Zeilen lesen muss, um eine übereinstimmende Zeile zu finden.

Wenn Sie diese Abfrage vollständig ausführen (was sehr lange dauern kann), werden Sie höchstwahrscheinlich feststellen, dass viel mehr als 93/94 Zeilen aus der Sortierung gelesen werden mussten, um schließlich 100 Zeilen zu erstellen. Im schlimmsten Fall wird die 100. Zeile anhand der letzten Zeile aus der Sortierung gefunden. Unter der Annahme, dass die Schätzung des Optimierers auf Knoten 4 korrekt ist, bedeutet dies, dass der Scan auf Knoten 5 38.284 Mal ausgeführt wird, was insgesamt etwa 15 Milliarden Zeilen entspricht. Es könnte mehr sein, wenn die Scan-Schätzungen ebenfalls deaktiviert sind.

Dieser Ausführungsplan enthält auch eine fehlende Indexwarnung:

/*
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])

Das Optimierungsprogramm weist Sie darauf hin, dass das Hinzufügen eines Index zur Tabelle die Leistung verbessern würde.

Planen Sie ohne die zusätzlichen Spalten

Weniger langsamer Plan

Dies ist im Wesentlichen derselbe Plan wie der vorherige, mit der Hinzufügung der Indexspule an Knoten 6 und des Filters an Knoten 5. Die wichtigen Unterschiede sind:

  1. Die Indexspule am Knoten 6 ist eine eifrige Spule. Er verbraucht mit Spannung das Ergebnis der Scan darunter, und baut einen temporären Index verkeilt Operation_Typeund Start_Timemit Idals Nicht-Schlüsselspalte.
  2. Der Nested Loops Join am Knoten 2 ist jetzt ein Index Join. Keine Joinvergleichselemente werden hier ausgewertet, sondern die per-Iteration aktuellen Werte von Operation_Type, Start_Time, Finish_Time, und Idaus der Abtastung an Knoten 4 weitergeleitet werden an den Innenseiten - Zweig als äußere Referenzen.
  3. Der Scan am Knoten 7 wird nur einmal durchgeführt.
  4. Der Index Spool am Knoten 6 sucht Zeilen aus dem temporären Index wo Operation_Typeentspricht den aktuellen äußeren Referenzwert ist , und das Start_Timewird in dem durch die definierten Bereich Start_Timeund Finish_Timeäußern Referenzen.
  5. Der Filter am Knoten 5 testet IdWerte aus der Indexspule auf Ungleichheit mit dem aktuellen äußeren Referenzwert von Id.

Die wichtigsten Verbesserungen sind:

  • Der Innenseiten-Scan wird nur einmal durchgeführt
  • Ein temporärer Index für ( Operation_Type, Start_Time) mit Idals eingeschlossene Spalte ermöglicht die Verknüpfung eines Index verschachtelter Schleifen. Der Index wird verwendet, um bei jeder Iteration nach übereinstimmenden Zeilen zu suchen, anstatt jedes Mal die gesamte Tabelle zu scannen.

Nach wie vor enthält das Optimierungsprogramm eine Warnung vor einem fehlenden Index:

/*
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

Fazit

Der Plan ohne die zusätzlichen Spalten ist schneller, da der Optimierer einen temporären Index für Sie erstellt hat.

Der Plan mit den zusätzlichen Spalten würde die Erstellung des temporären Index verteuern. Die [ParametersSpalte] gibt nvarchar(2000)bis zu 4000 Byte für jede Zeile des Index an. Die zusätzlichen Kosten reichen aus, um den Optimierer davon zu überzeugen, dass sich das Erstellen des temporären Index für jede Ausführung nicht auszahlt.

Der Optimierer warnt in beiden Fällen, dass ein permanenter Index eine bessere Lösung wäre. Die ideale Zusammensetzung des Index hängt von Ihrer Arbeitsbelastung ab. Für diese spezielle Abfrage sind die vorgeschlagenen Indizes ein vernünftiger Ausgangspunkt, aber Sie sollten die damit verbundenen Vorteile und Kosten verstehen.

Empfehlung

Eine breite Palette möglicher Indizes wäre für diese Abfrage von Vorteil. Der wichtige Aspekt ist, dass eine Art nicht gruppierter Index benötigt wird. Aus den bereitgestellten Informationen wäre meiner Meinung nach ein angemessener Index:

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

Ich wäre auch versucht, die Abfrage etwas besser zu organisieren und das Nachschlagen der breiten [Parameters]Spalten im Clustered-Index zu verzögern, bis die 100 besten Zeilen gefunden wurden ( Idals Schlüssel):

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;

Wenn die [Parameters]Spalten nicht benötigt werden, kann die Abfrage vereinfacht werden, um:

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;

Der FORCESEEKHinweis soll sicherstellen, dass das Optimierungsprogramm einen Plan für indizierte verschachtelte Schleifen auswählt (es besteht eine kostenbasierte Versuchung für das Optimierungsprogramm, ansonsten einen Hash oder (viele, viele) Zusammenführungsverknüpfungen auszuwählen, was bei dieser Art von nicht gut funktioniert Abfrage in der Praxis. Beide haben große Residuen (viele Elemente pro Bucket im Fall des Hash und viele Rückspulen für die Zusammenführung).

Alternative

Wenn die Abfrage (einschließlich ihrer spezifischen Werte) für die Leseleistung besonders kritisch wäre, würde ich stattdessen zwei gefilterte Indizes in Betracht ziehen:

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;

Für die Abfrage, für die die [Parameters]Spalte nicht benötigt wird, lautet der geschätzte Plan unter Verwendung der gefilterten Indizes:

Einfacher gefilterter Indexplan

Der Index-Scan gibt automatisch alle qualifizierenden Zeilen zurück, ohne zusätzliche Prädikate auszuwerten. Für jede Iteration des Joins mit verschachtelten Indexschleifen führt die Indexsuche zwei Suchoperationen aus:

  1. Ein Such Prefix - Match auf Operation_Typeund State= 3, dann den Bereich der Suche nach Start_TimeWerten, Rest Prädikat auf der IdUngleichheit.
  2. Eine Suchpräfixübereinstimmung mit Operation_Typeund State= 4, dann Suche nach dem Wertebereich Start_Time, verbleibendes Prädikat für die IdUngleichung.

Wenn die [Parameters]Spalte benötigt wird, fügt der Abfrageplan einfach maximal 100 Singleton-Lookups für jede Tabelle hinzu:

Gefilterter Indexplan mit zusätzlichen Spalten

Abschließend sollten Sie in Betracht ziehen, die integrierten Standard-Integer-Typen anstelle der numericggf. zu verwenden.

Paul White 9
quelle
-2

Bitte erstellen Sie folgenden Index:

create index Batch_Tasks_Queue_ix_Start_Time on Batch_Tasks_Queue(Start_Time);
David דודו Markovitz
quelle
Hast du es versucht? Ich habe einige Tests durchgeführt und es hat einen großen Unterschied gemacht.
David Markovitz