実際の例、SQLでOUTER / CROSS APPLYを使用する場合


124

私はCROSS / OUTER APPLY同僚と見ていましたが、実際に使用する場所の例を見つけるのに苦労しています。

私は内部結合よりもクロス適用をいつ使用すべきかを検討するのにかなりの時間を費やしましたか?グーグルするが、メイン(唯一)の例はかなり奇妙に見えます(テーブルの行数を使用して、別のテーブルから選択する行数を決定します)。

このシナリオには次のようなメリットがあると思いましたOUTER APPLY

連絡先テーブル(連絡先ごとに1つのレコードが含まれます)通信エントリテーブル(連絡先ごとに電話、ファックス、電子メールを含めることができます)

しかし、サブクエリ、共通テーブル式を使用しOUTER JOINRANK()し、OUTER APPLYすべてが同じように実行するように見えます。これは、シナリオがに該当しないことを意味していると思いAPPLYます。

実際の例をいくつか共有し、機能の説明にご協力ください!


5
「グループごとの上位n」またはXMLの解析が一般的です。私の答えのいくつかを参照してくださいstackoverflow.com/...
GBN




回答:


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
そこでの使用の素晴らしいリストですが、鍵は実際の例です-それぞれについて1つ見たいと思います。
リーチケット

#1の場合、これはランク、サブクエリ、または共通のテーブル式を使用して同等に達成できますか?これが真実ではない場合の例を提供できますか?
リーチケット

@LeeTickett-リンクをお読みください。それはあなたがいつ別のものを好むかについて4ページの議論があります。
マーティン・スミス

1
例1に含まれているリンクに必ずアクセスしてください。私はこれらのアプローチ(ROW OVERとCROSS APPLY)の両方をさまざまなシナリオで適切に実行して使用しましたが、なぜ異なる動作をするのか理解できません。その記事は天から送られました!方向による順序に一致する適切なインデックス付けに焦点を当てることで、「適切な」構造を持つクエリで大きな効果が得られましたが、クエリの実行時にパフォーマンスの問題が発生しました。含めてくれてありがとう!!
Chris Porter

1
@mr_eclairそれのようなルックスがで今itprotoday.com/software-development/...
マーティン・スミス

87

CROSS APPLYまたはを回避できないさまざまな状況がありますOUTER APPLY

2つのテーブルがあるとします。

マスターテーブル

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結果で2つのテーブルを結合する場合INNER JOIN

私たちが選択する必要がある場合を検討IdしてNameから、Masterそれぞれの最後の2つの日付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

ご覧のとおり、最後の2つの日付の結果を最後の2つの日付で生成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。最後の2つの日付を見つけると、結合はCROSS APPLYie 内で行われWHERE M.ID=D.IDます。

2. INNER JOIN関数を使用した機能が必要な場合。

CROSS APPLYtableとa INNER JOINから結果を取得する必要がある場合の代替として使用できます。Masterfunction

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結果で2つのテーブルを結合する場合LEFT JOIN

テーブルMasterから各IdのIdとName from と最後の2つの日付を選択する必要があるかどうかを検討しDetailsます。

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テーブルから最新の2つの日付データのみをもたらします。したがって、適切な解決策はを使用することです。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 APPLYtableとa LEFT JOINから結果を取得する必要がある場合の代替として使用できます。Masterfunction

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

を使用UNPIVOTしてFROMDATEAND TODATEを1つの列に配置すると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 APPLYまたはOUTER 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

2つの質問にまったく同じ答えを投稿する代わりに、1つを重複としてフラグを立ててみませんか?
Tab Alleman、2015

2
この答えは、元の質問に答えるのに適していると思います。その例は「実際の」シナリオを示しています。
FrankO 2016

明確にするために。「トップn」シナリオ。これは、左/内部結合で実行できますが、「IDによるパーティション上の行番号」を使用してから、「WHERE M.RowNumber <3」またはそのようなものを選択しますか?
Chaitanya 2018年

1
全体的に素晴らしい答えです!確かに、これは受け入れられたものよりも良い答えです。それは、シンプルであり、便利な視覚的な例と説明があるためです。
Arsen Khachaturyan

8

実際の例の1つは、スケジューラがあり、スケジュールされた各タスクの最新のログエントリを確認したい場合です。

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に対して最も効率的であることがわかりました(適用とサブクエリが両方とも筆記体/必須の入れ子ループであるため、これは常に正しいと思いました)。私は今それをクラックしたかもしれないと思うが...あなたがテーブル全体を返さず、テーブルに最適なインデックスがない場合、クロス適用を使用すると読み取りの数がはるかに少なくなることを示唆するマーティンのリンクのおかげで(またはサブクエリif top n where n = 1)
Lee Tickett

私は本質的にそのクエリをここに持っていますが、ネストされたループでサブクエリを実行することはありません。ログテーブルにtaskIDとlastUpdateDateのPKがある場合、その操作は非常に高速です。ウィンドウ関数を使用するようにそのクエリをどのように修正しますか?
BJury

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

次に、実行プランを使用して2つのクエリを実行します。

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

外側の適用クエリの方が効率的であることがわかります。(私は新しいユーザーなので、プランを添付できませんでした... Doh。)


実行計画は私に興味を持っています-インデックスシークを実行してソートを実行しないように見える外部適用とは対照的に、rank()ソリューションがインデックススキャンと高コストのソートを実行する理由を知っていますか? t並べ替えなしでトップを実行しますか?)
リーチケット

1
外側の適用では、基になるテーブルのインデックスを使用できるため、ソートを実行する必要はありません。おそらく、rank()関数を使用したクエリは、テーブル全体を処理して、ランキングが正しいことを確認する必要があります。
BJury

あなたはソートなしでトップを行うことはできません。テーブル全体の処理に関するあなたの主張は真実である可能性がありますが、私は驚かれるでしょう(SQLオプティマイザー/コンパイラーが時々失望することは知っていますが、これはクレイジーな振る舞いになるでしょう)
Lee Tickett

2
オプティマイザは既に並べ替えられていることをオプティマイザが知っているので、文字通り最初の(または最後の)エントリをインデックスから削除する必要があるので、グループ化のデータがインデックスに対するものである場合、並べ替えなしでトップをトップできます。
BJury
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.