Contoh kehidupan nyata, ketika menggunakan OUTER / CROSS APPLY di SQL

124

Saya telah melihat CROSS / OUTER APPLYdengan seorang kolega dan kami berjuang untuk menemukan contoh kehidupan nyata tentang di mana menggunakannya.

Saya telah menghabiskan cukup banyak waktu untuk melihat Kapan saya harus menggunakan Cross Apply daripada Inner Join? dan googling tetapi contoh utama (satu-satunya) tampaknya cukup aneh (menggunakan rowcount dari tabel untuk menentukan berapa banyak baris yang harus dipilih dari tabel lain).

Saya pikir skenario ini mungkin mendapat manfaat dari OUTER APPLY:

Tabel Kontak (berisi 1 record untuk setiap kontak) Tabel Entri Komunikasi (dapat berisi n telepon, fax, email dari setiap kontak)

Tetapi menggunakan subkueri, ekspresi tabel umum, OUTER JOINdengan RANK()dan OUTER APPLYsemua tampaknya bekerja sama. Saya menduga ini berarti skenario tersebut tidak berlaku untuk APPLY.

Harap bagikan beberapa contoh kehidupan nyata dan bantu menjelaskan fitur tersebut!

Lee Tickett
sumber
5
"n teratas per grup" atau parsing XML adalah hal biasa. Lihat beberapa jawaban saya stackoverflow.com/…
gbn

Jawaban:

174

Beberapa kegunaannya APPLYadalah ...

1) N teratas per kueri grup (bisa lebih efisien untuk beberapa kardinalitas)

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) Memanggil Fungsi Nilai Tabel untuk setiap baris di kueri luar

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

3) Menggunakan kembali alias kolom

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) Memisahkan lebih dari satu grup kolom

Mengasumsikan 1NF melanggar struktur tabel ....

CREATE TABLE T
  (
     Id   INT PRIMARY KEY,

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

Contoh menggunakan VALUESsintaks 2008+ .

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

Pada tahun 2005 UNION ALLbisa digunakan sebagai gantinya.

SELECT Id,
       Foo,
       Bar
FROM   T
       CROSS APPLY (SELECT Foo1, Bar1 
                    UNION ALL
                    SELECT Foo2, Bar2 
                    UNION ALL
                    SELECT Foo3, Bar3) V(Foo, Bar);
Martin Smith
sumber
1
Daftar kegunaan yang bagus di sana tetapi kuncinya adalah contoh kehidupan nyata- saya ingin sekali melihatnya untuk masing-masing.
Lee Tickett
Untuk # 1 ini dapat dicapai dengan menggunakan peringkat, subkueri atau ekspresi tabel umum? Dapatkah Anda memberikan contoh jika ini tidak benar?
Lee Tickett
@LeeTickett - Silakan baca tautannya. Ini memiliki 4 halaman diskusi tentang kapan Anda lebih memilih satu sama lain.
Martin Smith
1
Pastikan untuk mengunjungi tautan yang termasuk dalam contoh # 1. Saya telah menggunakan kedua pendekatan ini (ROW OVER dan CROSS APPLY) dengan keduanya berkinerja baik dalam berbagai skenario, tetapi saya tidak pernah mengerti mengapa mereka bekerja secara berbeda. Artikel itu dikirim dari surga !! Fokus pada pengindeksan yang tepat yang cocok dengan urutan menurut petunjuk sangat membantu untuk kueri yang memiliki struktur "tepat" tetapi memiliki masalah kinerja saat ditanyai. Terima kasih telah memasukkannya !!
Chris Porter
1
@mr_eclair sepertinya sekarang ada di itprotoday.com/software-development/…
Martin Smith
87

Ada berbagai situasi di mana Anda tidak dapat menghindari CROSS APPLYatau OUTER APPLY.

Anggaplah Anda memiliki dua tabel.

TABEL MASTER

x------x--------------------x
| Id   |        Name        |
x------x--------------------x
|  1   |          A         |
|  2   |          B         |
|  3   |          C         |
x------x--------------------x

TABEL DETAIL

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                                       



                                                            LINTAS BERLAKU

Ada banyak situasi di mana kita perlu mengganti INNER JOINdengan CROSS APPLY.

1. Jika kita ingin menggabungkan 2 tabel pada TOP nhasil dengan INNER JOINfungsionalitas

Pertimbangkan apakah kita perlu memilih Iddan Namedari Masterdan dua tanggal terakhir untuk masing-masing Iddari 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

Kueri di atas menghasilkan hasil sebagai berikut.

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

Lihat, itu menghasilkan hasil untuk dua tanggal terakhir dengan dua tanggal terakhir Iddan kemudian menggabungkan catatan ini hanya di kueri luar pada Id, yang salah. Untuk mencapai ini, kita perlu menggunakan 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

dan bentuk dia mengikuti hasil.

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

Inilah pekerjaannya. Kueri di dalam CROSS APPLYbisa mereferensikan tabel luar, di mana INNER JOINtidak bisa melakukan ini (melempar kesalahan kompilasi). Saat menemukan dua tanggal terakhir, penggabungan dilakukan di dalam CROSS APPLYyaitu WHERE M.ID=D.ID.

2. Saat kita membutuhkan INNER JOINfungsionalitas menggunakan fungsi.

CROSS APPLYdapat digunakan sebagai pengganti INNER JOINketika kita membutuhkan hasil dari Mastertabel dan a function.

SELECT M.ID,M.NAME,C.PERIOD,C.QTY
FROM MASTER M
CROSS APPLY dbo.FnGetQty(M.ID) C

Dan inilah fungsinya

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

yang menghasilkan hasil sebagai berikut

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



                                                            TERAPKAN LUAR

1. Jika kita ingin menggabungkan 2 tabel pada TOP nhasil dengan LEFT JOINfungsionalitas

Pertimbangkan jika kita perlu memilih Id dan Nama dari Masterdan dua tanggal terakhir untuk setiap Id dari Detailstabel.

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

yang membentuk hasil sebagai berikut

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

Ini akan membawa hasil yang salah, yaitu hanya akan membawa dua data tanggal terakhir dari Detailstabel terlepas dari Idmeskipun kita bergabung Id. Jadi solusi yang tepat adalah menggunakan 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

yang membentuk hasil yang diinginkan berikut

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. Saat kita membutuhkan LEFT JOINfungsionalitas menggunakan functions.

OUTER APPLYdapat digunakan sebagai pengganti LEFT JOINketika kita membutuhkan hasil dari Mastertabel dan a function.

SELECT M.ID,M.NAME,C.PERIOD,C.QTY
FROM MASTER M
OUTER APPLY dbo.FnGetQty(M.ID) C

Dan fungsinya di sini.

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

yang menghasilkan hasil sebagai berikut

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



                             Fitur umum CROSS APPLYdanOUTER APPLY

CROSS APPLYatau OUTER APPLYdapat digunakan untuk mempertahankan NULLnilai saat tidak berputar, yang dapat dipertukarkan.

Pertimbangkan Anda memiliki tabel di bawah ini

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

Saat Anda menggunakan UNPIVOTuntuk membawa FROMDATEDAN TODATEke satu kolom, ini akan menghilangkan NULLnilai secara default.

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

yang menghasilkan hasil di bawah ini. Perhatikan bahwa kami melewatkan rekor Idnomor3

  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

Dalam kasus seperti itu, a CROSS APPLYatau OUTER APPLYakan berguna

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

yang membentuk hasil berikut dan mempertahankan di Idmana nilainya3

  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
Sarath Avanavu
sumber
Daripada memposting jawaban yang persis sama untuk dua pertanyaan, mengapa tidak menandai salah satu sebagai duplikat?
Tab Alleman
2
Saya menemukan jawaban ini lebih dapat diterapkan untuk menjawab pertanyaan asli. Contohnya menunjukkan skenario 'kehidupan nyata'.
FrankO
Jadi untuk memperjelas. Skenario "top n"; dapatkah itu dilakukan dengan left / inner join tetapi menggunakan "row_number over partition by id" dan kemudian memilih "WHERE M.RowNumber <3" atau sesuatu seperti itu?
Chaitanya
1
Jawaban bagus secara keseluruhan! Yang pasti ini adalah jawaban yang lebih baik daripada yang diterima, karena: sederhana, dengan contoh visual yang praktis, dan penjelasan.
Arsen Khachaturyan
9

Salah satu contoh nyata adalah jika Anda memiliki penjadwal dan ingin melihat entri log terbaru apa untuk setiap tugas terjadwal.

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
BJury
sumber
dalam pengujian kami, kami selalu menemukan join dengan fungsi jendela yang paling efisien untuk n teratas (saya pikir ini akan selalu benar karena berlaku dan subquery keduanya kursif / membutuhkan loop bersarang). meskipun saya pikir saya mungkin sekarang telah memecahkannya ... berkat tautan Martin yang menyarankan jika Anda tidak mengembalikan seluruh tabel dan tidak ada indeks optimal pada tabel maka jumlah pembacaan akan jauh lebih kecil menggunakan penerapan silang (atau subkueri jika n teratas di mana n = 1)
Lee Tickett
Saya pada dasarnya punya kueri itu di sini dan yang pasti tidak melakukan subkueri apa pun dengan loop bersarang. Mengingat tabel log memiliki PK taskID dan lastUpdateDate, operasi yang sangat cepat. Bagaimana Anda mereformasi kueri itu untuk menggunakan fungsi jendela?
BJury
2
pilih * dari tugas t inner join (pilih taskid, logresult, lastupdatedate, rank () over (partisi by taskid order by lastupdatedate desc) _rank) lg di lg.taskid = t.taskid dan lg._rank = 1
Lee Tickett
5

Untuk menjawab point diatas ketuk contoh:

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

Dan sekarang jalankan dua kueri dengan rencana eksekusi.

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

Anda dapat melihat bahwa kueri penerapan luar lebih efisien. (Tidak dapat melampirkan paket karena saya adalah pengguna baru ... Doh.)

BJury
sumber
rencana pelaksanaan menarik minat saya - apakah Anda tahu mengapa solusi rank () melakukan pemindaian indeks dan pengurutan yang mahal dibandingkan dengan penerapan luar yang mencari indeks dan tampaknya tidak melakukan pengurutan (meskipun harus karena Anda bisa ' t melakukan top tanpa semacam?)
Lee Tickett
1
Penerapan luar tidak perlu melakukan pengurutan, karena dapat menggunakan indeks pada tabel yang mendasari. Agaknya kueri dengan fungsi rank () perlu memproses seluruh tabel untuk memastikan rangkingnya benar.
BJury
Anda tidak dapat melakukan top tanpa semacam itu. meskipun maksud Anda tentang memproses seluruh tabel BISA benar, itu akan mengejutkan saya (saya tahu pengoptimal / kompiler sql dapat mengecewakan dari waktu ke waktu tetapi ini akan menjadi perilaku gila)
Lee Tickett
2
Anda dapat menduduki puncak tanpa pengurutan saat data pengelompokan Anda bertentangan dengan indeks, karena pengoptimal tahu bahwa data tersebut sudah diurutkan sehingga benar-benar hanya perlu menarik entri pertama (atau terakhir) dari indeks.
BJury