使用子查询与派生表进行内部联接

Posted

技术标签:

【中文标题】使用子查询与派生表进行内部联接【英文标题】:Inner Join with derived table using sub query 【发布时间】:2014-12-16 16:00:19 【问题描述】:

环境:SQL 2008 R2

我使用子查询创建了一个派生表并与主表连接。我只想知道子查询是只执行一次还是会为结果集中的每一行执行一次。考虑以下示例(虚构表名仅供参考)

SELECT E.EID,DT.Salary FROM Employees E
INNER JOIN
(
    SELECT EID, (SR.Rate * AD.DaysAttended) Salary
    FROM SalaryRate SR
    INNER JOIN AttendanceDetails AD on AD.EID=SR.EID
) DT --Derived Table for inner join
ON DT.EID=E.EID

那么,用于Inner Join的子查询只会执行一次还是多次??

如果我使用 OUTER APPLY 重写上面的查询,我肯定会为每一行执行子查询。见下文。

SELECT E.EID,DT.Salary FROM Employees E
OUTER APPLY
(
    SELECT (SR.Rate * AD.DaysAttended) Salary
    FROM SalaryRate SR
    INNER JOIN AttendanceDetails AD on AD.EID=SR.EID
    WHERE SR.EID=E.EID
) DT --Derived Table for outer apply

所以只想保证 Inner Join 只会执行一次子查询。

【问题讨论】:

【参考方案1】:

首先要注意的是你的查询没有可比性,OUTER APPLY需要替换为CROSS APPLY,或者INNER JOIN替换为LEFT JOIN

但是,当它们进行比较时,您可以看到两个查询的查询计划是相同的。我刚刚模拟了一个示例 DDL:

CREATE TABLE #Employees (EID INT NOT NULL);
INSERT #Employees VALUES (0);
CREATE TABLE #SalaryRate (EID INT NOT NULL, Rate MONEY NOT NULL);
CREATE TABLE #AttendanceDetails (EID INT NOT NULL, DaysAttended INT NOT NULL);

运行以下:

SELECT E.EID,DT.Salary FROM #Employees E
OUTER APPLY
(
    SELECT (SR.Rate * AD.DaysAttended) Salary
    FROM #SalaryRate SR
    INNER JOIN #AttendanceDetails AD on AD.EID=SR.EID
    WHERE SR.EID=E.EID
) DT; --Derived Table for outer apply

SELECT E.EID,DT.Salary FROM #Employees E
LEFT JOIN
(
    SELECT SR.EID, (SR.Rate * AD.DaysAttended) Salary
    FROM #SalaryRate SR
    INNER JOIN #AttendanceDetails AD on AD.EID=SR.EID
) DT --Derived Table for inner join
ON DT.EID=E.EID;

给出以下计划:

并更改为 INNER/CROSS:

SELECT E.EID,DT.Salary FROM #Employees E
CROSS APPLY
(
    SELECT (SR.Rate * AD.DaysAttended) Salary
    FROM #SalaryRate SR
    INNER JOIN #AttendanceDetails AD on AD.EID=SR.EID
    WHERE SR.EID=E.EID
) DT; --Derived Table for outer apply


SELECT E.EID,DT.Salary FROM #Employees E
INNER JOIN
(
    SELECT SR.EID, (SR.Rate * AD.DaysAttended) Salary
    FROM #SalaryRate SR
    INNER JOIN #AttendanceDetails AD on AD.EID=SR.EID
) DT --Derived Table for inner join
ON DT.EID=E.EID;

给出以下计划:

这些是外部表中没有数据,员工中只有一行的计划,因此不太现实。在外部应用的情况下,SQL Server 能够确定员工中只有一行,因此只对外部表进行嵌套循环连接(即逐行查找)将是有益的。在将 1,000 行放入员工中后,使用 LEFT JOIN/OUTER APPLY 会产生以下计划:

您可以在此处看到连接现在是哈希匹配连接,这意味着(用最简单的术语来说)SQL Server 已经确定最好的计划是首先执行外部查询,对结果进行哈希处理,然后从员工中查找.然而,这并不意味着子查询作为一个整体被执行并存储结果,为简单起见,您可以考虑这一点,但仍然可以使用来自外部查询的谓词,例如,如果子查询在内部执行和存储,以下查询将产生大量开销:

SELECT E.EID,DT.Salary FROM #Employees E
LEFT JOIN
(
    SELECT SR.EID, (SR.Rate * AD.DaysAttended) Salary
    FROM #SalaryRate SR
    INNER JOIN #AttendanceDetails AD on AD.EID=SR.EID
) DT --Derived Table for inner join
ON DT.EID=E.EID
WHERE E.EID = 1;

检索所有员工费率、存储结果并实际查找一名员工的意义何在?检查执行计划表明EID = 1 谓词被传递到#AttendanceDetails 上的表扫描:

所以下面几点的答案是:

如果我使用 OUTER APPLY 重写上述查询,我​​肯定会为每一行执行子查询。 Inner Join 只会执行一次子查询。

视情况而定。如果可能,使用APPLY SQL Server 将尝试将查询重写为JOIN,因为这将产生最佳计划,因此使用OUTER APPLY 并不能保证查询将为每一行执行一次。同样,使用LEFT JOIN 并不能保证查询只执行一次。

SQL 是一种声明性语言,因为您告诉它您希望它做什么,而不是如何去做,因此您不应依赖特定命令来引发特定行为,相反,如果您发现性能问题,请检查执行计划和 IO 统计信息,以了解执行情况,并确定如何改进查询。

此外,SQL Server 不会对子查询进行处理,通常定义会扩展到主查询中,所以即使您已经编写了:

SELECT E.EID,DT.Salary FROM #Employees E
INNER JOIN
(
    SELECT SR.EID, (SR.Rate * AD.DaysAttended) Salary
    FROM #SalaryRate SR
    INNER JOIN #AttendanceDetails AD on AD.EID=SR.EID
) DT --Derived Table for inner join
ON DT.EID=E.EID;

实际执行的更像是:

SELECT  e.EID, sr.Rate * ad.DaysAttended AS Salary
FROM    #Employees e
        INNER JOIN #SalaryRate sr
            on e.EID = sr.EID
        INNER JOIN #AttendanceDetails ad
            ON ad.EID = sr.EID;

【讨论】:

感谢您的详细解释。所以基本上你是说,我们无法确定如何处理子查询。我的印象是子查询总是首先实现。 关于无法确定如何处理子查询,是的,我们不知道,我们可以有一个很好的猜测,但最终还是由优化器来决定执行查询的最佳方式,并且 99.9% 的时间(也许更多)这将是最好的方式。关于具体化子查询,不,查询的某些部分可能会具体化(无论是否在子查询中)并假脱机结果,但同样,由优化器决定这是否是最佳选择。跨度> 值得一提的是 connect item 打开请求查询提示,该提示将强制实现子查询/ctes。 投票赞成你的答案。我认为这是对这个问题的最准确答案。【参考方案2】:

使用 INNER JOIN,您的子查询将只执行一次,其记录可能会在复杂操作的 tempdb 工作表内部存储,然后与第一个表联接。

使用 APPLY 子句,将针对第一个表中的每一行执行子查询。

编辑:使用 CTE

;with SalaryRateCTE as 
(
    SELECT EID, (SR.Rate * AD.DaysAttended) AS Salary
    FROM SalaryRate SR
    INNER JOIN AttendanceDetails AD on AD.EID=SR.EID
)
SELECT E.EID, DT.Salary 
FROM Employees E
INNER JOIN SalaryRateCTE DT --Derived Table for inner join
ON DT.EID = E.EID

【讨论】:

这不是真的。在内部,SQL 会将问题中的 apply 重写为 JOIN,并将根据可用的统计信息和索引根据最佳计划执行。另外SQL Server不会对子查询进行matierialise,所以它不会执行一次并存储结果。 我是在笼统地谈论 APPLY 子句的行为,以及这两个子句中实际应该使用什么,而不应该让 SQL 优化器去猜测。在大多数情况下,它将多次执行子查询,因为 APPLY 子句旨在以这种方式设计,例如,如果您在子查询中包含 TOP 5 和 SELECT。但是是的,在这种情况下它会执行一次,但 APPLY 不应该在这里使用。同样关于子查询,如果它是一个复杂的查询,那么 SQL 优化器会在内部在 tempDB 中创建一个 WorkTable 来假脱机这些记录。希望这会有所帮助并避免混淆。 我的子查询实际上是一个复杂的子查询,它涉及到视图和行号函数来跳过重复项。将其设为子查询/应用的原因是为了让未来的开发人员具有更好的可读性。但不确定是否为每一行执行整个子查询 你的主要目标是什么?如果您只想加入表和子查询,那么只需使用 JOIN,而不是 APPLY 子句。使用 APPLY 不会使您的代码看起来很漂亮,但可能会使其他开发人员感到困惑。为了让它看起来更漂亮,使用 CTE 来存储子查询,见我上面的回复。 使用 CTE 与子查询没有什么不同,不会强制实体化。由于子查询定义扩展为主查询,子查询的复杂性与假脱机无关。您可以在没有子查询的情况下进行假脱机。关键是 SQL 是声明性的,尽管您可以很好地了解查询计划是否将使用合并、散列或嵌套循环连接,除非您明确告诉它,否则您不能依赖某个命令来产生某个计划.如我的回答所示,当查询不同的数据时,完全相同的查询会产生两个不同的计划。【参考方案3】:

子查询只会被评估一次。为避免混淆,我们可以简单地将子查询视为单个表/视图,因为外部和内部内部查询不相关。

【讨论】:

关于outer apply查询的2点: 1.不等同于Inner join。但在某些方面类似于外连接,因为它提供了与条件不匹配的额外行。 2. 外层查询的每一行都会执行。 SQL Server 不会对子查询进行matierialise,因此不会执行一次并存储结果。子查询定义扩展到主查询中以允许进一步优化。 感谢您指出这一点!在编写我未来的查询时,我会一直考虑这一点。

以上是关于使用子查询与派生表进行内部联接的主要内容,如果未能解决你的问题,请参考以下文章

哪个查询更有效?内部联接与子查询?总和案例与拥有

在 SQL 子查询中使用多个表进行 Oracle 半联接

数据库(比如MYSQL) ,表连结查询与子查询哪个效率高些? 为啥

MySQL-子查询,派生表,通用表达式

BigQuery:使用子查询和内部联接的计数更新行

如何将子查询包含到内部联接中?