何时应该使用CROSS APPLY而不是INNER JOIN?

1130
CROSS APPLY 主要作用是什么?
我了解到,对于分区数据集的选择操作,使用 CROSS APPLY 可能会更有效率,尤其是在处理分页场景时。此外,CROSS APPLY 不需要右表为 UDF。
在大多数 INNER JOIN 查询中(一对多关系),我可以重写成使用 CROSS APPLY,但它们总是给我等效的执行计划。
请问有没有人能给出一个很好的例子,在这些情况下 CROSS APPLY 会产生不同于 INNER JOIN 且更高效的执行计划呢?
create table Company (
    companyId int identity(1,1)
,   companyName varchar(100)
,   zipcode varchar(10) 
,   constraint PK_Company primary key (companyId)
)
GO

create table Person (
    personId int identity(1,1)
,   personName varchar(100)
,   companyId int
,   constraint FK_Person_CompanyId foreign key (companyId) references dbo.Company(companyId)
,   constraint PK_Person primary key (personId)
)
GO

insert Company
select 'ABC Company', '19808' union
select 'XYZ Company', '08534' union
select '123 Company', '10016'


insert Person
select 'Alan', 1 union
select 'Bobby', 1 union
select 'Chris', 1 union
select 'Xavier', 2 union
select 'Yoshi', 2 union
select 'Zambrano', 2 union
select 'Player 1', 3 union
select 'Player 2', 3 union
select 'Player 3', 3 


/* using CROSS APPLY */
select *
from Person p
cross apply (
    select *
    from Company c
    where p.companyid = c.companyId
) Czip

/* the equivalent query using INNER JOIN */
select *
from Person p
inner join Company c on p.companyid = c.companyId

60
我知道我有点挑剔,但“performant”绝对是一个词。它只是与效率无关。 - Rire1979
3
这个链接对于处理 SQL XQuery 很有用,可以查看一下。请点击 这里 - ARZ
4
使用"内连接(inner loop join)"似乎非常接近于使用"交叉应用(cross apply)"。我希望你的例子可以详细说明等效的连接提示(join hint)是哪个。仅仅说"join"可能会导致内/循环/合并或者甚至"其他",因为它可能会重组其他连接。 - crokusek
4
当连接操作会创建大量行,但您每次只需要评估一行连接时。我曾经遇到这样的情况,需要在一张有超过1亿行的表上进行自我连接,但内存不足。因此,我使用了游标来减少内存占用。从游标转为使用CROSS APPLY,仍然可以控制内存占用,并且比游标快三分之一。 - paparazzo
13
“CROSS APPLY”显然可以让一个集合依赖于另一个集合(不像“JOIN”运算符),但这并非没有代价:它的行为类似于作用在左侧集合的函数,因此在SQL Server中,它总是执行一个“循环连接”,而这几乎永远不是连接集合的最佳方式。因此,在需要时使用“APPLY”,但不要过度使用它来替代“JOIN”。 - Gerardo Lima
显示剩余4条评论
15个回答

793

有人能给我一个很好的例子,说明CROSS APPLY在INNER JOIN同样适用的情况下有何不同吗?

请参阅我的博客文章,了解详细的性能比较:

CROSS APPLY更适用于没有简单JOIN条件的情况。

这个例子选择t1每个记录中最后的3条记录来自t2

SELECT  t1.*, t2o.*
FROM    t1
CROSS APPLY
        (
        SELECT  TOP 3 *
        FROM    t2
        WHERE   t2.t1_id = t1.id
        ORDER BY
                t2.rank DESC
        ) t2o

使用INNER JOIN条件无法轻松地进行公式化。

您可能可以使用CTE和窗口函数来实现类似的操作:

WITH    t2o AS
        (
        SELECT  t2.*, ROW_NUMBER() OVER (PARTITION BY t1_id ORDER BY rank) AS rn
        FROM    t2
        )
SELECT  t1.*, t2o.*
FROM    t1
INNER JOIN
        t2o
ON      t2o.t1_id = t1.id
        AND t2o.rn <= 3

但这种方式可读性较差,可能效率也不高。

更新:

刚刚检查了一下。

master 是一个包含约 20,000,000 条记录的表,id 是其 PRIMARY KEY

以下是查询语句:

WITH    q AS
        (
        SELECT  *, ROW_NUMBER() OVER (ORDER BY id) AS rn
        FROM    master
        ),
        t AS 
        (
        SELECT  1 AS id
        UNION ALL
        SELECT  2
        )
SELECT  *
FROM    t
JOIN    q
ON      q.rn <= t.id

这个运行时间几乎为30秒,而这个:

WITH    t AS 
        (
        SELECT  1 AS id
        UNION ALL
        SELECT  2
        )
SELECT  *
FROM    t
CROSS APPLY
        (
        SELECT  TOP (t.id) m.*
        FROM    master m
        ORDER BY
                id
        ) q

是即时的。


3
请查看 Ariel 链接的末尾。row_number() 查询同样好用,甚至不需要连接。因此,我认为在这种情况下不应该使用 cross apply(选择前 3 个,按 t1.id 分区)。 - Jeff Meatball Yang
474
虽然这是最流行的答案,但我认为它并没有回答实际问题“使用CROSS APPLY的主要目的是什么”。主要目的是使带有参数的表函数能够针对每一行执行一次,然后将结果连接起来。 - MikeKulls
7
你好,@Mike问如何称呼使用INNER JOINTVF - Quassnoi
22
是的,但是OP并没有询问使用CROSS APPLY的主要目的,他询问了何时选择它而不是INNER JOIN,即在同样情况下可以使用INNER JOIN的时候。 - ErikE
14
值得一提的是,在标准(ANSI)SQL中,这被称为“lateral join”。 - user330315
显示剩余12条评论

294

假设你有两个表。

主表

x------x--------------------x
| Id   |        Name        |
x------x--------------------x
|  1   |          A         |
|  2   |          B         |
|  3   |          C         |
x------x--------------------x

详细信息表

x------x--------------------x-------x
| Id   |      PERIOD        |   QTY |
x------x--------------------x-------x
|  1   |   2014-01-13       |   10  |
|  1   |   2014-01-11       |   15  |
|  1   |   2014-01-12       |   20  |
|  2   |   2014-01-06       |   30  |
|  2   |   2014-01-08       |   40  |
x------x--------------------x-------x

有很多情况下我们需要将INNER JOIN替换为CROSS APPLY

1. 基于TOP n结果连接两个表

考虑如果我们需要从Master选择IdName,以及每个Id的最后两个日期从Details table中选择。

SELECT M.ID,M.NAME,D.PERIOD,D.QTY
FROM MASTER M
INNER JOIN
(
    SELECT TOP 2 ID, PERIOD,QTY 
    FROM DETAILS D      
    ORDER BY CAST(PERIOD AS DATE)DESC
)D
ON M.ID=D.ID

上述查询生成以下结果。

x------x---------x--------------x-------x
|  Id  |   Name  |   PERIOD     |  QTY  |
x------x---------x--------------x-------x
|   1  |   A     | 2014-01-13   |  10   |
|   1  |   A     | 2014-01-12   |  20   |
x------x---------x--------------x-------x

看,它生成了最近两个日期的结果,并使用最近两个日期的Id在外部查询中连接这些记录,这是错误的。这应该返回Ids 1和2,但它只返回了1,因为1具有最近的两个日期。为了实现这一点,我们需要使用CROSS APPLY

SELECT M.ID,M.NAME,D.PERIOD,D.QTY
FROM MASTER M
CROSS APPLY
(
    SELECT TOP 2 ID, PERIOD,QTY 
    FROM DETAILS D  
    WHERE M.ID=D.ID
    ORDER BY CAST(PERIOD AS DATE)DESC
)D

并生成以下结果。

x------x---------x--------------x-------x
|  Id  |   Name  |   PERIOD     |  QTY  |
x------x---------x--------------x-------x
|   1  |   A     | 2014-01-13   |  10   |
|   1  |   A     | 2014-01-12   |  20   |
|   2  |   B     | 2014-01-08   |  40   |
|   2  |   B     | 2014-01-06   |  30   |
x------x---------x--------------x-------x

这是它的工作原理。 CROSS APPLY 中的查询可以引用外部表格,而 INNER JOIN 不能这样做(它会出现编译错误)。当查找最后两个日期时,在 CROSS APPLY 内部执行连接操作,即 WHERE M.ID=D.ID

2. 当我们需要使用函数实现 INNER JOIN 功能时。

当我们需要从 Master 表和一个 function 获取结果时,可以使用 CROSS APPLY 替换 INNER JOIN

SELECT M.ID,M.NAME,C.PERIOD,C.QTY
FROM MASTER M
CROSS APPLY dbo.FnGetQty(M.ID) C

以下是该函数的代码:

CREATE FUNCTION FnGetQty 
(   
    @Id INT 
)
RETURNS TABLE 
AS
RETURN 
(
    SELECT ID,PERIOD,QTY 
    FROM DETAILS
    WHERE ID=@Id
)

生成了以下结果:

x------x---------x--------------x-------x
|  Id  |   Name  |   PERIOD     |  QTY  |
x------x---------x--------------x-------x
|   1  |   A     | 2014-01-13   |  10   |
|   1  |   A     | 2014-01-11   |  15   |
|   1  |   A     | 2014-01-12   |  20   |
|   2  |   B     | 2014-01-06   |  30   |
|   2  |   B     | 2014-01-08   |  40   |
x------x---------x--------------x-------x

CROSS APPLY 的其他优点

APPLY 可以用作 UNPIVOT 的替代方法。可以使用 CROSS APPLYOUTER APPLY,这两者是可以互换的。

考虑您拥有以下表格(名为 MYTABLE)。

x------x-------------x--------------x
|  Id  |   FROMDATE  |   TODATE     |
x------x-------------x--------------x
|   1  |  2014-01-11 | 2014-01-13   | 
|   1  |  2014-02-23 | 2014-02-27   | 
|   2  |  2014-05-06 | 2014-05-30   | 
|   3  |     NULL    |    NULL      |
x------x-------------x--------------x

以下是查询内容。

SELECT DISTINCT ID,DATES
FROM MYTABLE 
CROSS APPLY(VALUES (FROMDATE),(TODATE))
COLUMNNAMES(DATES)

这可以帮您得到结果。

  x------x-------------x
  | Id   |    DATES    |
  x------x-------------x
  |  1   |  2014-01-11 |
  |  1   |  2014-01-13 |
  |  1   |  2014-02-23 |
  |  1   |  2014-02-27 |
  |  2   |  2014-05-06 |
  |  2   |  2014-05-30 | 
  |  3   |    NULL     | 
  x------x-------------x

6
两个文件和四个文件的出色示例让我理解了需要这种情况的背景。 - trnelson
1
对于第一点,我们针对ID 1只有2行而不是4行的情况,难道我们不应该使用左连接吗? - Joseph Cho

222

CROSS APPLY有时可以让你做一些用INNER JOIN无法完成的事情。

例如(语法错误):

select F.* from sys.objects O  
inner join dbo.myTableFun(O.name) F   
on F.schema_id= O.schema_id

这是一个语法错误,因为在使用inner join时,表函数只能接受变量或常量作为参数。(也就是说,表函数参数不能依赖于另一个表的列。)

然而:

select F.* from sys.objects O  
cross apply ( select * from dbo.myTableFun(O.name) ) F  
where F.schema_id= O.schema_id

这是合法的。

编辑: 或者,更短的语法:(由ErikE提供)

select F.* from sys.objects O  
cross apply dbo.myTableFun(O.name) F
where F.schema_id= O.schema_id

编辑:

注意: Informix 12.10 xC2+拥有Lateral Derived Tables,而Postgresql(9.3+)拥有Lateral Subqueries,它们可以用于类似的效果。


13
我认为这就是我们拥有CROSS APPLY的推理原因。如果你查看下面的链接,这是微软关于CROSS APPLY的第一件事情。它可能有其他用途,但我认为这就是引入它的原因。如果没有它,表函数将无法在许多情况下使用。 http://technet.microsoft.com/en-us/library/ms175156.aspx - MikeKulls
Cross Apply与内联表函数配合使用时,不仅能保持所需的模块化,还能产生良好的执行计划。 - nurettin
17
CROSS APPLY 内部不需要使用 SELECT 语句。请尝试使用 CROSS APPLY dbo.myTableFun(O.name) F - ErikE
2
@ErikE 当然可以使用不太灵活的语法来进行交叉应用。我展示的是更通用的版本,有时可以使用它来避免将难以计算的列带入查询中。 - nurettin
我认为重点在于:inner join ( select * from dbo.myTableFun(O.name) ) F 也是合法的。因此,如果您想强调差异,最好使用较短的语法。 - Bolu
4
如果表函数参数依赖于另一个表的列(也称为外部引用)在外部查询中,内连接将无法工作。如果表函数参数是文字或变量,则它将起作用。交叉应用在两种情况下都有效。 - nurettin

47

在复杂/嵌套查询中,似乎CROSS APPLY可以填补计算字段方面的某些空白,并使它们更简单和可读。

简单例子:您有一个DoB,并且希望呈现多个与年龄相关的字段,这些字段还将依赖于其他数据源(例如就业)(如Age,AgeGroup,AgeAtHiring,MinimumRetirementDate等),以供最终用户应用程序(例如Excel PivotTables)使用。

选项有限且很少优雅:

  • JOIN子查询不能基于父查询中的数据引入数据集中的新值(它必须独立进行)。

  • UDF很好,但由于它们倾向于防止并行操作,因此速度较慢。而且作为一种独立实体可能是好(代码更少)或不好(代码在哪里)的事情。

  • Junction表。有时它们可以工作,但很快您将连接具有大量UNION的子查询。糟糕的情况。

  • 创建另一个专用视图,假设您的计算不需要在主查询进行中获取的数据。

  • 中介表。是的……那通常有效,并且通常是一个好选择,因为它们可以被索引和快速处理,但是由于UPDATE语句不是并行的,并且不允许级联公式(重用结果)以在同一语句中更新多个字段,因此性能也可能会降低。有时您只想一次完成所有工作。

  • 嵌套查询。是的,在任何时候,您都可以在整个查询上放置括号,并将其用作子查询,然后可以操作源数据和计算字段。但是,在变得丑陋之前,您只能这样做很多次。非常难看。

  • 重复代码。3个长(CASE ... ELSE ... END)语句的最大值是什么?那将是可读的!

    • 告诉您的客户自己计算这些东西。

我是否漏掉了什么?可能是的,欢迎评论。但嘿,CROSS APPLY在这种情况下就像是一件天赐之物:只需添加一个简单的 CROSS APPLY (select tbl.value + 1 as someFormula) as crossTbl,然后瞧!你的新字段现在几乎可以像源数据中一直存在的那样使用了。

通过CROSS APPLY引入的值可以...

  • 用于创建一个或多个计算字段,而不会增加性能、复杂性或可读性问题
  • 就像JOIN一样,多个后续的CROSS APPLY语句可以相互引用:CROSS APPLY (select crossTbl.someFormula + 1 as someMoreFormula) as crossTbl2
  • 您可以在后续的JOIN条件中使用CROSS APPLY引入的值
  • 作为奖励,还有表值函数方面的优点

该死,他们什么都能做!


1
这对我来说是一个很大的加分项,因为我很惊讶它没有被更频繁地提到。也许你可以扩展这个例子,展示如何在派生值链上执行“过程式”计算?例如:CROSS APPLY(select crossTbl.value * tbl.multiplier as Multiplied)multiTbl - CROSS APPLY(select multiTbl.Multiplied / tbl.DerivativeRatio as Derived)derivedTbl - 等等... - mrmillsy
1
有关如何使用Cross Apply替换CASE..ELSE..END的更多信息/示例吗? - przemo_li
3
APPLY可用于存储case语句(和其他内容)的结果以供引用。结构可以如下:SELECT CASE when subquery.intermediateResult > 0 THEN "是" ELSE "否" END FROM someTable OUTER APPLY (select CASE...END...ELSE作为中间结果) as subquery。 - mtone
使用CROSS APPLY进行计算的示例:http://col.sql.drylib.com/ - alpav

37

这个问题已经在技术上得到了很好的解答,但是让我举一个具体的例子来说明它是如何非常有用的:

假设你有两个表,客户和订单。客户有很多订单。

我想创建一个视图,给我关于客户的详细信息,并显示他们最近的订单。仅使用连接,这将需要一些自连接和聚合,这不太美观。但是使用Cross Apply,这非常容易:

SELECT *
FROM Customer
CROSS APPLY (
  SELECT TOP 1 *
  FROM Order
  WHERE Order.CustomerId = Customer.CustomerId
  ORDER BY OrderDate DESC
) T

迄今为止我读过的最佳答案 - Yahya
1
使用TOP 2会使这个例子更好,因为用GROUP BYMAX复制TOP 1并不难,但是要复制TOP N(N > 1)却相当困难。 - Clement Cherlin

14

使用CROSS APPLY 也可以很好地处理XML字段。如果您希望在组合其他字段的情况下选择节点值。

例如,如果您有一个包含一些 XML 的表:

<root>
    <subnode1>
       <some_node value="1" />
       <some_node value="2" />
       <some_node value="3" />
       <some_node value="4" />
    </subnode1>
</root>

使用查询

SELECT
       id as [xt_id]
      ,xmlfield.value('(/root/@attribute)[1]', 'varchar(50)') root_attribute_value
  ,node_attribute_value = [some_node].value('@value', 'int')
  ,lt.lt_name   
FROM dbo.table_with_xml xt
CROSS APPLY xmlfield.nodes('/root/subnode1/some_node') as g ([some_node])
LEFT OUTER JOIN dbo.lookup_table lt
ON [some_node].value('@value', 'int') = lt.lt_id

将返回结果

xt_id root_attribute_value node_attribute_value lt_name
----------------------------------------------------------------------
1     test1            1                    Benefits
1     test1            4                    FINRPTCOMPANY

12

交叉应用可以用来替代子查询,当你需要子查询的某一列时

子查询

select * from person p where
p.companyId in(select c.companyId from company c where c.companyname like '%yyy%')

在这里,我将无法选择公司表的列,因此使用交叉应用(cross apply)

select P.*,T.CompanyName
from Person p
cross apply (
    select *
    from Company C
    where p.companyid = c.companyId and c.CompanyName like '%yyy%'
) T

9
以下是一份简要的教程,可保存在.sql文件中,并在SSMS中执行,我为自己编写了这份教程,以便快速回顾如何使用CROSS APPLY以及何时使用它:
-- Here's the key to understanding CROSS APPLY: despite the totally different name, think of it as being like an advanced 'basic join'.
-- A 'basic join' gives the Cartesian product of the rows in the tables on both sides of the join: all rows on the left joined with all rows on the right.
-- The formal name of this join in SQL is a CROSS JOIN.  You now start to understand why they named the operator CROSS APPLY.

-- Given the following (very) simple tables and data:
CREATE TABLE #TempStrings ([SomeString] [nvarchar](10) NOT NULL);
CREATE TABLE #TempNumbers ([SomeNumber] [int] NOT NULL);
CREATE TABLE #TempNumbers2 ([SomeNumber] [int] NOT NULL);
INSERT INTO #TempStrings VALUES ('111'); INSERT INTO #TempStrings VALUES ('222');
INSERT INTO #TempNumbers VALUES (111); INSERT INTO #TempNumbers VALUES (222);
INSERT INTO #TempNumbers2 VALUES (111); INSERT INTO #TempNumbers2 VALUES (222); INSERT INTO #TempNumbers2 VALUES (222);

-- Basic join is like CROSS APPLY; 2 rows on each side gives us an output of 4 rows, but 2 rows on the left and 0 on the right gives us an output of 0 rows:
SELECT
    st.SomeString, nbr.SomeNumber
FROM -- Basic join ('CROSS JOIN')
    #TempStrings st, #TempNumbers nbr
    -- Note: this also works:
    --#TempStrings st CROSS JOIN #TempNumbers nbr

-- Basic join can be used to achieve the functionality of INNER JOIN by first generating all row combinations and then whittling them down with a WHERE clause:
SELECT
    st.SomeString, nbr.SomeNumber
FROM -- Basic join ('CROSS JOIN')
    #TempStrings st, #TempNumbers nbr
WHERE
    st.SomeString = nbr.SomeNumber

-- However, for increased readability, the SQL standard introduced the INNER JOIN ... ON syntax for increased clarity; it brings the columns that two tables are
-- being joined on next to the JOIN clause, rather than having them later on in the WHERE clause.  When multiple tables are being joined together, this makes it
-- much easier to read which columns are being joined on which tables; but make no mistake, the following syntax is *semantically identical* to the above syntax:
SELECT
    st.SomeString, nbr.SomeNumber
FROM -- Inner join
    #TempStrings st INNER JOIN #TempNumbers nbr ON st.SomeString = nbr.SomeNumber

-- Because CROSS APPLY is generally used with a subquery, the subquery's WHERE clause will appear next to the join clause (CROSS APPLY), much like the aforementioned
-- 'ON' keyword appears next to the INNER JOIN clause.  In this sense, then, CROSS APPLY combined with a subquery that has a WHERE clause is like an INNER JOIN with
-- an ON keyword, but more powerful because it can be used with subqueries (or table-valued functions, where said WHERE clause can be hidden inside the function).
SELECT
    st.SomeString, nbr.SomeNumber
FROM
    #TempStrings st CROSS APPLY (SELECT * FROM #TempNumbers tempNbr WHERE st.SomeString = tempNbr.SomeNumber) nbr

-- CROSS APPLY joins in the same way as a CROSS JOIN, but what is joined can be a subquery or table-valued function.  You'll still get 0 rows of output if
-- there are 0 rows on either side, and in this sense it's like an INNER JOIN:
SELECT
    st.SomeString, nbr.SomeNumber
FROM
    #TempStrings st CROSS APPLY (SELECT * FROM #TempNumbers tempNbr WHERE 1 = 2) nbr

-- OUTER APPLY is like CROSS APPLY, except that if one side of the join has 0 rows, you'll get the values of the side that has rows, with NULL values for
-- the other side's columns.  In this sense it's like a FULL OUTER JOIN:
SELECT
    st.SomeString, nbr.SomeNumber
FROM
    #TempStrings st OUTER APPLY (SELECT * FROM #TempNumbers tempNbr WHERE 1 = 2) nbr

-- One thing CROSS APPLY makes it easy to do is to use a subquery where you would usually have to use GROUP BY with aggregate functions in the SELECT list.
-- In the following example, we can get an aggregate of string values from a second table based on matching one of its columns with a value from the first
-- table - something that would have had to be done in the ON clause of the LEFT JOIN - but because we're now using a subquery thanks to CROSS APPLY, we
-- don't need to worry about GROUP BY in the main query and so we don't have to put all the SELECT values inside an aggregate function like MIN().
SELECT
    st.SomeString, nbr.SomeNumbers
FROM
    #TempStrings st CROSS APPLY (SELECT SomeNumbers = STRING_AGG(tempNbr.SomeNumber, ', ') FROM #TempNumbers2 tempNbr WHERE st.SomeString = tempNbr.SomeNumber) nbr
-- ^ First the subquery is whittled down with the WHERE clause, then the aggregate function is applied with no GROUP BY clause; this means all rows are
--   grouped into one, and the aggregate function aggregates them all, in this case building a comma-delimited string containing their values.

DROP TABLE #TempStrings;
DROP TABLE #TempNumbers;
DROP TABLE #TempNumbers2;

6

这篇文章详细解释了SQL Server中CROSS APPLY和OUTER APPLY与JOINS之间的性能差异和用法。

SQL Server CROSS APPLY和OUTER APPLY相对于JOINS的使用

正如本文所建议的,对于普通连接操作(INNER和CROSS),它们之间没有性能差异。

enter image description here

当你需要进行这样的查询时,使用差异就出现了:

CREATE FUNCTION dbo.fn_GetAllEmployeeOfADepartment(@DeptID AS INT)  
RETURNS TABLE 
AS 
RETURN 
   ( 
   SELECT * FROM Employee E 
   WHERE E.DepartmentID = @DeptID 
   ) 
GO 

SELECT * FROM Department D 
CROSS APPLY dbo.fn_GetAllEmployeeOfADepartment(D.DepartmentID)

也就是说,当您需要与函数相关联时,不能使用INNER JOIN,否则将出现错误“无法绑定多部分标识符“D.DepartmentID””。在这里,该值作为每行读取时传递给函数。听起来很酷 :)

5

我想你应该是希望它更易读 ;)

CROSS APPLY 对于读者来说可能有些独特,它告诉读者正在使用一个UDF,这将被应用于左侧表中的每一行。

当然,在其他限制条件下,CROSS APPLY 比 JOIN 更好,这是其他朋友在上面发帖中提到的。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接