关于sql:我什么时候应该使用cross apply over inner join?

When should I use cross apply over inner join?

使用交叉应用的主要目的是什么?

我(隐约地,通过互联网上的帖子)读到,如果要分区的话,在选择大型数据集时,cross apply可以更高效。(想到寻呼)

我也知道,cross apply不需要UDF作为正确的表。

在大多数INNER JOIN查询(一对多关系)中,我可以重写它们以使用cross apply,但它们总是给我等价的执行计划。

有人能给我一个很好的例子,说明cross apply在那些INNER JOIN也会起作用的情况下,什么时候起作用?

编辑:

下面是一个简单的例子,其中执行计划完全相同。(给我看一个不同的地方和cross apply更快/更有效的地方)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
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


Can anyone give me a good example of when CROSS APPLY makes a difference in those cases where INNER JOIN will work as well?

有关详细的性能比较,请参阅我的博客中的文章:

  • INNER JOINCROSS APPLY的比较

CROSS APPLY在没有简单JOIN条件的事物上工作得更好。

本次从t2中选择3最后一条记录作为来自t1的每条记录:

1
2
3
4
5
6
7
8
9
10
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和window函数执行类似的操作:

1
2
3
4
5
6
7
8
9
10
11
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记录的表,PRIMARY KEYid上。

此查询:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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

运行了几乎13秒,而这一秒:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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

瞬间。


CROSS APPLY有时能使你做一些你不能用INNER JOIN做的事情。

示例(语法错误):

1
2
3
SELECT F.* FROM sys.objects O  
INNER JOIN dbo.myTableFun(O.name) F  
ON F.schema_id= O.schema_id

这是一个语法错误,因为当与INNER JOIN一起使用时,表函数只能将变量或常量作为参数。(即,表函数参数不能依赖于另一个表的列。)

然而:

1
2
3
SELECT F.* FROM sys.objects O  
CROSS apply ( SELECT * FROM dbo.myTableFun(O.name) ) F  
WHERE F.schema_id= O.schema_id

这是合法的。

编辑:或者,更短的语法:(由Erike编写)

1
2
3
SELECT F.* FROM sys.objects O  
CROSS apply dbo.myTableFun(O.name) F
WHERE F.schema_id= O.schema_id

编辑:

注:Informix 12.10 XC2+有横向派生表,PostgreSQL(9.3+)有横向子查询,可以使用类似的效果。


假设你有两张桌子。

主表

1
2
3
4
5
6
7
x------x--------------------x
| Id   |        Name        |
x------x--------------------x
|  1   |          A         |
|  2   |          B         |
|  3   |          C         |
x------x--------------------x

细节表

1
2
3
4
5
6
7
8
9
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

在很多情况下,我们需要用CROSS APPLY替换INNER JOIN

1。根据TOP n结果联接两个表

考虑是否需要从Master中选择IdName,从Details table中选择每个Id的最后两个日期。

1
2
3
4
5
6
7
8
9
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
  • SQL小提琴

上述查询生成以下结果。

1
2
3
4
5
6
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生成最后两个日期的结果,然后只在Id的外部查询中加入这些记录,这是错误的。为此,我们需要使用CROSS APPLY

1
2
3
4
5
6
7
8
9
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
  • SQL小提琴

形成以下结果。

1
2
3
4
5
6
7
8
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

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

这里是函数

1
2
3
4
5
6
7
8
9
10
11
12
CREATE FUNCTION FnGetQty
(  
    @Id INT
)
RETURNS TABLE
AS
RETURN
(
    SELECT ID,PERIOD,QTY
    FROM DETAILS
    WHERE ID=@Id
)
  • SQL小提琴

结果如下

1
2
3
4
5
6
7
8
9
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

交叉应用的附加优势

APPLY可作为UNPIVOT的替代品。这里可以使用CROSS APPLYOUTER APPLY,它们可以互换。

假设您有下表(名为MYTABLE)。

1
2
3
4
5
6
7
8
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

查询如下。

1
2
3
4
SELECT DISTINCT ID,DATES
FROM MYTABLE
CROSS APPLY(VALUES (FROMDATE),(TODATE))
COLUMNNAMES(DATES)
  • SQL小提琴

这给你带来了结果

1
2
3
4
5
6
7
8
9
10
11
  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


在我看来,在复杂/嵌套查询中使用计算字段时,交叉应用可以填补一定的空白,并使它们更简单、更可读。

简单示例:您有一个DOB,并且希望显示多个与年龄相关的字段,这些字段还将依赖于其他数据源(如就业),如年龄、年龄组、年龄感应、最低退休日期等,以在最终用户应用程序(例如Excel数据透视表)中使用。

选项有限,很少优雅:

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

  • UDF很整洁,但速度很慢,因为它们往往会阻止并行操作。作为一个独立的实体可能是一件好事(代码较少)或坏事(代码在哪里)。

  • 接线表。有时它们可以工作,但很快您就可以将子查询与大量的联合连接起来。大乱。

  • 创建另一个单用途视图,假设您的计算不需要通过主查询中途获得数据。

  • 中间表。对。。。这通常是可行的,而且通常是一个很好的选择,因为它们可以被索引和快速,但是性能也会下降,因为更新语句不是并行的,并且不允许级联公式(重用结果)更新同一语句中的多个字段。有时候你更喜欢一次就做事情。

  • 嵌套查询。是的,在任何时候,您都可以在整个查询上加上括号,并将其用作子查询,在子查询上您可以类似地操作源数据和计算字段。但你只能在它变丑之前做这么多。非常丑陋。

  • 重复代码。3个长(case…else…end)语句的最大值是什么?那将是可读的!

    • 告诉你的客户自己计算这些该死的东西。

我错过什么了吗?可能吧,请随意评论。但是,在这种情况下,交叉应用就像是天赐之物:只需添加一个简单的CROSS APPLY (select tbl.value + 1 as someFormula) as crossTbl和voil_!您的新字段现在几乎可以像在源数据中一样使用了。

通过交叉应用引入的值可以…

  • 用于创建一个或多个计算字段,而不向组合添加性能、复杂性或可读性问题。
  • 和join一样,后面的几个交叉应用语句也可以引用它们自己:CROSS APPLY (select crossTbl.someFormula + 1 as someMoreFormula) as crossTbl2
  • 可以在后续联接条件中使用交叉应用引入的值
  • 另外,还有表值函数方面

党,他们什么都做不了!


下面是一个例子,当交叉应用与性能产生巨大差异时:

使用交叉应用优化条件之间的联接

注意,除了替换内部联接之外,您还可以重用代码,如截断日期,而不必为调用标量UDF支付性能惩罚,例如:使用内联UDF计算月份的第三个星期三


交叉应用也适用于XML字段。如果希望结合其他字段选择节点值。

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

1
2
3
4
5
6
7
8
<root>
    <subnode1>
       <some_node VALUE="1" />
       <some_node VALUE="2" />
       <some_node VALUE="3" />
       <some_node VALUE="4" />
    </subnode1>
</root>

使用查询

1
2
3
4
5
6
7
8
9
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

将返回结果

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


交叉应用可用于替换需要子查询列的子查询

子查询

1
2
SELECT * FROM person p WHERE
p.companyId IN(SELECT c.companyId FROM company c WHERE c.companyname LIKE '%yyy%')

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

1
2
3
4
5
6
7
SELECT P.*,T.CompanyName
FROM Person p
CROSS apply (
    SELECT *
    FROM Company C
    WHERE p.companyid = c.companyId AND c.CompanyName LIKE '%yyy%'
) T

我想应该是可读性;)

交叉应用对于阅读的人来说是有点独特的,他们会告诉他们正在使用一个UDF,它将应用于左侧表格中的每一行。

当然,还有其他的限制,即交叉应用比其他朋友在上面发布的连接更好地使用。


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

假设您有两张桌子,客户和订单。客户有很多订单。

我想创建一个视图,向我提供有关客户的详细信息,以及他们最近的订单。使用just join,这将需要一些自连接和聚合,但这并不美观。但是使用交叉应用,它非常容易:

1
2
3
4
5
6
7
8
SELECT *
FROM Customer
CROSS APPLY (
  SELECT TOP 1 *
  FROM ORDER
  WHERE ORDER.CustomerId = Customer.CustomerId
  ORDER BY OrderDate DESC
) T

这里有一篇文章解释了这一切,以及它们在连接上的性能差异和用法。

SQL Server交叉应用和外部应用于联接

正如本文所建议的,对于正常的连接操作(内部和交叉),它们之间没有性能差异。

enter image description here

当您必须执行如下查询时,会出现使用差异:

1
2
3
4
5
6
7
8
9
10
11
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)

也就是说,当你必须与功能相关时。使用内部联接无法完成此操作,这将导致错误"无法绑定多部分标识符"d.DepartmentID"。在这里,在读取每一行时,值将传递给函数。听起来很酷。:)


嗯,我不确定这是否符合使用交叉应用与内部连接的理由,但这个问题是在使用交叉应用的论坛帖子中回答的,所以我不确定是否有使用内部连接的相等方法:

1
2
3
4
CREATE PROCEDURE [dbo].[Message_FindHighestMatches]

-- Declare the Topical Neighborhood
@TopicalNeighborhood NCHAR(255)

AS开始

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
-- SET NOCOUNT ON added to prevent extra result sets from
-- interfering with SELECT statements.
SET NOCOUNT ON

CREATE TABLE  #temp
(
    MessageID         INT,
    Subjects          NCHAR(255),
    SubjectsCount    INT
)

INSERT INTO #temp SELECT MessageID, Subjects, SubjectsCount FROM Message

SELECT Top 20 MessageID, Subjects, SubjectsCount,
    (t.cnt * 100)/t3.inputvalues AS MatchPercentage

FROM #temp

CROSS apply (SELECT COUNT(*) AS cnt FROM dbo.Split(Subjects,',') AS t1
             JOIN dbo.Split(@TopicalNeighborhood,',') AS t2
             ON t1.value = t2.value) AS t
CROSS apply (SELECT COUNT(*) AS inputValues FROM dbo.Split(@TopicalNeighborhood,',')) AS t3

ORDER BY MatchPercentage DESC

DROP TABLE #temp

结束


APPLY运算符的本质是允许FROM子句中运算符的左侧和右侧之间存在相关性。

与join相反,不允许输入之间的相关性。

说到应用运算符中的相关性,我的意思是在右边我们可以放置:

  • 派生表-作为带有别名的相关子查询
  • 表值函数-带有参数的概念视图,其中参数可以引用左侧

两者都可以返回多个列和行。


这也许是一个古老的问题,但我仍然喜欢交叉应用的功能,它简化了逻辑的重用,并为结果提供了一个"链接"机制。

我在下面提供了一个SQL fiddle,它展示了一个简单的示例,说明如何使用交叉应用来对数据集执行复杂的逻辑操作,而不会造成任何混乱。从这里不难推断出更复杂的计算。

http://sqlfiddle.com/!3/23862/2