ตัวอย่างชีวิตจริงเมื่อใดควรใช้ OUTER / CROSS APPLY ใน SQL


124

ฉันได้ดูCROSS / OUTER APPLYกับเพื่อนร่วมงานคนหนึ่งและเรากำลังดิ้นรนเพื่อหาตัวอย่างในชีวิตจริงว่าจะนำไปใช้ที่ไหน

ฉันใช้เวลาค่อนข้างมากในการดูว่าเมื่อไหร่ควรใช้ Cross Apply ผ่าน Inner Join และ googling แต่ตัวอย่างหลัก (เท่านั้น) ดูค่อนข้างแปลก (ใช้ rowcount จากตารางเพื่อกำหนดจำนวนแถวที่จะเลือกจากตารางอื่น)

ฉันคิดว่าสถานการณ์นี้อาจได้รับประโยชน์จากOUTER APPLY:

ตารางรายชื่อ (มี 1 บันทึกสำหรับแต่ละรายชื่อ) ตารางรายการการสื่อสาร (สามารถมีโทรศัพท์โทรสารอีเมลสำหรับผู้ติดต่อแต่ละราย)

แต่การใช้ subqueries แสดงออกตารางทั่วไปOUTER JOINด้วยRANK()และOUTER APPLYทั้งหมดดูเหมือนจะดำเนินการอย่างเท่าเทียมกัน APPLYฉันคาดเดาที่นี้หมายถึงสถานการณ์ไม่สามารถใช้ได้กับ

โปรดแบ่งปันตัวอย่างชีวิตจริงและช่วยอธิบายคุณลักษณะนี้!


5
"top n ต่อกลุ่ม" หรือการแยกวิเคราะห์ XML เป็นเรื่องปกติ ดูคำตอบของฉันstackoverflow.com/…
gbn




ตรวจสอบที่นี่ด้วยstackoverflow.com/questions/27838045/where-to-use-outer-apply
Sarath Avanavu

คำตอบ:


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) การเรียกใช้ Table Valued Function สำหรับแต่ละแถวในแบบสอบถามด้านนอก

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
  ); 

ตัวอย่างการใช้VALUESไวยากรณ์2008+

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

ในปี 2548 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

สำหรับ # 1 สิ่งนี้สามารถทำได้อย่างเท่าเทียมกันโดยใช้อันดับแบบสอบถามย่อยหรือนิพจน์ทั่วไปของตาราง? คุณสามารถยกตัวอย่างเมื่อไม่เป็นความจริงได้หรือไม่?
Lee Tickett

@LeeTickett - โปรดอ่านลิงค์ มีการอภิปราย 4 หน้าเกี่ยวกับเวลาที่คุณต้องการซึ่งกันและกัน
Martin Smith

1
อย่าลืมไปที่ลิงค์ที่อยู่ในตัวอย่าง # 1 ฉันใช้ทั้งสองวิธีนี้ (ROW OVER และ CROSS APPLY) ซึ่งทั้งคู่ทำงานได้ดีในสถานการณ์ต่างๆ แต่ฉันไม่เคยเข้าใจว่าทำไมพวกเขาถึงทำงานต่างกัน บทความนั้นส่งมาจากสวรรค์ !! การให้ความสำคัญกับการจัดทำดัชนีที่เหมาะสมซึ่งตรงกับลำดับตามทิศทางช่วยได้มากสำหรับข้อความค้นหาที่มีโครงสร้าง "เหมาะสม" แต่ปัญหาด้านประสิทธิภาพเมื่อถูกสอบถาม ขอบคุณที่รวมไว้ !!
Chris Porter

1
@mr_eclair ดูเหมือนตอนนี้ที่itprotoday.com/software-development/…
Martin Smith

88

มีสถานการณ์ต่าง ๆ ที่คุณไม่สามารถหลีกเลี่ยงหรือCROSS APPLYOUTER APPLY

พิจารณาว่าคุณมีสองตาราง

ตาราง MASTER

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. หากเราต้องการเข้าร่วม 2 ตารางกับTOP nผลลัพธ์ที่มีINNER JOINฟังก์ชันการทำงาน

พิจารณาถ้าเราต้องเลือกIdและNameจากMasterและวันที่สองวันที่ผ่านมาสำหรับแต่ละจาก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 APPLYWHERE 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. หากเราต้องการเข้าร่วม 2 ตารางกับTOP nผลลัพธ์ที่มีLEFT JOINฟังก์ชันการทำงาน

พิจารณาว่าเราจำเป็นต้องเลือก Id และ Name หรือไม่Masterและสองวันสุดท้ายสำหรับแต่ละ Id จาก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ตารางโดยไม่คำนึงถึงIdแม้ว่าเราจะเข้าร่วมด้วยIdก็ตาม OUTER 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

เมื่อคุณใช้UNPIVOTเพื่อนำFROMDATEAND 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 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

2
ฉันพบว่าคำตอบนี้เหมาะกับการตอบคำถามเดิมมากขึ้น ตัวอย่างแสดงสถานการณ์ 'ชีวิตจริง'
FrankO

เพื่อชี้แจง สถานการณ์ "top n"; สามารถทำได้โดยใช้ left / inner join แต่ใช้ "row_number over partition by id" แล้วเลือก "WHERE M.RowNumber <3" หรืออะไรทำนองนั้น
Chaitanya

1
คำตอบที่ดีโดยรวม! แน่นอนว่านี่เป็นคำตอบที่ดีกว่าคำตอบที่ได้รับการยอมรับเนื่องจากเป็นคำตอบที่เรียบง่ายพร้อมตัวอย่างภาพและคำอธิบาย
Arsen Khachaturyan

ขอบคุณมากสำหรับคำตอบโดยละเอียดและเข้าใจง่าย!
Kushan Randima

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

ในการทดสอบของเราเราพบว่าฟังก์ชัน join with window มีประสิทธิภาพมากที่สุดสำหรับ n อันดับต้น ๆ เสมอ (ฉันคิดว่าสิ่งนี้จะเป็นจริงเสมอเมื่อใช้และแบบสอบถามย่อยเป็นทั้งแบบเล่นหาง / ต้องมีลูปซ้อนกัน) แม้ว่าฉันคิดว่าตอนนี้ฉันอาจแตกมันแล้ว ... ขอบคุณลิงค์ของ Martin ที่แนะนำหากคุณไม่คืนตารางทั้งหมดและไม่มีดัชนีที่เหมาะสมบนตารางจำนวนการอ่านจะน้อยลงมากโดยใช้ cross apply (หรือ แบบสอบถามย่อยถ้า n อันดับต้น ๆ โดยที่ n = 1)
Lee Tickett

โดยพื้นฐานแล้วฉันมีแบบสอบถามนั้นอยู่ที่นี่และแน่นอนว่ามันไม่ได้ดำเนินการค้นหาย่อยใด ๆ ที่มีลูปซ้อนกัน เนื่องจากตารางบันทึกมี PK ของ taskID และ lastUpdateDate จึงเป็นการดำเนินการที่รวดเร็วมาก คุณจะปฏิรูปแบบสอบถามนั้นให้ใช้ฟังก์ชันหน้าต่างได้อย่างไร
บียูรี่

2
เลือก * จากงาน t การรวมภายใน (เลือก taskid, logresult, lastupdatedate, rank () over (แบ่งพาร์ติชันตามลำดับ taskid โดย lastupdatedate desc) _rank) lg บน 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

คุณจะเห็นได้ว่าแบบสอบถามใช้ภายนอกมีประสิทธิภาพมากขึ้น (ไม่สามารถแนบแผนได้เนื่องจากฉันเป็นผู้ใช้ใหม่ ... Doh.)


แผนการดำเนินการสนใจฉัน - คุณรู้หรือไม่ว่าเหตุใดโซลูชัน rank () จึงสแกนดัชนีและการจัดเรียงที่มีราคาแพงเมื่อเทียบกับการใช้ภายนอกซึ่งค้นหาดัชนีและดูเหมือนจะไม่ทำการเรียงลำดับ (แม้ว่าจะต้องเป็นเพราะคุณสามารถ ' t ทำยอดโดยไม่เรียงลำดับ?)
Lee Tickett

1
การนำไปใช้ภายนอกไม่จำเป็นต้องทำการเรียงลำดับเนื่องจากสามารถใช้ดัชนีบนตารางพื้นฐานได้ สันนิษฐานว่าแบบสอบถามที่มีฟังก์ชัน rank () จำเป็นต้องประมวลผลทั้งตารางเพื่อให้แน่ใจว่าการจัดอันดับถูกต้อง
BJury

คุณไม่สามารถทำอันดับต้น ๆ โดยไม่เรียงลำดับ แม้ว่าประเด็นของคุณเกี่ยวกับการประมวลผลทั้งตารางอาจเป็นเรื่องจริง แต่ก็ทำให้ฉันประหลาดใจ (ฉันรู้ว่าเครื่องมือเพิ่มประสิทธิภาพ sql / คอมไพเลอร์อาจทำให้ผิดหวังเป็นครั้งคราว แต่นี่จะเป็นพฤติกรรมที่บ้าคลั่ง)
Lee Tickett

2
คุณสามารถขึ้นอันดับต้น ๆ ได้โดยไม่ต้องเรียงลำดับเมื่อข้อมูลที่คุณจัดกลุ่มโดยเทียบกับดัชนีเนื่องจากเครื่องมือเพิ่มประสิทธิภาพรู้ว่ามีการเรียงลำดับแล้วดังนั้นเพียงแค่ดึงรายการแรก (หรือสุดท้าย) ออกจากดัชนี
BJury
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.