Приклад із реального життя, коли використовувати OUTER / CROSS APPLY в SQL


124

Я дивився CROSS / OUTER APPLYз колегою і ми намагаємося знайти приклади реального життя, де їх використовувати.

Я витратив досить багато часу на перегляд, коли я повинен використовувати Cross Apply через внутрішнє приєднання ? та googling, але головний (єдиний) приклад здається досить химерним (використовуючи кількість рядків з таблиці, щоб визначити, скільки рядків вибрати з іншої таблиці).

Я думав, що цей сценарій може отримати користь від OUTER APPLY:

Таблиця контактів (містить 1 запис для кожного контакту) Таблиця записів комунікацій (може містити n телефону, факсу, електронної пошти для кожного контакту)

Але використання підзапитів, загальних табличних виразів, OUTER JOINз RANK()і OUTER APPLYвсе , здається, працюють однаково. Я здогадуюсь, це означає, що сценарій не застосовується до APPLY.

Поділіться кількома прикладами з реального життя та допоможіть пояснити цю особливість!


5
"Топ n для групи" або розбір XML є загальним. Дивіться деякі з моїх відповідей stackoverflow.com / ...
ГБН




Відповіді:


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

Приклад з використанням VALUESсинтаксису 2008+ .

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 цього можна досягти однаково, використовуючи вирази рангів, підзапитів або загальних таблиць? Чи можете ви навести приклад, коли це не відповідає дійсності?
Лі Тікетт

@LeeTickett - прочитайте посилання. У ньому обговорюється 4 сторінки про те, коли ви віддасте перевагу одному.
Мартін Сміт

1
Не забудьте відвідати посилання, включене в приклад №1. Я використовував обидва ці підходи (РОЗДІЛЬНІ та КРІШНІ ЗАСТОСУВАННЯ) з обома результатами в різних сценаріях, але ніколи не розумів, чому вони працюють по-різному. Ця стаття була надіслана з небес !! Зосередженість на належній індексації, що відповідає порядку за вказівками, значною мірою допомогла запитам, які мають "належну" структуру, але проблеми із виконанням запитів. Дякую за включення !!
Кріс Портер

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. Якщо ми хочемо об'єднати 2 таблиці за TOP nрезультатами з INNER JOINфункціональністю

Поміркуйте, чи потрібно нам вибрати Idі Nameвід, 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таблиці та a 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функціональністю

Поміркуйте, чи потрібно нам вибрати ідентифікатор та ім’я з 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таблиці та a 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для підведення FROMDATEІ 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

У таких випадках a 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

Тож для уточнення. Сценарій "верхній п"; це можна зробити з лівим / внутрішнім з'єднанням, але з використанням "row_number over partition by id", а потім вибору "WHERE M.RowNumber <3" або щось подібне?
Чайтанья

1
Чудова відповідь загалом! Напевно, це краща відповідь, ніж прийнята, адже це: простий, із зручними наочними прикладами та поясненнями.
Арсен Хачатурян

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

у наших тестах ми завжди знаходили об'єднання з функцією вікна, найбільш ефективною для верхнього n (я вважав, що це завжди буде правдою, оскільки застосовуватись і підзапит, як скорочувальний / вимагають вкладених циклів). хоча я думаю, що я, можливо, зараз його зламав ... завдяки посиланню Мартіна, яке говорить про те, що якщо ти не повертаєш всю таблицю, а в таблиці немає оптимальних індексів, то кількість читань буде значно меншою за допомогою крос-застосунку (або підзапит, якщо верх n, де n = 1)
Лі Тікетт

Я отримав по суті той запит саме тут, і він, звичайно, не виконує жодного підзапиту з вкладеними петлями. З огляду на те, що таблиця журналу містить ПК задачіID та lastUpdateDate, це дуже швидка робота. Як би ви реформували цей запит, щоб використовувати функцію вікна?
BJury

2
виберіть * із завдання t внутрішнє з'єднання (виберіть taskid, logresult, lastupdateate, rank () over (розділ за наказом taskid за lastupdateate desc) _rank) lg on 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

Ви бачите, що зовнішній запит застосувати є більш ефективним. (Не вдалося долучити план, оскільки я новий користувач ... Дох.)


план виконання мене цікавить - чи знаєте ви, чому рішення rank () виконує сканування індексу та дорогий сорт на відміну від зовнішнього застосунку, який шукає індекс, а не здається, що це робить (хоча це потрібно, тому що ви можете " т робити топ без роду?)
Лі Тікетт

1
Зовнішнє застосування не потребує сортування, оскільки воно може використовувати індекс у нижній таблиці. Імовірно, запит з функцією rank () повинен обробити всю таблицю, щоб переконатися, що її ранжування є правильним.
БЮР

ви не можете зробити топ без роду. хоча ваша думка про обробку всієї таблиці МОЖЕ бути правдою, це мене здивує (я знаю, оптимізатор / компілятор sql може час від часу розчаровуватись, але це було б шаленою поведінкою)
Lee Tickett

2
Ви можете зверху без сортування, коли дані, за якими групується, проти індексу, оскільки оптимізатор знає, що його вже відсортовано, так що буквально просто потрібно витягнути з індексу перший (або останній) запис.
БЮР
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.