Пример из реальной жизни, когда использовать OUTER / CROSS APPLY в SQL

124

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

Я потратил довольно много времени на то, чтобы узнать, когда мне следует использовать перекрестное применение вместо внутреннего соединения? и поиск в Google, но основной (единственный) пример кажется довольно странным (используя количество строк из таблицы, чтобы определить, сколько строк выбрать из другой таблицы).

Я подумал, что этому сценарию может быть полезно OUTER APPLY:

Таблица контактов (содержит по 1 записи для каждого контакта) Таблица записей связи (может содержать n телефонных номеров, факсов, электронной почты для каждого контакта)

Но использование подзапросов, общих табличных выражений OUTER JOINс RANK()и, OUTER APPLYпохоже, работает одинаково. Я предполагаю, что это означает, что сценарий неприменим к APPLY.

Пожалуйста, поделитесь некоторыми примерами из реальной жизни и помогите объяснить эту функцию!

Ли Тикетт
источник
5
"top n на группу" или синтаксический анализ XML является обычным явлением. См. Некоторые из моих ответов stackoverflow.com/…
gbn,
1
объяснятьextended.com
2009/07/16/inner-join-vs-cross-apply
Проверьте здесь также stackoverflow.com/questions/27838045/where-to-use-outer-apply
Сарат Аванаву,

Ответы:

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) Отмена поворота более чем одной группы столбцов

Предполагается, что 1НФ нарушает структуру таблицы ....

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. Я использовал оба этих подхода (ROW OVER и CROSS APPLY), оба они хорошо работали в различных сценариях, но я никогда не понимал, почему они работают по-разному. Эта статья была послана с небес !! Сосредоточение внимания на правильной индексации в соответствии с порядком по направлениям в значительной степени помогло запросам, которые имеют «правильную» структуру, но при запросе возникают проблемы с производительностью. Спасибо, что включили это !!
Крис Портер,
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функциональность с использованием functions.

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функциональностью

Подумайте, нужно ли нам выбирать 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таблицы и 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для переноса 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
Итак, чтобы уточнить. Сценарий «наверху»; можно ли это сделать с помощью левого / внутреннего соединения, но с использованием «row_number по разделу по идентификатору», а затем выбрать «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
BJury
источник
в наших тестах мы всегда находили соединение с оконной функцией наиболее эффективным для вершины n (я думал, что это всегда будет верно, так как apply и subquery являются курсивными / требующими вложенными циклами). хотя я думаю, что теперь, возможно, я его взломал ... благодаря ссылке Мартина, которая предполагает, что если вы не возвращаете всю таблицу и в таблице нет оптимальных индексов, то количество чтений будет намного меньше с использованием перекрестного применения (или подзапрос if top n, где n = 1)
Ли Тикетт,
По сути, у меня есть этот запрос, и он определенно не выполняет никаких подзапросов с вложенными циклами. Учитывая, что таблица журнала имеет PK taskID и lastUpdateDate, это очень быстрая операция. Как бы вы преобразовали этот запрос, чтобы использовать оконную функцию?
BJury
2
select * from task t внутреннее соединение (select taskid, logresult, lastupdatedate, rank () over (разделение по taskid по порядку lastupdatedate desc) _rank) lg на lg.taskid = t.taskid и lg._rank = 1
Ли Тикетт,
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

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

BJury
источник
план выполнения меня интересует - знаете ли вы, почему решение rank () выполняет сканирование индекса и дорогостоящую сортировку, в отличие от внешнего применения, которое выполняет поиск по индексу и, похоже, не выполняет сортировку (хотя это необходимо, потому что вы можете '' разве топ без сортировки?)
Ли Тикетт,
1
Внешнему приложению не требуется выполнять сортировку, так как он может использовать индекс базовой таблицы. Предположительно, запрос с функцией rank () должен обработать всю таблицу, чтобы обеспечить правильное ранжирование.
BJury
не получишь топ без сорта. хотя ваша точка зрения об обработке всей таблицы МОЖЕТ быть правдой, это меня удивит (я знаю, что оптимизатор / компилятор sql может время от времени разочаровывать, но это было бы сумасшедшим поведением)
Ли Тикетт
2
Вы можете возглавить вершину без сортировки, когда данные, по которым вы группируете, относятся к индексу, поскольку оптимизатор знает, что он уже отсортирован, поэтому буквально нужно просто вытащить первую (или последнюю) запись из индекса.
BJury