现实生活中的例子,何时在SQL中使用OUTER/CROSS APPLY

Lee*_*ett 119 sql sql-server sql-server-2005 sql-server-2008

我一直在CROSS / OUTER APPLY和同事一起看,我们正在努力寻找现实生活中使用它们的例子.

我花了很多时间看看我何时应该使用Cross Apply而不是Inner Join?和谷歌搜索但主要(唯一)的例子似乎很奇怪(使用表中的行数来确定从另一个表中选择多少行).

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

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

但是使用子查询,公共表表达式,OUTER JOINRANK()OUTER APPLY似乎都同样执行.我猜这意味着该方案不适用于APPLY.

请分享一些现实生活中的例子并帮助解释这个功能!

Mar*_*ith 169

一些用途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 
Run Code Online (Sandbox Code Playgroud)

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

SELECT *
FROM sys.dm_exec_query_stats AS qs
CROSS APPLY sys.dm_exec_query_plan(qs.plan_handle)
Run Code Online (Sandbox Code Playgroud)

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

4) 取消隐藏多组列

假设违反表结构的1NF ....

CREATE TABLE T
  (
     Id   INT PRIMARY KEY,

     Foo1 INT, Foo2 INT, Foo3 INT,
     Bar1 INT, Bar2 INT, Bar3 INT
  ); 
Run Code Online (Sandbox Code Playgroud)

使用2008+ VALUES语法的示例.

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

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

  • 那里有一个很好的用途清单,但关键是现实生活中的例子——我很想看到每个例子。 (2认同)
  • 对于#1,这可以使用排名、子查询或公用表表达式同样实现吗?当这不是真的时,你能举个例子吗? (2认同)
  • 确保访问示例 #1 中包含的链接。我已经使用过这两种方法(ROW OVER 和 CROSS APPLY),它们在各种场景中都表现良好,但我一直不明白为什么它们的表现不同。那篇文章是天上送来的!!专注于按方向匹配顺序的正确索引对具有“正确”结构但在查询时出现性能问题的查询有很大帮助。谢谢你把它包括在内!! (2认同)
  • @mr_eclair 看起来现在在 http://www.itprotoday.com/software-development/optimizing-top-n-group-queries (2认同)

Sar*_*avu 82

有各种情况你无法避免CROSS APPLYOUTER APPLY.

考虑你有两张桌子.

主表

x------x--------------------x
| Id   |        Name        |
x------x--------------------x
|  1   |          A         |
|  2   |          B         |
|  3   |          C         |
x------x--------------------x
Run Code Online (Sandbox Code Playgroud)

详情表

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



                                                            交叉申请

有很多情况下,我们需要更换INNER JOINCROSS APPLY.

1.如果我们想要在功能上加入2个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
Run Code Online (Sandbox Code Playgroud)

以上查询生成以下结果.

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

看,它生成最后两个日期的结果与最后两个日期的结果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
Run Code Online (Sandbox Code Playgroud)

并形成他追随的结果.

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

这是工作.里面的查询CROSS APPLY可以引用外部表,在那里INNER JOIN不能这样做(抛出编译错误).当找到最后两个日期时,加入在内部完成,CROSS APPLYWHERE M.ID=D.ID.

2.当我们需要INNER JOIN使用功能的功能时.

CROSS APPLYINNER JOIN当我们需要从Master表和a 获得结果时,可以用作替代function.

SELECT M.ID,M.NAME,C.PERIOD,C.QTY
FROM MASTER M
CROSS APPLY dbo.FnGetQty(M.ID) C
Run Code Online (Sandbox Code Playgroud)

这是功能

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

产生了以下结果

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



                                                            外面申请

1.如果我们想要在功能上加入2个TOP n结果表LEFT JOIN

考虑我们是否需要MasterDetails表中选择Id和Name 以及每个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
Run Code Online (Sandbox Code Playgroud)

形成以下结果

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

这将带来错误的结果,即,即使我们加入,它也只会从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
Run Code Online (Sandbox Code Playgroud)

这形成了以下期望的结果

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

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

OUTER APPLYLEFT JOIN当我们需要从Master表和a 获得结果时,可以用作替代function.

SELECT M.ID,M.NAME,C.PERIOD,C.QTY
FROM MASTER M
OUTER APPLY dbo.FnGetQty(M.ID) C
Run Code Online (Sandbox Code Playgroud)

功能就在这里.

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

产生了以下结果

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



                             的共同特点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
Run Code Online (Sandbox Code Playgroud)

当您使用AND 将一个列UNPIVOT引入时,它将默认消除值.FROMDATETODATENULL

SELECT ID,DATES
FROM MYTABLE
UNPIVOT (DATES FOR COLS IN (FROMDATE,TODATE)) P
Run Code Online (Sandbox Code Playgroud)

产生以下结果.请注意,我们已经错过了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
Run Code Online (Sandbox Code Playgroud)

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

SELECT DISTINCT ID,DATES
FROM MYTABLE 
OUTER APPLY(VALUES (FROMDATE),(TODATE))
COLUMNNAMES(DATES)
Run Code Online (Sandbox Code Playgroud)

它形成以下结果并保留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
Run Code Online (Sandbox Code Playgroud)

  • 总体来说答案很好!当然,这是一个比公认的答案更好的答案,因为它很简单,有方便的视觉示例和解释。 (3认同)
  • 与其在两个问题上发布完全相同的答案,为什么不将其中一个标记为重复? (2认同)
  • 我发现此答案更适合回答原始问题。其示例显示了“现实生活”场景。 (2认同)
  • 非常感谢您详细且易于理解的答案! (2认同)
  • 我认为这应该是公认的答案。它显示了标题所说的“现实生活”示例。 (2认同)

BJu*_*ury 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
Run Code Online (Sandbox Code Playgroud)

  • select * from task t inner join (select taskid, logresult, lastupdatedate, rank() over(partition by taskid order by lastupdatedate desc) _rank) lg on lg.taskid = t.taskid and lg._rank = 1 (2认同)

BJu*_*ury 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
Run Code Online (Sandbox Code Playgroud)

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

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

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

  • 外部应用不需要执行排序,因为它可以使用基础表上的索引。据推测,带有 rank() 函数的查询需要处理整个表以确保其排名正确。 (2认同)
  • 当分组依据的数据与索引不符时,您可以不进行排序而排在首位,因为优化器知道其已经排序,因此从字面上仅需从索引中拉出第一个(或最后一个)条目即可。 (2认同)