Was passiert mit einer Abfrage, die für eine Ansicht ausgeführt wird?

7

Welche Optimierungen und / oder Änderungen treten bei einer Abfrage auf, die für eine Ansicht ausgeführt wird? (Es wäre großartig zu wissen, ob alle wichtigen DBs - Oracle, SQL Server, Postgres, MySQL / MariaDB - dies anders handhaben.)


Nehmen wir zum Beispiel an, ich habe die folgende Ansicht:

CREATE VIEW my_view AS
  SELECT
      id,
      val0,
      val1,
      val2
    FROM my_table
    WHERE val0 = 42;

... und ich möchte die folgende Abfrage für diese Ansicht ausführen:

SELECT
    id,
    val1
  FROM my_view
  WHERE val2 = 'Fish fingers and custard';

... optimiert die Datenbank die Abfrage oder werden im Wesentlichen zwei Auswahlen durchgeführt? IE ist es im Wesentlichen das gleiche wie die Durchführung der potenziellen Abfrage 1 oder 2 unten oder keine?


Mögliche Abfrage 1:

SELECT
    id,
    val1
  FROM (
    SELECT
        id,
        val0,
        val1,
        val2
      FROM my_table
      WHERE val0 = 42
  )
  WHERE val2 = 'Fish fingers and custard';


Mögliche Abfrage 2:

SELECT
    id,
    val1
  FROM my_table
  WHERE val0 = 42
    AND val2 = 'Fish fingers and custard';
bsara
quelle
5
Haben Sie sich den Ausführungsplan oder den Profiler angesehen?
Scsimon
@scsimon das ist keine schlechte Idee. Ich werde es ausprobieren.
Bsara

Antworten:

14

Meine Antwort konzentriert sich fast ausschließlich auf SQL Server, nur weil ich eine ziemlich detaillierte Antwort geben werde und nicht über das gleiche Fachwissen auf anderen Plattformen verfüge.

Zunächst ist es wichtig zu wissen, dass das Abfrageoptimierungsprogramm nicht direkt mit dem von Ihnen geschriebenen SQL funktioniert. Es wird vor der Optimierung in ein internes Format umgewandelt. Was Sie für potenzielle Abfrage 1 und Abfrage 2 aufgelistet haben, ist bis auf subtile Unterschiede in der Ansicht ziemlich dasselbe. Eine ähnliche Frage zum Unterschied zwischen Abfrage 1 und Abfrage 2 wurde hier gestellt und beantwortet . Wenn Sie mehr über das interne Format von SQL Server erfahren möchten, können Sie eine hervorragende Reihe von Blog-Posts von Paul White durchgehen . In den meisten Fällen reicht es jedoch aus, nur die Abfragepläne von zwei Abfragen zu vergleichen, von denen Sie vermuten, dass sie auf dieselbe Weise optimiert wurden.

Es gibt verschiedene Möglichkeiten, wie die Verwendung einer Ansicht die Leistung verbessern kann:

  1. Es ist möglich, Ansichten zu definieren, die als physische Strukturen in der Datenbank implementiert sind. In SQL Server werden diese als indizierte Ansichten bezeichnet . In Oracle werden diese als materialisierte Ansichten bezeichnet. In Oracle kann eine Abfrage, die nicht für eine materialisierte Ansicht geschrieben wurde, weiterhin die materialisierte Ansicht verwenden. Weitere Diskussionen liegen außerhalb des Rahmens dieser Antwort.

  2. Manchmal muss dieselbe SQL-Abfrage auf mehreren RDBMS-Plattformen ausgeführt werden. Mit einer Ansicht können wir eine Syntax verwenden, die für jede Plattform eindeutig ist, aber dieselbe Abfrage an die Datenbanken gesendet wird. Ohne eine Ansicht müssen wir möglicherweise benutzerdefinierte Funktionen verwenden, die die Leistung beeinträchtigen können.

  3. Manchmal fügen Leute wirklich cleveren Code in Ansichten ein. Wenn es besser ist als das, was Sie geschrieben haben, können Sie die Leistung mithilfe der Ansicht verbessern.

Im Allgemeinen ist eine Abfrage, die für eine Ansicht geschrieben wurde, genauso effizient oder weniger effizient als eine Abfrage, die direkt für die Basistabellen geschrieben wurde. Dies liegt daran, dass eine Ansichtsdefinition häufig zusätzliche Spalten und Verknüpfungen enthält, die für die spezifische Frage, die eine Abfrage für die Ansicht stellt, möglicherweise nicht erforderlich sind. Um eine gute Leistung bei einer komplexen Sichtweise zu erzielen, hoffen wir, dass drei Dinge passieren:

  1. Spalteneliminierung. Wenn eine Spalte in einer Ansicht dargestellt, aber in der Abfrage nicht für die Ansicht erwähnt wird, sollte der Wert nicht berechnet werden.

  2. Eliminieren Sie die Eliminierung. Wenn eine Tabelle in einer Ansicht verwendet wird, die sicher entfernt werden kann, ohne die Ergebnisse zu ändern, sollte sie entfernt werden. Manchmal kann dies passieren, wenn der Optimierer mehr Informationen hat. Beispielsweise kann ein Fremdschlüssel nicht in der Datenbank deklariert werden. In anderen Fällen deckt die Regel zum Implementieren der Join-Eliminierung möglicherweise ein bestimmtes Szenario nicht ab. Beispielsweise kann in Oracle die Join-Eliminierung für einen mehrspaltigen Join nicht erfolgen, in SQL Server jedoch.

  3. Prädikat Pushdown. Wenn ich der Ansicht einen Filter hinzufüge und die zugrunde liegende Spalte einen Index enthält, sollte ich diesen Index verwenden können. Ich glaube, das ist es, worauf Ihr Beispiel hinweist. Auch ohne Index möchte ich, dass die Filter so weit wie möglich in den Plan aufgenommen werden, um unnötige Arbeit zu vermeiden.

Nach meiner Erfahrung werden diese Regeln vom Abfrageoptimierer ziemlich gut implementiert, was natürlich eine gute Sache ist, aber für SE-Demos kann es schlecht sein. Wenn wir jedoch hinterhältigen Code schreiben, können wir Beispiele erhalten, die zeigen, dass alle oben genannten Optimierungen fehlschlagen. Dies liegt daran, dass die Regeln, die die Optimierungen implementieren, nicht für jedes mögliche Szenario ausgelegt sind.

Zuerst erstelle ich einige einfache Beispieldaten. Die Daten selbst sind nicht so wichtig, aber die Tabellendefinitionen.

DROP TABLE IF EXISTS dbo.BASE_TABLE;
CREATE TABLE dbo.BASE_TABLE (
ID INT NOT NULL,
ID2 INT NOT NULL,
FILLER VARCHAR(50), 
CONSTRAINT BASE_TABLE_ID CHECK (ID > 0),
PRIMARY KEY (ID)
);

INSERT INTO dbo.BASE_TABLE WITH (TABLOCK)
SELECT TOP (1000000) 
  ROW_NUMBER() OVER (ORDER BY (SELECT NULL))
, ROW_NUMBER() OVER (ORDER BY (SELECT NULL))
, REPLICATE('Z', 50)
FROM master..spt_values t1
CROSS JOIN master..spt_values t2;

DROP TABLE IF EXISTS dbo.EXTRA_TABLE;
CREATE TABLE dbo.EXTRA_TABLE (
ID INT NOT NULL,
ID2 INT NOT NULL,
FILLER VARCHAR(50),
PRIMARY KEY (ID, ID2)
);

INSERT INTO dbo.EXTRA_TABLE WITH (TABLOCK)
SELECT TOP (1000000) 
  ROW_NUMBER() OVER (ORDER BY (SELECT NULL))
, ROW_NUMBER() OVER (ORDER BY (SELECT NULL))
, REPLICATE('Z', 50)
FROM master..spt_values t1
CROSS JOIN master..spt_values t2;

DROP TABLE IF EXISTS dbo.EMPTY_TABLE;
CREATE TABLE dbo.EMPTY_TABLE(
ID INT NOT NULL,
PRIMARY KEY (ID)
);

DROP TABLE IF EXISTS dbo.EMPTY_CCI;
CREATE TABLE dbo.EMPTY_CCI (
ID INT NOT NULL
, INDEX CCI_EMPTY_CCI CLUSTERED COLUMNSTORE
);

GO

CREATE FUNCTION dbo.THE_BEST_FUNCTION () RETURNS INT
WITH SCHEMABINDING
AS
BEGIN
RETURN NULL;
END;

GO

Hier ist meine hinterhältige Ansichtsdefinition:

CREATE VIEW dbo.SNEAKY_VIEW
AS
SELECT 
  ABS(t.ID) ID
, COUNT(*) OVER (PARTITION BY ABS(t.ID)) CNT
, dbo.THE_BEST_FUNCTION() FUNCTION_VALUE
FROM dbo.BASE_TABLE t
LEFT OUTER JOIN dbo.EXTRA_TABLE e ON CAST(t.ID AS VARCHAR(10)) = CAST(e.ID AS VARCHAR(10)) + '|' + CAST(e.ID2 AS VARCHAR(10))
LEFT OUTER JOIN dbo.EMPTY_CCI ON 1 = 0
LEFT OUTER JOIN dbo.EMPTY_TABLE e2 ON t.ID = e2.ID;

GO

Die Aussicht ist leider ein Chaos. Als menschlicher Optimierer ist es möglich, diese Abfrage erheblich zu vereinfachen. Der Join gegen EXTRA_TABLEkann eliminiert werden, da wir gegen den vollständigen Primärschlüssel verbinden, sodass sich die Anzahl der Zeilen nicht ändert. Es ist auch nicht möglich, dass Zeilen übereinstimmen, aber es handelt sich um eine äußere Verknüpfung, sodass nichts beseitigt wird. Der Join gegen EMPTY_CCIkann beseitigt werden, da die Join-Bedingung niemals übereinstimmt. Die Verknüpfung gegen EMPTY_TABLEkann beseitigt werden, da wir gegen den vollständigen Primärschlüssel beitreten . Die Funktion kehrt auch immer zurück, NULLsodass dies nicht berücksichtigt werden muss. Wir können dies also vereinfachen:

SELECT 
  ID
, 1 CNT
, NULL
FROM dbo.BASE_TABLE t;

Wir können es jedoch noch besser machen. IDist aufgrund der Einschränkung immer positiv und IDimmer eindeutig, da es sich um den Primärschlüssel handelt. Die COUNTFensterfunktion ist also immer 1. Die Abfrage kann folgendermaßen umgeschrieben werden:

SELECT 
  ID
, 1 CNT
, NULL
FROM dbo.BASE_TABLE t;

Wird das Abfrageoptimierungsprogramm in der Lage sein, eine Abfrage gegenüber der Ansicht zu vereinfachen? Lassen Sie uns das herausfinden, indem wir die Pläne vergleichen. Hier ist der Plan für die einfache Abfrage:

einfache Abfrage

Hier ist der Plan SELECT *gegen die Aussicht:

Abfrage gegen Ansicht

Sie sind ziemlich unterschiedlich. Lassen Sie uns weitere Beispielabfragen durchgehen, um Beispiele für die obigen Optimierungen zu sehen, die möglicherweise nicht genau wie erwartet funktionieren.

Erstens sind skalare benutzerdefinierte Funktionen für die Leistung in SQL Server schlecht. Unter anderem zwingen sie die gesamte Abfrage zu einem seriellen Plan. Diese Abfrage ist für einen parallelen Plan berechtigt und ich erhalte einen auf meinem Computer:

SELECT *
FROM (
    SELECT 
      ABS(t.ID) ID
    , COUNT(*) OVER (PARTITION BY ABS(t.ID)) CNT
    FROM dbo.BASE_TABLE t
    LEFT OUTER JOIN dbo.EXTRA_TABLE e ON CAST(t.ID AS VARCHAR(10)) = CAST(e.ID AS VARCHAR(10)) + '|' + CAST(e.ID2 AS VARCHAR(10))
    LEFT OUTER JOIN dbo.EMPTY_CCI ON 1 = 0
    LEFT OUTER JOIN dbo.EMPTY_TABLE e2 ON t.ID = e2.ID
) derived_table;

Der Plan:

paralleler Plan

Selbst wenn ich die Spalte nicht basierend auf der benutzerdefinierten Funktion in der Ansicht auswähle, erhalte ich dennoch einen erzwungenen Serienplan:

SELECT ID, CNT
FROM dbo.SNEAKY_VIEW;

Serienplan

Es gibt also einen Unterschied zwischen der Verwendung einer Ansicht und einer abgeleiteten Tabelle. Manchmal funktioniert die Spalteneliminierung nicht auf die gleiche Weise.

Im zweiten Beispiel wirkt sich die EMPTY_CCITabelle nicht auf die Ergebnisse der Abfrage aus. Entfernen Sie sie daher aus der abgeleiteten Tabelle:

SELECT *
FROM (
    SELECT 
      ABS(t.ID) ID
    , COUNT(*) OVER (PARTITION BY ABS(t.ID)) CNT
    FROM dbo.BASE_TABLE t
    LEFT OUTER JOIN dbo.EXTRA_TABLE e ON CAST(t.ID AS VARCHAR(10)) = CAST(e.ID AS VARCHAR(10)) + '|' + CAST(e.ID2 AS VARCHAR(10))
    LEFT OUTER JOIN dbo.EMPTY_TABLE e2 ON t.ID = e2.ID
) derived_table;

Hier ist der Abfrageplan:

kein Batch-Modus

Es gibt jedoch einen Unterschied im Abfrageplan gegenüber der Ansicht:

SELECT ID, CNT, FUNCTION_VALUE
FROM dbo.SNEAKY_VIEW;

Batch-Modus

Der Blick ist in der Lage zu verwenden Batch - Modus , die eine besondere Art und Weise ist die Ausführung der Abfrage der Implementierung , wenn gruppiert columns Indizes in einer Abfrage beteiligt ist. Auch wenn die EMPTY_CCITabelle nicht im Plan angezeigt wird, kann die Abfrage weiterhin für den Stapelmodus verwendet werden. Beachten Sie, dass dies EXTRA_TABLEin beiden Abfragen unnötig abgefragt wird. Dies liegt daran, dass die Join-Bedingung für SQL Server zu kompliziert war, um festzustellen, ob der Join sicher beseitigt werden konnte. Beachten Sie auch, dass die EMPTY_TABLETabelle in keinem der Abfragepläne angezeigt wird. Das Abfrageoptimierungsprogramm konnte es in beiden Abfragen entfernen.

Schauen wir uns für das dritte Beispiel den Prädikat-Pushdown an. Nehmen wir an, ich möchte filtern, um nur Zeilen einzuschließen, in denen ID = 500000. Wenn ich das etwas direkt außerhalb der Ansicht mache:

SELECT 
  ABS(t.ID) ID
, COUNT(*) OVER (PARTITION BY ABS(t.ID)) CNT
FROM dbo.BASE_TABLE t
LEFT OUTER JOIN dbo.EXTRA_TABLE e ON CAST(t.ID AS VARCHAR(10)) = CAST(e.ID AS VARCHAR(10)) + '|' + CAST(e.ID2 AS VARCHAR(10))
WHERE ABS(t.ID) = 500000
OPTION (MAXDOP 1);

Ich kann den Index BASE_TABLE.IDaufgrund der ABS()Funktion nicht verwenden. Der Filter wird jedoch in den Scan gedrückt, und es wird erwartet, dass nur eine Zeile zurückgegeben wird. Das kann die Leistung verbessern:

Prädikat

Mit dieser Abfrage:

SELECT ID, CNT
FROM dbo.SNEAKY_VIEW
WHERE ID = 500000;

Wir bekommen einen weniger effizienten Plan. Der Filter on ID = 500000wird ganz am Ende des Plans implementiert, sodass die Fensterfunktion anhand von fast einer Million unnötiger Zeilen bewertet wird:

kein Pushdown

Das war vielleicht ein bisschen tiefer als das, wonach Sie gesucht haben. Wenn ich zu Ihrer ursprünglichen Frage zurückkehre, würde ich sagen, dass die Abfrage gegen die Ansicht der potenziellen Abfrage 1 am ähnlichsten ist. Ich habe Gerüchte über einige Fälle gehört, in denen dies nicht der Fall ist. Wenn Sie beispielsweise viele verschachtelte Ansichten haben, kann das Abfrageoptimierungsprogramm möglicherweise Schwierigkeiten haben, diese zu entpacken, und Sie können allein aus diesem Grund eine schlecht optimierte Abfrage erhalten. Ich weiß nicht, ob dies in einer aktuellen Version von SQL Server zutrifft, aber es lohnt sich, dies einfach zu vermeiden, damit andere Personen, die versuchen, Ihren Code zu verstehen, es leichter haben.

Joe Obbish
quelle
0

Für SQL Server (ich weiß nicht für andere Datenbanksysteme).

  1. Das Erstellen einer Ansicht bringt Ihnen keinen Leistungsgewinn, kann Ihnen jedoch bei der Abstraktion des zugrunde liegenden Objekts und der Verwaltung der Sicherheit auf Objektebene für Ihre Benutzer helfen.
  2. Die Verwendung der Indexansicht kann Ihnen definitiv helfen, indem Sie Daten (Aggregat, Berechnung usw.) vorab vorbereiten. Sie müssen jedoch vorsichtig sein, wie hier ausführlich erläutert .

Wenn ich die select-Anweisung mit view und der zugrunde liegenden Tabelle ausführe, sehen Sie, dass Sie denselben Ausführungsplan erhalten und die Kosten gleich sind (jeweils 50%).

--Using View
SELECT [CustomerID]
      ,[CustomerName]
      ,[CustomerCategoryName]
      ,[PrimaryContact]
      ,[AlternateContact]
      ,[PhoneNumber]
      ,[FaxNumber]
      ,[BuyingGroupName]
      ,[WebsiteURL]
      ,[DeliveryMethod]
      ,[CityName]
      ,[DeliveryLocation]
      ,[DeliveryRun]
      ,[RunPosition]
  FROM [WideWorldImporters].[Website].[Customers]
  WHERE CustomerID=2
  GO
  --Using underlying table (view definition)
  SELECT s.CustomerID,
       s.CustomerName,
       sc.CustomerCategoryName,
       pp.FullName AS PrimaryContact,
       ap.FullName AS AlternateContact,
       s.PhoneNumber,
       s.FaxNumber,
       bg.BuyingGroupName,
       s.WebsiteURL,
       dm.DeliveryMethodName AS DeliveryMethod,
       c.CityName AS CityName,
       s.DeliveryLocation AS DeliveryLocation,
       s.DeliveryRun,
       s.RunPosition
FROM Sales.Customers AS s
LEFT OUTER JOIN Sales.CustomerCategories AS sc
ON s.CustomerCategoryID = sc.CustomerCategoryID
LEFT OUTER JOIN [Application].People AS pp
ON s.PrimaryContactPersonID = pp.PersonID
LEFT OUTER JOIN [Application].People AS ap
ON s.AlternateContactPersonID = ap.PersonID
LEFT OUTER JOIN Sales.BuyingGroups AS bg
ON s.BuyingGroupID = bg.BuyingGroupID
LEFT OUTER JOIN [Application].DeliveryMethods AS dm
ON s.DeliveryMethodID = dm.DeliveryMethodID
LEFT OUTER JOIN [Application].Cities AS c
ON s.DeliveryCityID = c.CityID
WHERE s.CustomerID=2

Geben Sie hier die Bildbeschreibung ein

Geben Sie hier die Bildbeschreibung ein

SqlWorldWide
quelle