Einen Scan bekommen, obwohl ich eine Suche erwarte

9

Ich muss eine SELECTAnweisung optimieren , aber SQL Server führt immer einen Index-Scan anstelle einer Suche durch. Dies ist die Abfrage, die sich natürlich in einer gespeicherten Prozedur befindet:

CREATE PROCEDURE dbo.something
  @Status INT = NULL,
  @IsUserGotAnActiveDirectoryUser BIT = NULL    
AS

    SELECT [IdNumber], [Code], [Status], [Sex], 
           [FirstName], [LastName], [Profession], 
           [BirthDate], [HireDate], [ActiveDirectoryUser]
    FROM Employee
    WHERE (@Status IS NULL OR [Status] = @Status)
    AND 
    (
      @IsUserGotAnActiveDirectoryUser IS NULL 
      OR 
      (
        @IsUserGotAnActiveDirectoryUser IS NOT NULL AND       
        (
          @IsUserGotAnActiveDirectoryUser = 1 AND ActiveDirectoryUser <> ''
        )
        OR
        (
          @IsUserGotAnActiveDirectoryUser = 0 AND ActiveDirectoryUser = ''
        )
      )
    )

Und das ist der Index:

CREATE INDEX not_relevent ON dbo.Employee
(
    [Status] DESC,
    [ActiveDirectoryUser] ASC
)
INCLUDE (...all the other columns in the table...); 

Der Plan:

Bild planen

Warum hat SQL Server einen Scan ausgewählt? Wie kann ich es reparieren?

Spaltendefinitionen:

[Status] int NOT NULL
[ActiveDirectoryUser] VARCHAR(50) NOT NULL

Statusparameter können sein:

NULL: all status,
1: Status= 1 (Active employees)
2: Status = 2 (Inactive employees)

IsUserGotAnActiveDirectoryUser kann sein:

NULL: All employees
0: ActiveDirectoryUser is empty for that employee
1: ActiveDirectoryUser  got a valid value (not null and not empty)
Bestter
quelle
Können Sie den tatsächlichen Ausführungsplan irgendwo veröffentlichen (kein Bild davon, sondern die .sqlplan-Datei in XML-Form)? Ich vermute, Sie haben die Prozedur geändert, aber auf Anweisungsebene keine neue Zusammenstellung erhalten. Können Sie einen Text der Abfrage ändern (z. B. das Hinzufügen des Schema-Präfix zum Tabellennamen ) und dann einen gültigen Wert für übergeben @Status?
Aaron Bertrand
1
Auch die Indexdefinition wirft die Frage auf: Warum ist der Schlüssel eingeschaltet Status DESC? Für wie viele Werte gibt es Status, wofür sind sie (wenn die Anzahl klein ist) und wird jeder Wert ungefähr gleich dargestellt? Zeigen Sie uns die Ausgabe vonSELECT TOP (20) [Status], c = COUNT(*) FROM dbo.Employee GROUP BY [Status] ORDER BY c DESC;
Aaron Bertrand

Antworten:

11

Ich glaube nicht, dass der Scan durch die Suche nach einer leeren Zeichenfolge verursacht wird (und obwohl Sie für diesen Fall einen gefilterten Index hinzufügen könnten, hilft dies nur bei sehr spezifischen Variationen der Abfrage). Es ist wahrscheinlicher, dass Sie Opfer von Parameter-Sniffing und einem einzelnen Plan sind, der nicht für alle verschiedenen Kombinationen von Parametern (und Parameterwerten) optimiert ist, die Sie für diese Abfrage bereitstellen.

Ich nenne dies das "Küchenspülen" -Verfahren , da Sie erwarten, dass eine Abfrage alle Dinge bereitstellt, einschließlich des Küchenspülbeckens.

Ich habe ein Video über meine Lösung für dieses Problem hier , aber im Grunde die beste Erfahrung , die ich für solche Fragen haben ist:

  • Erstellen Sie die Anweisung dynamisch. Auf diese Weise können Sie Klauseln weglassen, in denen Spalten erwähnt werden, für die keine Parameter angegeben wurden, und Sie erhalten einen Plan, der genau für die tatsächlichen Parameter optimiert ist , die mit Werten übergeben wurden.
  • VerwendungOPTION (RECOMPILE) - Dies verhindert, dass bestimmte Parameterwerte den falschen Plan-Typ erzwingen. Dies ist besonders hilfreich, wenn Sie einen Datenversatz oder schlechte Statistiken haben oder wenn bei der ersten Ausführung einer Anweisung ein atypischer Wert verwendet wird, der zu einem anderen Plan führt als später und häufiger Hinrichtungen.
  • Verwenden Sie die Serveroption. Dadurchoptimize for ad hoc workloads wird verhindert, dass nur einmal verwendete Abfragevariationen Ihren Plan-Cache verschmutzen.

Aktivieren Sie die Optimierung für Ad-hoc-Workloads:

EXEC sys.sp_configure 'show advanced options', 1;
GO
RECONFIGURE WITH OVERRIDE;
GO
EXEC sys.sp_configure 'optimize for ad hoc workloads', 1;
GO
RECONFIGURE WITH OVERRIDE;
GO
EXEC sys.sp_configure 'show advanced options', 0;
GO
RECONFIGURE WITH OVERRIDE;

Ändern Sie Ihre Vorgehensweise:

ALTER PROCEDURE dbo.Whatever
  @Status INT = NULL,
  @IsUserGotAnActiveDirectoryUser BIT = NULL
AS
BEGIN 
  SET NOCOUNT ON;
  DECLARE @sql NVARCHAR(MAX) = N'SELECT [IdNumber], [Code], [Status], 
     [Sex], [FirstName], [LastName], [Profession],
     [BirthDate], [HireDate], [ActiveDirectoryUser]
   FROM dbo.Employee -- please, ALWAYS schema prefix
   WHERE 1 = 1';

   IF @Status IS NOT NULL
     SET @sql += N' AND ([Status]=@Status)'

   IF @IsUserGotAnActiveDirectoryUser = 1
     SET @sql += N' AND ActiveDirectoryUser <> ''''';
   IF @IsUserGotAnActiveDirectoryUser = 0
     SET @sql += N' AND ActiveDirectoryUser = ''''';

   SET @sql += N' OPTION (RECOMPILE);';

   EXEC sys.sp_executesql @sql, N'@Status INT, @Status;
END
GO

Sobald Sie eine Arbeitslast haben, die auf diesen Abfragen basiert, die Sie überwachen können, können Sie die Ausführungen analysieren und feststellen, welche von zusätzlichen oder unterschiedlichen Indizes am meisten profitieren würden - Sie können dies aus verschiedenen Blickwinkeln tun, aus einfachen "welcher Kombination von Parameter werden am häufigsten angegeben? " zu "Welche einzelnen Abfragen haben die längsten Laufzeiten?" Wir können diese Fragen nicht nur anhand Ihres Codes beantworten. Wir können nur vorschlagen, dass ein Index nur für eine Teilmenge aller möglichen Parameterkombinationen hilfreich ist, die Sie unterstützen möchten. Zum Beispiel wenn@StatusIst NULL, ist keine Suche nach diesem nicht gruppierten Index möglich. In den Fällen, in denen sich Benutzer nicht um den Status kümmern, erhalten Sie einen Scan, es sei denn, Sie haben einen Index, der den anderen Klauseln entspricht (aber ein solcher Index ist angesichts Ihrer aktuellen Abfragelogik auch nicht hilfreich - entweder leere Zeichenfolge oder nicht leere Zeichenfolge ist nicht genau selektiv).

In diesem Fall ist dies abhängig von der Menge der möglichen StatusWerte und der Verteilung dieser Werte OPTION (RECOMPILE)möglicherweise nicht erforderlich. Wenn Sie jedoch einige Werte haben, die 100 Zeilen ergeben, und einige Werte, die Hunderttausende ergeben, möchten Sie sie möglicherweise dort haben (sogar auf CPU-Kosten, die angesichts der Komplexität dieser Abfrage marginal sein sollten), damit Sie dies können in so vielen Fällen wie möglich suchen. Wenn der Wertebereich endlich genug ist, können Sie mit dem dynamischen SQL sogar etwas Kniffliges tun, indem Sie sagen: "Ich habe diesen sehr selektiven Wert für @Status. Wenn dieser bestimmte Wert übergeben wird, nehmen Sie diese geringfügige Änderung am Abfragetext vor Dies wird als eine andere Abfrage betrachtet und für diesen Parameterwert optimiert. "

Aaron Bertrand
quelle
3
Ich habe diesen Ansatz schon oft verwendet und es ist eine fantastische Möglichkeit, den Optimierer dazu zu bringen, Dinge so zu tun, wie Sie es sich sowieso vorstellen sollten. Kim Tripp spricht hier über eine ähnliche Lösung: sqlskills.com/blogs/kimberly/high-performance-procedures Und hat ein Video von einer Sitzung, die sie vor ein paar Jahren bei PASS durchgeführt hat, das wirklich ins Detail geht, warum es funktioniert. Das heißt, es fügt dem, was Herr Bertrand hier gesagt hat, wirklich keine Tonne hinzu. Dies ist eines der Werkzeuge, die jeder in seinem Werkzeuggürtel aufbewahren sollte. Es kann wirklich einige massive Schmerzen für diese Sammelanfragen ersparen.
mskinner
3

Haftungsausschluss : Einige der Dinge in dieser Antwort können einen DBA zusammenzucken lassen. Ich gehe es von einem reinen Performance-Standpunkt aus an - wie man Index-Suchanfragen erhält, wenn man immer Index-Scans erhält.

Damit geht es aus dem Weg.

Ihre Abfrage wird als "Abfrage für Küchenspülen" bezeichnet - eine einzelne Abfrage, die eine Reihe möglicher Suchbedingungen abdeckt. Wenn der Benutzer @statuseinen Wert festlegt , möchten Sie nach diesem Status filtern. Wenn dies der Fall @statusist NULL, geben Sie alle Status usw. zurück.

Dies führt zu Problemen bei der Indizierung, die jedoch nicht mit der Sargabilität zusammenhängen, da alle Suchbedingungen den Kriterien entsprechen.

Das ist sargable:

WHERE [status]=@status

Dies ist nicht sargable, da SQL Server ISNULL([status], 0)für jede Zeile auswerten muss, anstatt einen einzelnen Wert im Index nachzuschlagen :

WHERE ISNULL([status], 0)=@status

Ich habe das Problem mit dem Spülbecken in einer einfacheren Form nachgebildet:

CREATE TABLE #work (
    A    int NOT NULL,
    B    int NOT NULL
);

CREATE UNIQUE INDEX #work_ix1 ON #work (A, B);

INSERT INTO #work (A, B)
VALUES (1,  1), (2,  1),
       (3,  1), (4,  1),
       (5,  2), (6,  2),
       (7,  2), (8,  3),
       (9,  3), (10, 3);

Wenn Sie Folgendes versuchen, erhalten Sie einen Index-Scan, obwohl A die erste Spalte des Index ist:

DECLARE @a int=4, @b int=NULL;

SELECT *
FROM #work
WHERE (@a IS NULL OR @a=A) AND
      (@b IS NULL OR @b=B);

Dies führt jedoch zu einer Indexsuche:

DECLARE @a int=4, @b int=NULL;

SELECT *
FROM #work
WHERE @a=A AND
      @b IS NULL;

Solange Sie eine überschaubare Anzahl von Parametern verwenden (in Ihrem Fall zwei), könnten Sie wahrscheinlich nur UNIONeine Reihe von Suchabfragen durchführen - im Grunde alle Permutationen von Suchkriterien. Wenn Sie drei Kriterien haben, sieht dies chaotisch aus, bei vier ist es völlig unüberschaubar. Du wurdest gewarnt.

DECLARE @a int=4, @b int=NULL;

SELECT *
FROM #work
WHERE @a=A AND
      @b IS NULL
UNION ALL
SELECT *
FROM #work
WHERE @a=A AND
      @b=B
UNION ALL
SELECT *
FROM #work
WHERE @a IS NULL AND
      @b=B
UNION ALL
SELECT *
FROM #work
WHERE @a IS NULL AND
      @b IS NULL;

Damit der dritte dieser vier einen Index-Suchvorgang verwenden kann, benötigen Sie jedoch einen zweiten Index (B, A). So könnte Ihre Abfrage mit diesen Änderungen aussehen (einschließlich meines Refactorings der Abfrage, um sie besser lesbar zu machen).

DECLARE @Status int = NULL,
        @IsUserGotAnActiveDirectoryUser bit = NULL;

SELECT [IdNumber], [Code], [Status], [Sex], [FirstName], [LastName],
       [Profession], [BirthDate], [HireDate], [ActiveDirectoryUser]
FROM Employee
WHERE [Status]=@Status AND
      @IsUserGotAnActiveDirectoryUser IS NULL

UNION ALL

SELECT [IdNumber], [Code], [Status], [Sex], [FirstName], [LastName],
       [Profession], [BirthDate], [HireDate], [ActiveDirectoryUser]
FROM Employee
WHERE [Status]=@Status AND
      @IsUserGotAnActiveDirectoryUser=1 AND ActiveDirectoryUser<>''

UNION ALL

SELECT [IdNumber], [Code], [Status], [Sex], [FirstName], [LastName],
       [Profession], [BirthDate], [HireDate], [ActiveDirectoryUser]
FROM Employee
WHERE [Status]=@Status AND
      @IsUserGotAnActiveDirectoryUser=0 AND (ActiveDirectoryUser IS NULL OR ActiveDirectoryUser='')

UNION ALL

SELECT [IdNumber], [Code], [Status], [Sex], [FirstName], [LastName],
       [Profession], [BirthDate], [HireDate], [ActiveDirectoryUser]
FROM Employee
WHERE @Status IS NULL AND
      @IsUserGotAnActiveDirectoryUser IS NULL

UNION ALL

SELECT [IdNumber], [Code], [Status], [Sex], [FirstName], [LastName],
       [Profession], [BirthDate], [HireDate], [ActiveDirectoryUser]
FROM Employee
WHERE @Status IS NULL AND
      @IsUserGotAnActiveDirectoryUser=1 AND ActiveDirectoryUser<>''

UNION ALL

SELECT [IdNumber], [Code], [Status], [Sex], [FirstName], [LastName],
       [Profession], [BirthDate], [HireDate], [ActiveDirectoryUser]
FROM Employee
WHERE @Status IS NULL AND
      @IsUserGotAnActiveDirectoryUser=0 AND (ActiveDirectoryUser IS NULL OR ActiveDirectoryUser='');

... und Sie benötigen einen zusätzlichen Index, Employeewobei die beiden Indexspalten vertauscht sind.

Der Vollständigkeit halber sollte ich erwähnen, dass dies x=@ximplizit bedeutet, dass xes nicht sein kann, NULLweil NULLes niemals gleich ist NULL. Das vereinfacht die Abfrage etwas.

Und ja, Aaron Bertrands dynamische SQL-Antwort ist in den meisten Fällen die bessere Wahl (dh wann immer Sie mit den Neukompilierungen leben können).

Daniel Hutmacher
quelle
3

Ihre grundlegende Frage scheint "Warum" zu sein, und ich denke, Sie finden die Antwort auf ungefähr 55 Minuten dieser großartigen Präsentation von Adam Machanic auf der TechEd vor einigen Jahren.

Ich erwähne die 5 Minuten in Minute 55, aber die gesamte Präsentation ist die Zeit wert. Wenn Sie sich den Abfrageplan für Ihre Abfrage ansehen, werden Sie sicher feststellen, dass er Restprädikate für die Suche enthält. Grundsätzlich kann SQL nicht alle Teile des Index "sehen", da einige von ihnen durch Ungleichungen und andere Bedingungen verborgen sind. Das Ergebnis ist ein Index-Scan für eine Supermenge basierend auf dem Prädikat. Dieses Ergebnis wird gespoolt und dann unter Verwendung des verbleibenden Prädikats erneut gescannt.

Überprüfen Sie die Eigenschaften des Scan-Operators (F4) und prüfen Sie, ob die Eigenschaftsliste sowohl "Prädikat suchen" als auch "Prädikat" enthält.

Wie andere angegeben haben, ist die Abfrage so wie sie ist schwer zu indizieren. Ich habe in letzter Zeit an vielen ähnlichen gearbeitet und jede hat eine andere Lösung benötigt. :(

Strahl
quelle
0

Bevor wir uns fragen, ob die Indexsuche dem Indexscan vorgezogen wird, besteht eine Faustregel darin, zu überprüfen, wie viele Zeilen im Vergleich zu den Gesamtzeilen der zugrunde liegenden Tabelle zurückgegeben werden. Wenn Sie beispielsweise erwarten, dass Ihre Abfrage 10 von 1 Million Zeilen zurückgibt, wird die Indexsuche wahrscheinlich dem Index-Scan vorgezogen. Wenn jedoch einige tausend Zeilen (oder mehr) von der Abfrage zurückgegeben werden sollen, wird die Indexsuche möglicherweise NICHT unbedingt bevorzugt.

Ihre Anfrage ist nicht komplex. Wenn Sie also einen Ausführungsplan veröffentlichen können, haben wir möglicherweise bessere Ideen, um Sie zu unterstützen.

jyao
quelle
Wenn ich ein paar tausend Zeilen aus einer Tabelle von 1 Million herausfiltere, möchte ich immer noch suchen - es ist immer noch eine enorme Leistungsverbesserung gegenüber dem Scannen der gesamten Tabelle.
Daniel Hutmacher
-6

Dies ist nur das Originalformat

DECLARE @Status INT = NULL,
        @IsUserGotAnActiveDirectoryUser BIT = NULL    

SELECT [IdNumber], [Code], [Status], [Sex], [FirstName], [LastName], [Profession],
       [BirthDate], [HireDate], [ActiveDirectoryUser]
FROM Employee
WHERE (@Status IS NULL OR [Status]=@Status)  
AND (            @IsUserGotAnActiveDirectoryUser IS NULL 
      OR (       @IsUserGotAnActiveDirectoryUser IS NOT NULL 
           AND (     @IsUserGotAnActiveDirectoryUser = 1 
                 AND ActiveDirectoryUser <> '') 
           OR  (     @IsUserGotAnActiveDirectoryUser = 0 
                 AND ActiveDirectoryUser =  '')
         )
    )

Dies ist die Revision - nicht 100% sicher, aber (vielleicht) probieren Sie es aus,
sogar ein ODER wird wahrscheinlich ein Problem sein,
das bei ActiveDirectoryUser null brechen würde

  WHERE isnull(@Status, [Status]) = [Status]
    AND (      (     isnull(@IsUserGotAnActiveDirectoryUser, 1) = 1 
                 AND ActiveDirectoryUser <> '' ) 
           OR  (     isnull(@IsUserGotAnActiveDirectoryUser, 0) = 0 
                 AND ActiveDirectoryUser =  '' )
        )
Paparazzo
quelle
3
Mir ist unklar, wie diese Antwort die Frage des OP löst.
Erik
@Erik Könnten wir vielleicht das OP ausprobieren lassen? Zwei ODER gingen weg. Wissen Sie sicher, dass dies die Abfrageleistung nicht verbessern kann?
Paparazzo
@ ypercubeᵀᴹ IsUserGotAnActiveDirectoryUser IS NOT NULL wird entfernt. Diese beiden unnötigen entfernen ein ODER und entfernen IsUserGotAnActiveDirectoryUser IS NULL. Sind Sie sicher, dass diese Abfrage nicht schnell ausgeführt wird als das OP?
Paparazzo
@ ypercubeᵀᴹ Hätte viele Dinge tun können. Einfacher suche ich nicht. Zwei oder sind weg. Oder ist normalerweise schlecht für Abfragepläne. Ich komme hierher, es gibt eine Art Club hier und ich bin nicht Teil des Clubs. Aber ich mache das für meinen Lebensunterhalt und poste, was ich weiß, hat funktioniert. Meine Antworten sind nicht von Abstimmungen betroffen.
Paparazzo