实际示例,何时在SQL中使用OUTER / CROSS APPLY


124

我一直在CROSS / OUTER APPLY和一位同事一起研究,我们正在努力寻找在哪里使用它们的真实示例。

我已经花了很多时间研究什么时候应该在内部联接上使用交叉申请?和谷歌搜索,但主要的(唯一的)示例似乎很奇怪(使用表中的行数来确定要从另一个表中选择多少行)。

我认为这种情况可能会受益于OUTER APPLY

联系人表(每个联系人包含1条记录)通信条目表(每个联系人可以包含n个电话,传真,电子邮件)

但是,使用子查询,通用表表达式OUTER JOIN以及RANK()OUTER APPLY似乎都表现相同。我猜这意味着该方案不适用于APPLY

请分享一些真实的例子,并帮助解释该功能!


5
“每个组的前n个”或解析XML很常见。见我的一些回答stackoverflow.com/...
GBN




Answers:


174

的一些用途APPLY是...

1) 每组查询的前N个(某些基数可能更有效)

SELECT pr.name,
       pa.name
FROM   sys.procedures pr
       OUTER APPLY (SELECT TOP 2 *
                    FROM   sys.parameters pa
                    WHERE  pa.object_id = pr.object_id
                    ORDER  BY pr.name) pa
ORDER  BY pr.name,
          pa.name 

2)为外部查询中的每一行调用一个表值函数

SELECT *
FROM sys.dm_exec_query_stats AS qs
CROSS APPLY sys.dm_exec_query_plan(qs.plan_handle)

3) 重用列别名

SELECT number,
       doubled_number,
       doubled_number_plus_one
FROM master..spt_values
CROSS APPLY (SELECT 2 * CAST(number AS BIGINT)) CA1(doubled_number)  
CROSS APPLY (SELECT doubled_number + 1) CA2(doubled_number_plus_one)  

4) 取消不止一组列

假设1NF违反了表格结构。

CREATE TABLE T
  (
     Id   INT PRIMARY KEY,

     Foo1 INT, Foo2 INT, Foo3 INT,
     Bar1 INT, Bar2 INT, Bar3 INT
  ); 

使用2008+ VALUES语法的示例。

SELECT Id,
       Foo,
       Bar
FROM   T
       CROSS APPLY (VALUES(Foo1, Bar1),
                          (Foo2, Bar2),
                          (Foo3, Bar3)) V(Foo, Bar); 

在2005年UNION ALL可以代替使用。

SELECT Id,
       Foo,
       Bar
FROM   T
       CROSS APPLY (SELECT Foo1, Bar1 
                    UNION ALL
                    SELECT Foo2, Bar2 
                    UNION ALL
                    SELECT Foo3, Bar3) V(Foo, Bar);

1
那里有很多用途,但关键是现实生活中的例子-我很乐意为每个例子看看。
Lee Tickett'2

对于#1,可以使用等级,子查询或公用表表达式来实现?如果事实并非如此,您能提供一个例子吗?
Lee Tickett

@LeeTickett-请阅读链接。它有一个4页的讨论,讨论何时需要彼此相较。
马丁·史密斯

1
确保访问示例1中包含的链接。我在这两种情况下都使用了这两种方法(ROW OVER和CROSS APPLY),它们在各种情况下都表现良好,但是我从来不理解为什么它们的表现会有所不同。那篇文章是从天上来的!对于按方向匹配顺序的正确索引的关注在很大程度上有助于具有“适当”结构但在查询时出现性能问题的查询。谢谢您的加入!!
克里斯·波特


87

在许多情况下,您无法避免CROSS APPLYOUTER APPLY

考虑您有两个表。

主表

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 JOINCROSS APPLY

1.如果我们想将TOP n具有INNER JOIN功能的结果的两个表连接起来

试想,如果我们需要选择IdNameMaster和最后两个日期为每个IdDetails 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,然后仅在上的外部查询中加入了这些记录Id,这是错误的。为此,我们需要使用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使用功能的功能时。

CROSS APPLYINNER JOIN当需要从Mastertable和a 获取结果时,可以用作的替代function

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



                                                            外层申请

1.如果我们想将TOP n具有LEFT JOIN功能的结果的两个表连接起来

考虑是否需要选择Id和Name from Master以及Details表中每个ID的最后两个日期。

SELECT M.ID,M.NAME,D.PERIOD,D.QTY
FROM MASTER M
LEFT 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   |
|   2  |   B     |   NULL       |  NULL |
|   3  |   C     |   NULL       |  NULL |
x------x---------x--------------x-------x

这将带来错误的结果,即,即使我们与联接,也将仅从Details表中获得最新的两个日期数据。所以正确的解决方案是使用IdIdOUTER APPLY

SELECT M.ID,M.NAME,D.PERIOD,D.QTY
FROM MASTER M
OUTER 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   |
|   3  |   C     |   NULL       |  NULL |
x------x---------x--------------x-------x

2.当我们需要LEFT JOIN使用以下功能时functions

OUTER APPLYLEFT JOIN当需要从Mastertable和a 获取结果时,可以用作的替代function

SELECT M.ID,M.NAME,C.PERIOD,C.QTY
FROM MASTER M
OUTER 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   |
|   3  |   C     |   NULL       |  NULL |
x------x---------x--------------x-------x



                             的共同特点CROSS APPLYOUTER APPLY

CROSS APPLY 要么 OUTER APPLY可用于NULL在取消透视时保留可互换的值。

考虑你有下表

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

当您用于UNPIVOTFROMDATEAND TODATE带到一列时,NULL默认情况下它将消除值。

SELECT ID,DATES
FROM MYTABLE
UNPIVOT (DATES FOR COLS IN (FROMDATE,TODATE)) P

产生以下结果。请注意,我们错过了Id数字记录3

  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 |
  x------x-------------x

在这种情况下,CROSS APPLYOUTER APPLY将很有用

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

形成以下结果并保留Id其值为3

  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

与其在两个问题上发布完全相同的答案,不如将一个标记为重复?
Tab Alleman 2015年

2
我发现此答案更适合回答原始问题。其示例显示了“现实生活”场景。
FrankO '02

因此要澄清。“前n个”方案;可以通过左/内连接来完成,但是使用“按ID划分行号”,然后选择“ WHERE M.RowNumber <3”或类似的方法吗?
Chaitanya

1
总体来说,答案很好!可以肯定的是,这是比公认的更好的答案,因为它是:简单,带有方便的可视示例和说明。
Arsen Khachaturyan,

8

一个现实的例子是,如果您有一个调度程序,并且想查看每个调度任务的最新日志条目。

select t.taskName, lg.logResult, lg.lastUpdateDate
from task t
cross apply (select top 1 taskID, logResult, lastUpdateDate
             from taskLog l
             where l.taskID = t.taskID
             order by lastUpdateDate desc) lg

在我们的测试中,我们总是发现与窗口函数的连接对于前n个最有效(我认为这始终是正确的,因为apply和subquery都是草书/要求嵌套循环)。尽管我认为我现在可能已经破解了它……多亏了Martin的链接,该链接建议如果您不返回整个表并且表上没有最佳索引,则使用交叉应用(或一个子查询(如果前n个,其中n = 1)
Lee Tickett 2012年

我基本上在这里有该查询,它肯定不会执行带有嵌套循环的任何子查询。给定日志表具有taskID和lastUpdateDate的PK,这是一个非常快速的操作。您将如何重构该查询以使用窗口函数?
BJury 2012年

2
从任务t内部联接中选择*(选择taskid,logresult,lastupdatedate,rank()over(按taskid的顺序按lastupdatedate desc进行分区)_rank)lg.taskid = t.taskid和lg._rank = 1
Lee Tickett

5

要回答以上几点,请举一个例子:

create table #task (taskID int identity primary key not null, taskName varchar(50) not null)
create table #log (taskID int not null, reportDate datetime not null, result varchar(50) not null, primary key(reportDate, taskId))

insert #task select 'Task 1'
insert #task select 'Task 2'
insert #task select 'Task 3'
insert #task select 'Task 4'
insert #task select 'Task 5'
insert #task select 'Task 6'

insert  #log
select  taskID, 39951 + number, 'Result text...'
from    #task
        cross join (
            select top 1000 row_number() over (order by a.id) as number from syscolumns a cross join syscolumns b cross join syscolumns c) n

现在,使用执行计划运行两个查询。

select  t.taskID, t.taskName, lg.reportDate, lg.result
from    #task t
        left join (select taskID, reportDate, result, rank() over (partition by taskID order by reportDate desc) rnk from #log) lg
            on lg.taskID = t.taskID and lg.rnk = 1

select  t.taskID, t.taskName, lg.reportDate, lg.result
from    #task t
        outer apply (   select  top 1 l.*
                        from    #log l
                        where   l.taskID = t.taskID
                        order   by reportDate desc) lg

您可以看到外部Apply查询更加有效。(由于我是新用户,所以无法附加该计划。。。。)


执行计划使我感兴趣-您知道为什么rank()解决方案进行索引扫描和昂贵的排序,而不是进行索引查找并且似乎没有进行排序的外部应用(尽管这样做必须这样做,因为您可以这样做)难道不做排序吗?)
Lee Tickett 2012年

1
外部应用不需要执行排序,因为它可以使用基础表上的索引。假定具有rank()函数的查询需要处理整个表,以确保其排名正确。
BJury 2012年

你不能没有排序就做到最好。尽管您关于处理整个表的观点可能是正确的,但这会让我感到惊讶(我知道sql优化器/编译器有时会令人失望,但这将是疯狂的行为)
Lee Tickett 2012年

2
当分组依据的数据与索引不符时,您可以不进行排序而排在首位,因为优化器知道其已经排序,因此从字面上仅需从索引中拉出第一个(或最后一个)条目即可。
BJury 2012年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.