Пример из реальной жизни, когда использовать 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,




Ответы:


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

в наших тестах мы всегда находили соединение с оконной функцией наиболее эффективным для вершины 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

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


план выполнения меня интересует - знаете ли вы, почему решение rank () выполняет сканирование индекса и дорогостоящую сортировку, в отличие от внешнего применения, которое выполняет поиск по индексу и, похоже, не выполняет сортировку (хотя это необходимо, потому что вы можете '' разве топ без сортировки?)
— Ли Тикетт,

1
Внешнему приложению не требуется выполнять сортировку, так как он может использовать индекс базовой таблицы. Предположительно, запрос с функцией rank () должен обработать всю таблицу, чтобы обеспечить правильное ранжирование.
— BJury

не получишь топ без сорта. хотя ваша точка зрения об обработке всей таблицы МОЖЕТ быть правдой, это меня удивит (я знаю, что оптимизатор / компилятор sql может время от времени разочаровывать, но это было бы сумасшедшим поведением)
— Ли Тикетт

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