SQL Server verwendet keinen Index für einfache Bijektion


11

Dies ist ein weiteres Rätsel des Abfrageoptimierers.

Vielleicht überschätze ich nur die Abfrageoptimierer, oder vielleicht fehlt mir etwas - also stelle ich es da raus.

Ich habe einen einfachen Tisch

CREATE TABLE [dbo].[MyEntities](
  [Id] [uniqueidentifier] NOT NULL,
  [Number] [int] NOT NULL,
  CONSTRAINT [PK_dbo.MyEntities] PRIMARY KEY CLUSTERED ([Id])
)

CREATE NONCLUSTERED INDEX [IX_Number] ON [dbo].[MyEntities] ([Number])

mit einem Index und einigen tausend Zeilen, Numberdie gleichmäßig in den Werten 0, 1 und 2 verteilt sind.

Nun diese Abfrage:

SELECT * FROM
    (SELECT
        [Extent1].[Number] AS [Number],
        CASE
        WHEN (0 = [Extent1].[Number]) THEN 'one'
        WHEN (1 = [Extent1].[Number]) THEN 'two'
        WHEN (2 = [Extent1].[Number]) THEN 'three'
        ELSE '?'
        END AS [Name]
        FROM [dbo].[MyEntities] AS [Extent1]
        ) P
WHERE P.Number = 0;

Sucht ein Index so, IX_Numberwie man es erwarten würde?

Wenn die where-Klausel ist

WHERE P.Name = 'one';

Es wird jedoch ein Scan.

Die case-Klausel ist offensichtlich eine Bijektion, daher sollte theoretisch eine Optimierung möglich sein, um den ersten Abfrageplan von der zweiten Abfrage abzuleiten.

Es ist auch nicht rein akademisch: Die Abfrage ist inspiriert von der Übersetzung von Enum-Werten in ihre jeweiligen Anzeigenamen.

Ich würde gerne von jemandem hören, der weiß, was von Abfrageoptimierern erwartet werden kann (und insbesondere von dem in SQL Server): Erwarte ich einfach zu viel?

Ich frage, wie ich es zuvor getan habe, in denen eine geringfügige Variation einer Abfrage eine Optimierung plötzlich ans Licht bringen würde.

Ich verwende SQL Server 2016 Developer Edition.

Antworten:


18

Erwarte ich einfach zu viel?

Ja. Zumindest in aktuellen Versionen des Produkts.

SQL Server wird nicht die zerpflücken CASEAussage und Reverse Engineering es zu entdecken , dass , wenn das Ergebnis der berechneten Spalte ist 'one'dann [Extent1].[Number]sein muss 0.

Sie müssen sicherstellen, dass Sie Ihre Prädikate so schreiben, dass sie sarkierbar sind. Was fast immer bedeutet, dass es in der Form ist. basetable_column_name comparison_operator expression.

Selbst geringfügige Abweichungen beeinträchtigen die Sargabilität.

WHERE P.Number + 0 = 0;

würde auch keine Indexsuche verwenden, obwohl die Vereinfachung noch einfacher ist als der CASEAusdruck.

Wenn Sie nach einem Zeichenfolgennamen suchen und eine Suche nach einer Nummer erhalten möchten, benötigen Sie eine Zuordnungstabelle mit den Namen und Nummern, die Sie in der Abfrage verknüpfen möchten. Der Plan enthält möglicherweise eine Suche in der Zuordnungstabelle, gefolgt von einer korrelierten Suche weiter [dbo].[MyEntities]mit der Nummer, die von der ersten Suche zurückgegeben wurde.


6

Projizieren Sie Ihre Aufzählung nicht als case-Anweisung. Projizieren Sie es wie folgt als abgeleitete Tabelle:

SELECT * FROM
   (SELECT
      [Extent1].[Number] AS [Number],
      enum.Name
   FROM
      [dbo].[MyEntities] AS [Extent1]
      LEFT JOIN (VALUES
         (0, 'one'),
         (1, 'two'),
         (2, 'three')
      ) enum (Number, Name)
         ON Extent1.Number = enum.Number
   ) P
WHERE
   P.Name = 'one';

Ich vermute, Sie werden bessere Ergebnisse erzielen. (Ich habe den Namen nicht konvertiert, ?wenn er fehlt, da dies möglicherweise die Leistungssteigerung beeinträchtigen würde. Sie können jedoch die WHEREKlausel in die äußere Abfrage verschieben, um das Prädikat in die enumTabelle aufzunehmen, oder Sie können zwei Spalten aus dem zurückgeben innere Abfrage, eine für das Prädikat und eine für die Anzeige, wobei das Prädikat ist, NULLwenn es keinen übereinstimmenden Aufzählungswert gibt.)

Ich vermute jedoch, dass Sie aus diesem Grund [Extent1]ein ORM wie Entity Framework oder Linq-To-SQL verwenden. Ich kann Ihnen nicht zeigen, wie Sie eine solche Projektion nativ durchführen können, aber Sie könnten eine andere Technik verwenden.

In einem meiner Projekte habe ich Code-Enum-Werte in realen Tabellen in der Datenbank durch eine benutzerdefinierte Build-Klasse reflektiert, die die Enum-Werte in der Datenbank zusammengeführt hat. (Sie müssen die Regel einhalten, dass Sie Ihre Enum-Werte explizit auflisten müssen, keine löschen können, ohne Ihre Tabellen zu überprüfen, und sie niemals ändern können, obwohl Sie dies zumindest bei Ihrem aktuellen Setup bereits beachten müssen.) .

Jetzt habe ich eine Aufzählung von a verwendet Identifier Basisklasse verwendet, die viele verschiedene konkrete Unterklassen hat, aber es gibt keinen Grund, warum dies mit einer einfachen Vanille-Aufzählung nicht möglich wäre. Hier ist ein Beispiel:

new EnumOrIdentifierProjector<CodeClassOrEnum, PrivateDbDtoObject>(
   _sqlConnector.Connection,
   "dbo.TableName",
   "PrimaryKeyId",
   "NameColumnName",
   dtoObject => dtoObject.PrimaryKeyId,
   dtoObject => dtoObject.NameField,
   EnumerableOfIdentifierOrTypeOfEnum
)
   .Populate();

Sie können sehen, dass ich alle notwendigen Informationen übergeben habe, um die Datenbankwerte sowohl zu schreiben als auch zu lesen. (Ich hatte eine Situation, in der die aktuelle Anforderung möglicherweise nicht alle vorhandenen Werte enthält und daher zusätzliche Werte aus der Datenbank sowie den aktuell geladenen Satz zurückgeben muss. Ich habe auch die Datenbank IDs zuweisen lassen, obwohl Sie dies für eine Aufzählung wahrscheinlich nicht tun würden will das.)

Die Idee ist, dass Sie, sobald Sie eine Tabelle haben, die beim Start nur einmal gelesen / geschrieben wird und zuverlässig alle Enum-Werte enthält, einfach wie jede andere Tabelle daran teilnehmen und die Leistung gut sein sollte.

Ich hoffe, diese Ideen reichen aus, um eine Verbesserung zu erzielen.


Ja, ich verwende EntityFramework und dort sollte die Lösung wirklich in einer optimalen Welt sein. Bevor dies geschieht, ist Ihr Vorschlag eine der besten Problemumgehungen, die ich glaube.
John

5

Ich interpretiere die Frage so, dass Sie an Optimierern im Allgemeinen interessiert sind, aber mit einem besonderen Interesse für SQL Server. Ich habe Ihr Szenario mit db2 LUW V11.1 getestet:

]$ db2 "create table myentities ( id int not null, number int not null )"
]$ db2 "create index ix_number on myentities (number)"
]$ db2 "insert into myentities (id, number) with t(n) as ( values 0 union all select n+1 from t where n<10000) select n, mod(n,3) from t"

Das Optimierungsprogramm in DB2 schreibt die zweite Abfrage in die erste um:

Original Statement:
------------------
SELECT 
  * 
FROM 
  (SELECT 
     number,

   CASE 
   WHEN (0 = Number) 
   THEN 'one' 
   WHEN (1 = Number) 
   THEN 'two' 
   WHEN (2 = Number) 
   THEN 'three' 
   ELSE '?' END AS Name 
   FROM 
     MyEntities
  ) P 
WHERE 
  P.name = 'one'


Optimized Statement:
-------------------
SELECT 
  Q1.NUMBER AS "NUMBER",

CASE 
WHEN (0 = Q1.NUMBER) 
THEN 'one' 
WHEN (1 = Q1.NUMBER) 
THEN 'two' 
WHEN (2 = Q1.NUMBER) 
THEN 'three' 
ELSE '?' END AS "NAME" 
FROM 
  LELLE.MYENTITIES AS Q1 
WHERE 
  (0 = Q1.NUMBER)

Der Plan sieht aus wie:

Access Plan:
-----------
        Total Cost:             33.5483
        Query Degree:           1


      Rows 
     RETURN
     (   1)
      Cost 
       I/O 
       |
      3334 
     IXSCAN
     (   2)
     33.1861 
     4.66713 
       |
      10001 
 INDEX: LELLE   
    IX_NUMBER
       Q1

Ich weiß nicht viel über andere Optimierer, aber ich habe das Gefühl, dass der DB2-Optimierer selbst unter Wettbewerbern als ziemlich gut angesehen wird.


Das ist aufregend. Können Sie etwas Licht ins Dunkel bringen, woher die "optimierte Aussage" kommt? Gibt Ihnen db2 selbst das zurück? - Außerdem habe ich Probleme beim Lesen des Plans. Ich nehme an, "IXSCAN" bedeutet in diesem Fall nicht Index-Scan?
John

1
Sie können DB2 anweisen, eine Anweisung für Sie zu erläutern. Die gesammelten Informationen werden in einer Reihe von Tabellen gespeichert, und Sie können entweder Visual EXPLAIN oder wie in diesem Fall das Dienstprogramm db2exfmt verwenden (oder Ihr eigenes Dienstprogramm erstellen). Darüber hinaus können Sie eine Aussage überwachen und die geschätzte Kardinalität im Plan mit dem tatsächlichen Plan vergleichen. In diesem Plan können wir sehen, dass es sich tatsächlich um einen Indexscan (IXSCAN) handelt und die geschätzte Ausgabe dieses Operators 3334 Zeilen beträgt. Ist das schlecht in SQL Server? Es kennt den Startschlüssel und den Stoppschlüssel, sodass nur die relevanten Zeilen in DB2 gescannt werden.
Lennart

Was es Scan nennt, beinhaltet also das Suchen, und um ehrlich zu sein, die entsprechenden Planerklärungen von SQL Server nennen manchmal auch etwas einen Scan, der Suchen beinhaltet, und manchmal nennt es es eine Suche. Ich muss immer auf die Zeilenanzahl schauen, um zu verstehen, was was ist. Da die Ausgabe von db2 eindeutig eine 3334 enthält, macht sie genau das, was ich mir erhofft hatte. Sehr interessant.
John

Ja, ich finde es auch manchmal verwirrend. Man muss sich die detaillierteren Informationen für jeden Bediener ansehen, um wirklich zu verstehen, was los ist.
Lennart

0

In dieser speziellen Abfrage ist es ziemlich dumm, überhaupt eine CASEAussage zu haben. Sie filtern auf einen bestimmten Fall! Vielleicht ist dies nur ein Detail der speziellen Beispielabfrage, die Sie gegeben haben. Wenn nicht, können Sie diese Abfrage schreiben, um gleichwertige Ergebnisse zu erhalten:

SELECT
    [Extent1].[Number] AS [Number],
    'one' AS [Name]
FROM [dbo].[MyEntities] AS [Extent1]
WHERE [Extent1].[Number] = 0;

Dadurch erhalten Sie genau die gleiche Ergebnismenge, und da Sie ohnehin bereits Werte in einer CASEAnweisung fest codieren , verlieren Sie hier keine Wartbarkeit.


1
Ich denke, Sie verpassen den Punkt - dies ist generiertes SQL aus einer Back-End-Codebasis, die mit Aufzählungen über ihre Zeichenfolgendarstellungen arbeitet. Der Code, der SQL projiziert, übt die Gewalt gegen die Abfrage aus. Ich bin sicher, dass der Fragesteller, wenn er die SQL selbst schreiben würde, eine bessere Abfrage schreiben könnte. Es ist also nicht albern, überhaupt eine CASEAussage zu haben, weil ORMs so etwas tun. Was albern ist, dass Sie diese einfachen Facetten des Problems nicht erkannt haben ... (wie ist das, wenn Sie indirekt als hirnlos bezeichnet werden?)
ErikE

@ErikE Immer noch irgendwie albern, da Sie nur den numerischen Wert der Aufzählung verwenden können, vorausgesetzt, C # ist trotzdem. (Eine ziemlich sichere Annahme, da es sich um SQL Server handelt.)
jpmc26

Sie haben jedoch keine Ahnung, was der Anwendungsfall ist. Vielleicht wäre es eine große Änderung, auf den numerischen Wert umzuschalten. Vielleicht wurden die Aufzählungen in eine vorhandene riesige Codebasis nachgerüstet. Ohne Wissen zu kritisieren ist lächerlich.
ErikE

@ErikE Wenn es lächerlich ist, warum machst du es dann? =) Ich habe nur geantwortet, um darauf hinzuweisen, dass wenn der Anwendungsfall so einfach ist wie das Beispiel in der Frage (das im Vorwort meiner Antwort klar angegeben ist), die CASEAussage ohne Nachteil vollständig beseitigt werden kann. Von Natürlich könnte es unbekannte Faktoren sein, aber sie sind nicht spezifiziert.
jpmc26

Ich habe keine Einwände gegen die sachlichen Teile Ihrer Antwort, nur gegen die Teile, die subjektiv charakterisieren. Ob ich ohne Wissen kritisiere, ich bin ganz Ohr, um zu verstehen, auf welche Weise ich keine gewissenhaft saubere Logik
angewendet
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.