在视图上执行的查询会发生什么?

bsa*_*ara 7 optimization view

对视图执行的查询有哪些优化和/或更改?(如果还知道每个主要数据库 - Oracle、SQL Server、Postgres、MySQL/MariaDB - 是否以不同方式处理这个问题,那就太好了)。


例如,假设我有以下观点:

CREATE VIEW my_view AS
  SELECT
      id,
      val0,
      val1,
      val2
    FROM my_table
    WHERE val0 = 42;
Run Code Online (Sandbox Code Playgroud)

...我想对该视图执行以下查询:

SELECT
    id,
    val1
  FROM my_view
  WHERE val2 = 'Fish fingers and custard';
Run Code Online (Sandbox Code Playgroud)

...数据库是优化查询还是本质上执行了两个选择?IE 是否与执行下面的潜在查询 1 或 2 或两者都不相同?


潜在问题 1:

SELECT
    id,
    val1
  FROM (
    SELECT
        id,
        val0,
        val1,
        val2
      FROM my_table
      WHERE val0 = 42
  )
  WHERE val2 = 'Fish fingers and custard';
Run Code Online (Sandbox Code Playgroud)


潜在问题 2:

SELECT
    id,
    val1
  FROM my_table
  WHERE val0 = 42
    AND val2 = 'Fish fingers and custard';
Run Code Online (Sandbox Code Playgroud)

Joe*_*ish 14

我的回答几乎完全集中在 SQL Server 上,因为我将给出一个相当详细的答案,而且我在其他平台上没有相同水平的专业知识。

首先,重要的是要意识到查询优化器并不直接针对您编写的 SQL 工作。它在优化之前被转换为内部格式。您为潜在查询 1 和查询 2 列出的内容几乎相同,只是视图存在细微差别。此处提出并回答有关查询 1 和查询 2 之间差异的类似问题。如果您想了解有关 SQL Server 使用的内部格式的更多信息,可以阅读Paul White 撰写的一系列出色的博客文章。但是,大多数情况下,只需比较您怀疑可能以相同方式优化的两个查询的查询计划就足够了。

使用视图可以通过以下几种方式提高性能:

  1. 可以定义在数据库上作为物理结构实现的视图。在 SQL Server 中,这些被称为索引视图。在 Oracle 中,这些被称为物化视图。在 Oracle 中,不针对物化视图编写的查询仍然可以使用物化视图。进一步讨论超出了本答案的范围。

  2. 有时,同一个 SQL 查询需要在多个 RDBMS 平台上运行。通过视图,我们可以使用每个平台独有的语法,但相同的查询会发送到数据库。如果没有视图,我们可能不得不使用用户定义的函数,这可能对性能不利。

  3. 有时人们会在视图中放入非常聪明的代码。如果它比你写的更好,你可以通过使用视图来提高性能。

通常,针对视图编写的查询与直接针对基表编写的查询一样有效或效率较低。这是因为视图定义通常包含额外的列和联接,对于针对视图的查询提出的特定问题可能不需要这些。为了在复杂视图上获得良好的性能,我们希望发生三件事:

  1. 列消除。如果某个列出现在视图中,但未在针对该视图的查询中提及,则不应计算该值。

  2. 加入消除。如果在视图中使用的表可以在不更改结果的情况下安全地消除,则应将其消除。如果优化器有更多信息,有时会发生这种情况。例如,可能未在数据库中声明外键。其他时候,实现连接消除的规则可能不涵盖特定场景。例如,在 Oracle 中,多列连接不会发生连接消除,但在 SQL Server 中可以。

  3. 谓词下推。如果我向视图添加过滤器并且基础列上有一个索引,那么我应该能够使用该索引。我相信这就是你的例子所暗示的。即使没有索引,我仍然希望尽可能将过滤器推入计划中,以避免不必要的工作。

根据我的经验,查询优化器很好地实现了这些规则,这当然是一件好事,但对于 SE 演示来说可能是坏事。然而,如果我们编写偷偷摸摸的代码,我们最终可能会得到显示上述所有优化失败的示例。这是因为实现优化的规则并非旨在涵盖所有可能的场景。

首先,我将创建一些简单的示例数据。数据本身并不重要,但表定义很重要。

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
Run Code Online (Sandbox Code Playgroud)

这是我的偷偷摸摸的视图定义:

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
Run Code Online (Sandbox Code Playgroud)

不幸的是,景色一团糟。作为人工优化器,可以大大简化该查询。在加入反对EXTRA_TABLE可以,因为我们要加入对全主键,以便行数不会改变被淘汰。任何行也不可能匹配,但它是外连接,因此不会消除任何内容。EMPTY_CCI可以消除联接反对,因为联接条件永远不会匹配。加入反对EMPTY_TABLE可以被消除,因为我们加入反对完整的主键。该函数也始终返回,NULL因此无需包含该函数。所以我们可以简化为:

SELECT 
  ID
, 1 CNT
, NULL
FROM dbo.BASE_TABLE t;
Run Code Online (Sandbox Code Playgroud)

然而,我们可以做得比这更好。ID由于约束,ID它总是正的,并且总是唯一的,因为它是主键。所以COUNT窗口函数将始终为 1。查询可以这样重写:

SELECT 
  ID
, 1 CNT
, NULL
FROM dbo.BASE_TABLE t;
Run Code Online (Sandbox Code Playgroud)

查询优化器能否将针对视图的查询简化为这样?让我们通过比较计划来找出答案。这是简单查询的计划:

简单查询

这是SELECT *反对观点的计划:

查询视图

他们有很大的不同。让我们通过更多示例查询来查看上述优化的示例,这些示例可能无法完全按预期进行。

首先,标量用户定义函数不利于 SQL Server 中的性能。除其他问题外,它们强制整个查询具有串行计划。这个查询符合并行计划的条件,我在我的机器上得到了一个:

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;
Run Code Online (Sandbox Code Playgroud)

计划:

平行计划

但是,即使我没有在视图中根据用户定义的函数选择列,我仍然会得到一个强制串行计划:

SELECT ID, CNT
FROM dbo.SNEAKY_VIEW;
Run Code Online (Sandbox Code Playgroud)

系列计划

因此,使用视图和派生表之间存在一个区别。有时列消除不会以相同的方式工作。

对于第二个示例,该EMPTY_CCI表不会影响查询结果,因此让我们将其从派生表中删除:

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;
Run Code Online (Sandbox Code Playgroud)

这是查询计划:

没有批处理模式

但是,针对视图的查询计划存在差异:

SELECT ID, CNT, FUNCTION_VALUE
FROM dbo.SNEAKY_VIEW;
Run Code Online (Sandbox Code Playgroud)

批处理模式

该视图能够使用批处理模式,这是一种在查询中涉及聚集列存储索引时实现查询执行的特殊方式。即使该EMPTY_CCI表没有出现在计划中,查询仍然适用于批处理模式。请注意,EXTRA_TABLE在两个查询中都不必要地查询了。这是因为连接条件太复杂,SQL Server 无法确定连接是否可以安全消除。另请注意,该EMPTY_TABLE表不会出现在任一查询计划中。查询优化器能够在两个查询中消除它。

对于第三个示例,让我们看看谓词下推。假设我想过滤以仅包含行 where ID = 500000。如果我直接在视图之外这样做:

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);
Run Code Online (Sandbox Code Playgroud)

BASE_TABLE.ID由于该ABS()功能,我无法使用索引。但是,过滤器被推入扫描中,预计只会返回一行。这可以提高性能:

谓词

有了这个查询:

SELECT ID, CNT
FROM dbo.SNEAKY_VIEW
WHERE ID = 500000;
Run Code Online (Sandbox Code Playgroud)

我们得到了一个效率较低的计划。过滤器在ID = 500000计划的最后实现,因此窗口函数将针对近一百万行不必要的行进行评估:

没有下推

这可能比您想要的更深入。回到您最初的问题,我会说针对视图的查询与潜在查询 1 最相似。我听说过有关某些情况的谣言,但在某些情况下并非如此。例如,假设您有许多嵌套视图,那么查询优化器可能难以解包它们,并且仅由于这个原因,您最终可能会得到优化不佳的查询。我不知道在当前版本的 SQL Server 中是否如此,但值得避免这样做,以便其他试图理解您的代码的人可以更轻松地使用它。


Sql*_*ide 0

对于 SQL Server(我不知道是否适用于其他数据库系统)。

  1. 创建视图不会给您带来任何性能提升,但可以帮助您抽象底层对象并为用户管理对象级安全性。
  2. 使用索引视图绝对可以帮助您提前准备数据(聚合、计算等),但您需要小心,如此处详细解释的那样

如果我使用视图和基础表运行 select 语句,您可以看到获得相同的执行计划并且成本相同(各 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
Run Code Online (Sandbox Code Playgroud)

在此输入图像描述

在此输入图像描述