Ist die WHERE-JOIN-ORDER- (SELECT) -Regel für die Indexspaltenreihenfolge falsch?


9

Ich versuche, diese (Unter-) Abfrage zu verbessern, die Teil einer größeren Abfrage ist:

select SUM(isnull(IP.Q, 0)) as Q, 
        IP.OPID 
    from IP
        inner join I
        on I.ID = IP.IID
    where 
        IP.Deleted=0 and
        (I.Status > 0 AND I.Status <= 19) 
    group by IP.OPID

Sentry Plan Explorer wies auf einige relativ teure Key Lookups für die Tabelle dbo hin. [I], die von der obigen Abfrage durchgeführt wurden.

Tabelle dbo.I.

    CREATE TABLE [dbo].[I] (
  [ID]  UNIQUEIDENTIFIER NOT NULL,
  [OID]  UNIQUEIDENTIFIER NOT NULL,
  []  UNIQUEIDENTIFIER NOT NULL,
  []  UNIQUEIDENTIFIER NOT NULL,
  [] UNIQUEIDENTIFIER NULL,
  []  UNIQUEIDENTIFIER NOT NULL,
  []  CHAR (3) NOT NULL,
  []  CHAR (3)  DEFAULT ('EUR') NOT NULL,
  []  DECIMAL (18, 8) DEFAULT ((1)) NOT NULL,
  [] CHAR (10)  NOT NULL,
  []  DECIMAL (18, 8) DEFAULT ((1)) NOT NULL,
  []  DATETIME  DEFAULT (getdate()) NOT NULL,
  []  VARCHAR (35) NULL,
  [] NVARCHAR (100) NOT NULL,
  []  NVARCHAR (100) NULL,
  []  NTEXT  NULL,
  []  NTEXT  NULL,
  []  NTEXT  NULL,
  []  NTEXT  NULL,
  [Status]  INT DEFAULT ((0)) NOT NULL,
  []  DECIMAL (18, 2)  NOT NULL,
  [] DECIMAL (18, 2)  NOT NULL,
  [] DECIMAL (18, 2)  NOT NULL,
  [] DATETIME DEFAULT (getdate()) NULL,
  []  DATETIME NULL,
  []  NTEXT  NULL,
  []  NTEXT  NULL,
  [] TINYINT  DEFAULT ((0)) NOT NULL,
  []  DATETIME NULL,
  []  VARCHAR (50) NULL,
  []  DATETIME  DEFAULT (getdate()) NOT NULL,
  []  VARCHAR (50) NOT NULL,
  []  DATETIME NULL,
  []  VARCHAR (50) NULL,
  []  ROWVERSION NOT NULL,
  []  DATETIME NULL,
  []  INT  NULL,
  [] TINYINT DEFAULT ((0)) NOT NULL,
  [] UNIQUEIDENTIFIER NULL,
  []  TINYINT DEFAULT ((0)) NOT NULL,
  []  TINYINT  DEFAULT ((0)) NOT NULL,
  [] NVARCHAR (50)  NULL,
  [] TINYINT DEFAULT ((0)) NOT NULL,
  []  UNIQUEIDENTIFIER NULL,
  [] UNIQUEIDENTIFIER NULL,
  []  TINYINT  DEFAULT ((0)) NOT NULL,
  []  TINYINT DEFAULT ((0)) NOT NULL,
  []  UNIQUEIDENTIFIER NULL,
  []  DECIMAL (18, 2)  NULL,
  []  DECIMAL (18, 2)  NULL,
  [] DECIMAL (18, 2)  DEFAULT ((0)) NOT NULL,
  [] UNIQUEIDENTIFIER NULL,
  [] DATETIME NULL,
  [] DATETIME NULL,
  []  VARCHAR (35) NULL,
  [] DECIMAL (18, 2)  DEFAULT ((0)) NOT NULL,
  CONSTRAINT [PK_I] PRIMARY KEY NONCLUSTERED ([ID] ASC) WITH (FILLFACTOR = 90),
  CONSTRAINT [FK_I_O] FOREIGN KEY ([OID]) REFERENCES [dbo].[O] ([ID]),
  CONSTRAINT [FK_I_Status] FOREIGN KEY ([Status]) REFERENCES [dbo].[T_Status] ([Status])
);                  


GO
CREATE CLUSTERED INDEX [CIX_Invoice]
  ON [dbo].[I]([OID] ASC) WITH (FILLFACTOR = 90);

Tabelle dbo.IP.

CREATE TABLE [dbo].[IP] (
 [ID] UNIQUEIDENTIFIER DEFAULT (newid()) NOT NULL,
 [IID] UNIQUEIDENTIFIER NOT NULL,
 [OID] UNIQUEIDENTIFIER NOT NULL,
 [Deleted] TINYINT DEFAULT ((0)) NOT NULL,
 [] UNIQUEIDENTIFIER NULL,
 [] UNIQUEIDENTIFIER NULL,
 [] UNIQUEIDENTIFIER NULL,
 [] UNIQUEIDENTIFIER NULL,
 [] UNIQUEIDENTIFIER NULL,
 []UNIQUEIDENTIFIER NULL,
 [] UNIQUEIDENTIFIER NULL,
 [] UNIQUEIDENTIFIER NULL,
 [] INT NOT NULL,
 [] VARCHAR (35) NULL,
 [] NVARCHAR (100) NOT NULL,
 [] NTEXT NULL,
 [] DECIMAL (18, 4) DEFAULT ((0)) NOT NULL,
 [] NTEXT NULL,
 [] NTEXT NULL,
 [] DECIMAL (18, 4) DEFAULT ((0)) NOT NULL,
 [] DECIMAL (18, 4) DEFAULT ((0)) NOT NULL,
 [] DECIMAL (4, 2) NOT NULL,
 [] INT DEFAULT ((1)) NOT NULL,
 [] DATETIME DEFAULT (getdate()) NOT NULL,
 [] VARCHAR (50) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL,
 [] DATETIME NULL,
 [] VARCHAR (50) COLLATE SQL_Latin1_General_CP1_CI_AS NULL,
 [] ROWVERSION NOT NULL,
 [] INT DEFAULT ((1)) NOT NULL,
 [] DATETIME NULL,
 [] UNIQUEIDENTIFIER NULL,
 [] DECIMAL (18, 4) DEFAULT ((1)) NOT NULL,
 [] DECIMAL (18, 4) DEFAULT ((1)) NOT NULL,
 [] INT DEFAULT ((0)) NOT NULL,
 [] UNIQUEIDENTIFIER NULL,
 [] UNIQUEIDENTIFIER NULL,
 [] UNIQUEIDENTIFIER NULL,
 [] UNIQUEIDENTIFIER NULL,
 []UNIQUEIDENTIFIER NULL,
 []NVARCHAR (35) NULL,
 [] VARCHAR (35) NULL,
 [] NVARCHAR (35) NULL,
 [] NVARCHAR (35) NULL,
 [] NVARCHAR (35) NULL,
 [] NVARCHAR (35) NULL,
 [] NVARCHAR (35) NULL,
 [] NVARCHAR (35) NULL,
 [] UNIQUEIDENTIFIER NULL,
 [] VARCHAR (12) NULL,
 [] VARCHAR (4) NULL,
 [] NVARCHAR (50) NULL,
 [] NVARCHAR (50) NULL,
 [] VARCHAR (35) NULL,
 [] UNIQUEIDENTIFIER NULL,
 [] UNIQUEIDENTIFIER NULL,
 [] TINYINT DEFAULT ((0)) NOT NULL,
 [] UNIQUEIDENTIFIER NULL,
 [] TINYINT DEFAULT ((0)) NOT NULL,
 [] TINYINT DEFAULT ((0)) NOT NULL,
 [] NVARCHAR (50) NULL,
 [] TINYINT DEFAULT ((0)) NOT NULL,
 [] DECIMAL (18, 2) NULL,
 []TINYINT DEFAULT ((1)) NOT NULL,
 [] UNIQUEIDENTIFIER NULL,
 [] UNIQUEIDENTIFIER NULL,
 [] UNIQUEIDENTIFIER NULL,
 [] TINYINT DEFAULT ((1)) NOT NULL,
 CONSTRAINT [PK_IP] PRIMARY KEY NONCLUSTERED ([ID] ASC) WITH (FILLFACTOR = 90),
 CONSTRAINT [FK_IP_I] FOREIGN KEY ([IID]) REFERENCES [dbo].[I] ([ID]) ON DELETE CASCADE NOT FOR REPLICATION,
 CONSTRAINT [FK_IP_XType] FOREIGN KEY ([XType]) REFERENCES [dbo].[xTYPE] ([Value]) NOT FOR REPLICATION
);

GO
CREATE CLUSTERED INDEX [IX_IP_CLUST]
 ON [dbo].[IP]([IID] ASC) WITH (FILLFACTOR = 90);

Die Tabelle "I" hat ungefähr 100.000 Zeilen, der Clustered-Index hat 9.386 Seiten.
Die Tabelle IP ist die "untergeordnete" Tabelle von I und hat ungefähr 175.000 Zeilen.

Ich habe versucht, einen neuen Index gemäß der Regel für die Reihenfolge der Indexspalten hinzuzufügen: "WHERE-JOIN-ORDER- (SELECT)"

https://www.mssqltips.com/sqlservertutorial/3208/use-where-join-orderby-select-column-order-when-creating-indexes/

So adressieren Sie die Schlüsselsuche und erstellen eine Indexsuche:

CREATE NONCLUSTERED INDEX [IX_I_Status_1]
    ON [dbo].[Invoice]([Status], [ID])

Die extrahierte Abfrage verwendete diesen Index sofort. Aber die ursprüngliche größere Abfrage, zu der es gehört, hat es nicht getan. Es wurde nicht einmal verwendet, als ich es zur Verwendung von WITH (INDEX (IX_I_Status_1)) zwang.

Nach einer Weile entschied ich mich, einen anderen neuen Index auszuprobieren und änderte die Reihenfolge der indizierten Spalten:

CREATE NONCLUSTERED INDEX [IX_I_Status_2]
    ON [dbo].[Invoice]([ID], [Status])

WOHA! Dieser Index wurde von der extrahierten Abfrage und auch von der größeren Abfrage verwendet!

Dann habe ich die E / A-Statistiken der extrahierten Abfragen verglichen, indem ich sie zur Verwendung von [IX_I_Status_1] und [IX_I_Status_2] gezwungen habe:

Ergebnisse [IX_I_Status_1]:

Table 'I'. Scan count 5, logical reads 636, physical reads 16, read-ahead reads 574
Table 'IP'. Scan count 5, logical reads 1134, physical reads 11, read-ahead reads 1040
Table 'Worktable'. Scan count 0, logical reads 0, physical reads 0, read-ahead reads 0

Ergebnisse [IX_I_Status_2]:

Table 'I'. Scan count 1, logical reads 615, physical reads 6, read-ahead reads 631
Table 'IP'. Scan count 1, logical reads 1024, physical reads 5, read-ahead reads 1040
Table 'Worktable'. Scan count 0, logical reads 0, physical reads 0,  read-ahead reads 0

OK, ich könnte verstehen, dass die Mega-Large-Monster-Abfrage möglicherweise einfach zu komplex ist, um SQL Server den idealen Ausführungsplan abzufangen und möglicherweise meinen neuen Index zu verfehlen. Ich verstehe jedoch nicht, warum der Index [IX_I_Status_2] für die Abfrage geeigneter und effizienter zu sein scheint.

Da die Abfrage zuerst Tabelle I nach Spalte STATUS filtert und dann mit Tabelle IP verknüpft, verstehe ich nicht, warum [IX_I_Status_2] besser ist und von SQL Server anstelle von [IX_I_Status_1] verwendet wird.


Ja, dieser Index wird verwendet, wenn die Filterkriterien erfüllt sind. Es führt einen Index-Scan durch (wie bei IX_I_Status_2) und speichert im Vergleich dazu 1 physischen Lesevorgang. Ich musste diesen Index jedoch "einschließen (Status)", da der Status in der Ausgabe enthalten ist und zuvor erneut nachgeschlagen wurde.
Magier

Witzige Randnotiz: Nachdem ich mich jetzt für den besten Index beworben habe, konnte ich herausfinden ([IX_I_Status_2]) und die Abfrage erneut ausführen. Jetzt erhalte ich einen fehlenden Indexvorschlag: CREATE NONCLUSTERED INDEX [<Name des fehlenden Index, Systemname,>] ON [ dbo]. [I] ([Status]) INCLUDE ([ID]) Dies ist ein schlechter Vorschlag und verringert die Leistung der Abfrage. TY SQL Server :)
Magier

Antworten:


19

Ist die WHERE-JOIN-ORDER- (SELECT) -Regel für die Indexspaltenreihenfolge falsch?

Zumindest ist es ein unvollständiger und möglicherweise irreführender Rat (ich habe mir nicht die Mühe gemacht, den ganzen Artikel zu lesen). Wenn Sie Inhalte im Internet lesen möchten (einschließlich dieser), sollten Sie Ihr Vertrauensniveau entsprechend der Kenntnis und dem Vertrauen des Autors anpassen, dies jedoch immer selbst überprüfen.

Abhängig vom genauen Szenario gibt es eine Reihe von "Faustregeln" für die Erstellung von Indizes, aber keine ist wirklich ein guter Ersatz, um die Kernprobleme für sich selbst zu verstehen. Informieren Sie sich über die Implementierung von Indizes und Ausführungsplanoperatoren in SQL Server, führen Sie einige Übungen durch und erhalten Sie ein solides Verständnis dafür, wie Indizes verwendet werden können, um Ausführungspläne effizienter zu gestalten. Es gibt keine wirksame Abkürzung, um dieses Wissen und diese Erfahrung zu erlangen.

Im Allgemeinen kann ich sagen, dass Ihre Indizes meistens Spalten enthalten sollten, die zuerst für Gleichheitstests verwendet werden, wobei Ungleichungen zuletzt angezeigt werden und / oder von einem Filter im Index bereitgestellt werden. Dies ist keine vollständige Aussage, da Indizes auch die Reihenfolge angeben können. Dies kann in bestimmten Situationen nützlicher sein, als direkt nach einem oder mehreren Schlüsseln zu suchen. Beispielsweise kann die Reihenfolge verwendet werden, um eine Sortierung zu vermeiden, die Kosten einer physischen Verknüpfungsoption wie der Zusammenführungsverbindung zu senken, ein Stream-Aggregat zu aktivieren, die ersten qualifizierenden Zeilen schnell zu finden ... und so weiter.

Ich bin hier etwas vage, weil die Auswahl der idealen Indizes für eine Abfrage von so vielen Faktoren abhängt - dies ist ein sehr breites Thema.

Auf jeden Fall ist es nicht ungewöhnlich, widersprüchliche Signale für die "besten" Indizes in einer Abfrage zu finden. Beispielsweise möchte Ihr Join-Prädikat, dass Zeilen für einen Zusammenführungs-Join in eine Richtung geordnet werden, die Gruppe nach möchte, dass Zeilen für ein Stream-Aggregat auf eine andere Weise sortiert werden, und dass das Finden der qualifizierenden Zeilen mithilfe der Prädikate der where-Klausel andere Indizes vorschlägt.

Der Grund, warum Indizierung sowohl eine Kunst als auch eine Wissenschaft ist, ist, dass eine ideale Kombination nicht immer logisch möglich ist. Die Auswahl der besten Kompromissindizes für die Arbeitslast (nicht nur eine einzelne Abfrage) erfordert analytische Fähigkeiten, Erfahrung und systemspezifisches Wissen. Wenn es einfach wäre, wären die automatisierten Tools perfekt und Berater für Leistungsoptimierung wären viel weniger gefragt.

Fehlende Indexvorschläge sind opportunistisch. Das Optimierungsprogramm macht Sie darauf aufmerksam, wenn es versucht, Prädikate und die erforderliche Sortierreihenfolge mit einem nicht vorhandenen Index abzugleichen. Die Vorschläge basieren daher auf bestimmten Übereinstimmungsversuchen im spezifischen Kontext der jeweiligen Teilplanvariante, die zu diesem Zeitpunkt in Betracht gezogen wurde.

Im Kontext sind die Vorschläge immer sinnvoll, um die geschätzten Kosten für den Datenzugriff gemäß dem Modell des Optimierers zu senken. Es wird keine umfassendere Analyse der Abfrage als Ganzes durchgeführt (geschweige denn die Arbeitsbelastung). Sie sollten diese Vorschläge daher als sanften Hinweis betrachten, dass ein Fachmann die verfügbaren Indizes mit den Vorschlägen als Ausgangspunkt betrachten muss Punkt (und normalerweise nicht mehr als das).

In Ihrem Fall kam der (Status) INCLUDE (ID)Vorschlag wahrscheinlich zustande, als die Möglichkeit eines Hash- oder Merge-Joins geprüft wurde (Beispiel später). In diesem engen Kontext ist der Vorschlag sinnvoll. Für die gesamte Abfrage vielleicht nicht. Der Index (ID, Status)ermöglicht eine verschachtelte Schleifenverknüpfung IDals äußere Referenz: Gleichheitssuche ein IDund Ungleichheit ein Statuspro Iteration.

Eine mögliche Auswahl von Indizes ist:

CREATE INDEX i1 ON dbo.I (ID, [Status]);
CREATE INDEX i1 ON dbo.IP (Deleted, OPID, IID) INCLUDE (Q);

... was einen Plan ergibt wie:

Möglicher Plan

Ich sage nicht, dass diese Indizes für Sie optimal sind . Sie arbeiten zufällig daran, einen vernünftig aussehenden Plan für mich zu erstellen, ohne Statistiken für die beteiligten Tabellen oder die vollständigen Definitionen und die vorhandene Indizierung anzeigen zu können. Außerdem weiß ich nichts über die größere Arbeitsbelastung oder die echte Abfrage.

Alternativ (nur um eine der unzähligen zusätzlichen Möglichkeiten aufzuzeigen):

CREATE INDEX i1 ON dbo.I ([Status]) INCLUDE (ID);
CREATE INDEX i1 ON dbo.IP (Deleted, IID, OPID) INCLUDE (Q);

Gibt:

Alternativer Plan

Ausführungspläne wurden mit SQL Sentry Plan Explorer erstellt .

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.