실제 예, SQL에서 OUTER / CROSS APPLY를 사용하는 경우


124

저는 CROSS / OUTER APPLY동료와 함께 살펴 보았고 우리는 그것들을 어디에 사용할 것인지에 대한 실제 사례를 찾기 위해 고군분투하고 있습니다.

Inner Join보다 Cross Apply언제 사용해야하나요? 인터넷 검색이 가능하지만 주된 (유일한) 예제는 매우 이상해 보입니다 (테이블의 행 개수를 사용하여 다른 테이블에서 선택할 행 수를 결정).

이 시나리오가 다음과 같은 이점을 얻을 수 있다고 생각했습니다 OUTER APPLY.

연락처 테이블 (각 연락처에 대해 1 개의 레코드 포함) 통신 항목 테이블 (각 연락처에 대해 n 개의 전화, 팩스, 이메일 포함 가능)

그러나 하위 쿼리, 공통 테이블 표현식을 사용하면 OUTER JOINwith RANK()OUTER APPLYall이 동일하게 수행되는 것 같습니다. 이것은 시나리오가에 적용되지 않는다는 것을 의미한다고 생각 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
좋은 사용 목록이 있지만 핵심은 실제 사례입니다. 각각에 대해 하나씩보고 싶습니다.
Lee Tickett 2012

# 1의 경우 순위, 하위 쿼리 또는 공통 테이블 식을 사용하여 동일하게 달성 할 수 있습니까? 이것이 사실이 아닌 경우에 예를 제공 할 수 있습니까?
Lee Tickett 2012

@LeeTickett-링크를 읽어주세요. 언제 서로를 선호하는지에 대한 4 페이지 토론이 있습니다.
Martin Smith

1
예제 # 1에 포함 된 링크를 방문하십시오. 저는이 두 가지 접근 방식 (ROW OVER 및 CROSS APPLY)을 모두 다양한 시나리오에서 잘 수행했지만 왜 다르게 수행되는지 이해하지 못했습니다. 그 기사는 하늘에서 보냈습니다 !! 방향 별 순서와 일치하는 적절한 인덱싱에 대한 초점은 "적절한"구조를 가지고 있지만 쿼리 할 때 성능 문제가있는 쿼리에 큰 도움이되었습니다. 포함 해주셔서 감사합니다 !!
Chris Porter

1
@mr_eclair 그 같은 외모에 지금 itprotoday.com/software-development/...
마틴 스미스

87

피할 수없는 상황 CROSS APPLY이나 OUTER 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 JOIN와 함께 CROSS APPLY.

1. 기능이있는 TOP n결과 에 대해 2 개의 테이블을 조인하려는 경우INNER JOIN

우리가 선택해야 할 경우 고려 IdName에서 Master각 마지막 두 날짜 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한 다음 의 외부 쿼리에서만 이러한 레코드를 결합했습니다 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 APPLY와 교체로 사용할 수 있습니다 INNER JOIN우리가에서 결과를 얻을 필요가있을 때 Master테이블과 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결과 에 대해 2 개의 테이블을 조인하려는 경우LEFT JOIN

테이블의 Master각 Id에 대해 Id 및 Name from 과 마지막 두 날짜 를 선택해야하는지 고려하십시오 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테이블 에서 최근 두 날짜 데이터 만 가져옵니다 . 따라서 적절한 솔루션은 .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 APPLY와 교체로 사용할 수 있습니다 LEFT JOIN우리가에서 결과를 얻을 필요가있을 때 Master테이블과 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 APPLY및의 공통 기능OUTER 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

AND 를 하나의 열로 UNPIVOT가져 오는 데 사용하면 기본적으로 값 이 제거 됩니다.FROMDATETODATENULL

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

두 질문에 똑같은 답변을 게시하는 대신 하나를 중복으로 표시하지 않는 이유는 무엇입니까?
Tab Alleman 2015 년

2
이 답변이 원래 질문에 대한 답변에 더 적합하다고 생각합니다. 그 예는 '실제'시나리오를 보여줍니다.
FrankO

그래서 명확하게. "상위 n"시나리오; 왼쪽 / 내부 조인으로 수행 할 수 있지만 "id 별 파티션의 행 번호"를 사용하고 "WHERE M.RowNumber <3"또는 이와 유사한 것을 선택하면됩니다.
Chaitanya

1
전반적으로 훌륭한 답변입니다! 확실히 이것은 허용되는 것보다 더 나은 대답입니다. 왜냐하면 간단하고 편리한 시각적 예제와 설명이 있기 때문입니다.
Arsen Khachaturyan

9

실제 사례 중 하나는 스케줄러가 있고 각 예약 된 작업에 대한 최신 로그 항목이 무엇인지 확인하려는 경우입니다.

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

우리의 테스트에서 우리는 항상 top n에 대해 join with window 함수가 가장 효율적이라는 것을 발견했습니다 (적용 및 하위 쿼리가 모두 필기체 / 필수 중첩 루프이기 때문에 항상 참이라고 생각했습니다). 내가 지금 그것을 깨 뜨렸을지도 모른다고 생각하지만 ... 전체 테이블을 반환하지 않고 테이블에 최적의 인덱스가 없다면 교차 적용을 사용하여 읽기 횟수가 훨씬 더 적을 것이라고 제안하는 Martin의 링크 덕분에 (또는 n = 1 인 경우 상위 n 인 경우 하위 쿼리)
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

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

외부 적용 쿼리가 더 효율적임을 알 수 있습니다. (신규 사용자이므로 계획을 첨부 할 수 없습니다 ... Doh.)


실행 계획에 관심이 있습니다. rank () 솔루션이 인덱스 스캔을 수행하고 인덱스 검색을 수행하고 정렬을 수행하지 않는 것으로 보이는 외부 적용과는 달리 값 비싼 정렬을 수행하는 이유를 알고 있습니까? t 정렬없이 정상을합니까?)
Lee Tickett

1
외부 적용은 기본 테이블의 인덱스를 사용할 수 있으므로 정렬을 수행 할 필요가 없습니다. 아마도 rank () 함수가있는 쿼리는 순위가 올바른지 확인하기 위해 전체 테이블을 처리해야합니다.
BJury

당신은 정렬없이 정상을 할 수 없습니다. 전체 테이블을 처리에 대한 요점은 사실이 될 수 있지만 그것은 나를 놀라게 할 것이다 (내가 알고있는 SQL 최적화 / 컴파일러 때때로 실망 할 수 있지만,이 미친 행동 일 것입니다)
리 Tickett

2
옵티마이 저가 이미 정렬 된 데이터를 알고 있으므로 문자 그대로 인덱스에서 첫 번째 (또는 마지막) 항목을 가져와야하기 때문에 그룹화 기준 데이터가 인덱스에 대해있을 때 정렬하지 않고 최상위를 차지할 수 있습니다.
BJury
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.