Ejemplo de la vida real, cuándo usar OUTER / CROSS APPLY en SQL

124

He estado mirando CROSS / OUTER APPLYcon un colega y estamos luchando por encontrar ejemplos de la vida real sobre dónde usarlos.

He pasado mucho tiempo mirando ¿ Cuándo debo usar la Aplicación cruzada sobre la Unión interna? y buscar en Google, pero el ejemplo principal (solo) parece bastante extraño (usar el recuento de filas de una tabla para determinar cuántas filas seleccionar de otra tabla).

Pensé que este escenario podría beneficiarse de OUTER APPLY:

Tabla de contactos (contiene 1 registro para cada contacto) Tabla de entradas de comunicación (puede contener n teléfono, fax, correo electrónico para cada contacto)

Pero el uso de subconsultas, expresiones de tabla comunes, OUTER JOINcon RANK()y OUTER APPLYtodos parecen funcionar por igual. Supongo que esto significa que el escenario no es aplicable APPLY.

¡Comparta algunos ejemplos de la vida real y ayude a explicar la característica!

Lee Tickett
fuente
55
"top n por grupo" o análisis XML es común. Vea algunas de mis respuestas stackoverflow.com/…
gbn
posible duplicado de ¿ Cuándo debo usar Cross Apply sobre Inner Join?
Tab Alleman
Consulte aquí también stackoverflow.com/questions/27838045/where-to-use-outer-apply
Sarath Avanavu

Respuestas:

174

Algunos usos para APPLYson ...

1) N principales consultas por grupo (puede ser más eficiente para algunas cardinalidades)

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) Llamar a una función de valor de tabla para cada fila en la consulta externa

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

3) Reutilizando un alias de columna

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) Desactivar más de un grupo de columnas

Asume que 1NF viola la estructura de la tabla ...

CREATE TABLE T
  (
     Id   INT PRIMARY KEY,

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

Ejemplo usando la VALUESsintaxis 2008+ .

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

En 2005 UNION ALLse puede utilizar en su lugar.

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
fuente
1
Una buena lista de usos allí, pero la clave son los ejemplos de la vida real: me encantaría ver uno para cada uno.
Lee Tickett
Para el n. ° 1, ¿esto se puede lograr por igual utilizando rango, subconsultas o expresiones de tabla comunes? ¿Puedes dar un ejemplo cuando esto no es cierto?
Lee Tickett
@LeeTickett: lea el enlace. Tiene una discusión de 4 páginas sobre cuándo preferiría uno a otro.
Martin Smith
1
Asegúrese de visitar el enlace incluido en el ejemplo # 1. He utilizado estos dos enfoques (ROW OVER y CROSS APPLY) con un buen desempeño en varios escenarios, pero nunca he entendido por qué funcionan de manera diferente. ¡Ese artículo fue enviado desde los cielos! El enfoque en una indexación adecuada que coincida con el orden por direcciones ayudó en gran medida a las consultas que tienen una estructura "adecuada" pero problemas de rendimiento cuando se consultan. ¡Gracias por incluirlo!
Chris Porter
1
@mr_eclair parece que ahora está en itprotoday.com/software-development/…
Martin Smith
87

Hay varias situaciones en las que no puedes evitar CROSS APPLYo OUTER APPLY.

Considera que tienes dos mesas.

MESA PRINCIPAL

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

TABLA DE DETALLES

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                                       



                                                            APLICACIÓN CRUZADA

Hay muchas situaciones donde tenemos que reemplazar INNER JOINcon CROSS APPLY.

1. Si queremos unir 2 tablas de TOP nresultados con INNER JOINfuncionalidad

Considere si necesitamos seleccionar Idy Namedesde Mastery las dos últimas fechas para cada uno Idde 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

La consulta anterior genera el siguiente resultado.

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

Vea, generó resultados para las últimas dos fechas con las últimas dos fechas Idy luego unió estos registros solo en la consulta externa Id, lo cual es incorrecto. Para lograr esto, necesitamos usar 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

y forma el siguiente resultado.

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

Aquí está el trabajo. La consulta dentro CROSS APPLYpuede hacer referencia a la tabla externa, donde INNER JOINno puede hacer esto (arroja un error de compilación). Cuando la búsqueda de las dos últimas fechas, uniéndose a que se hace en el interior CROSS APPLY, es decir, WHERE M.ID=D.ID.

2. Cuando necesitamos INNER JOINfuncionalidad usando funciones.

CROSS APPLYpuede usarse como un reemplazo INNER JOINcuando necesitamos obtener resultados de la Mastertabla y a function.

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

Y aquí está la función.

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

que generó el siguiente resultado

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



                                                            APLICACIÓN EXTERIOR

1. Si queremos unir 2 tablas de TOP nresultados con LEFT JOINfuncionalidad

Considere si necesitamos seleccionar Id y Nombre de Mastery las dos últimas fechas para cada Id de la Detailstabla.

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

que forma el siguiente resultado

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

Esto traerá resultados incorrectos, es decir, traerá solo los últimos datos de dos fechas de la Detailstabla, independientemente de Idsi nos unimos Id. Entonces, la solución adecuada es usar 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

que forma el siguiente resultado deseado

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. Cuando necesitamos LEFT JOINfuncionalidad usando functions.

OUTER APPLYpuede usarse como un reemplazo LEFT JOINcuando necesitamos obtener resultados de la Mastertabla y a function.

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

Y la función va aquí.

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

que generó el siguiente resultado

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



                             Característica común de CROSS APPLYyOUTER APPLY

CROSS APPLYo OUTER APPLYse puede usar para retener NULLvalores al desconectar, que son intercambiables.

Considera que tienes la tabla de abajo

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

Cuando usa UNPIVOTpara llevar FROMDATEAND TODATEa una columna, eliminará los NULLvalores por defecto.

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

que genera el siguiente resultado. Tenga en cuenta que hemos perdido el registro de Idnúmero3

  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

En tales casos, un CROSS APPLYo OUTER APPLYserá útil

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

que forma el siguiente resultado y retiene Iddonde está su valor3

  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
fuente
En lugar de publicar exactamente la misma respuesta en dos preguntas, ¿por qué no marcar una como duplicado?
Tab Alleman
2
Considero que esta respuesta es más aplicable para responder la pregunta original. Sus ejemplos muestran escenarios de la "vida real".
FrankO
Entonces para aclarar. El escenario "top n"; ¿podría hacerse con la unión izquierda / interna pero usando un "número de fila sobre la partición por id" y luego seleccionando "WHERE M.RowNumber <3" o algo así?
Chaitanya
1
Gran respuesta en general! Por supuesto, esta es una mejor respuesta que la aceptada, porque es: simple, con ejemplos visuales útiles y explicaciones.
Arsen Khachaturyan
8

Un ejemplo de la vida real sería si tuviera un programador y quisiera ver cuál fue la entrada de registro más reciente para cada tarea programada.

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
Jurado
fuente
En nuestras pruebas, siempre encontramos que la función de unión con ventana es la más eficiente para top n (pensé que esto siempre sería cierto ya que aplicar y subconsultar son bucles anidados cursivos / obligatorios). aunque creo que ahora podría haberlo descifrado ... gracias al enlace de Martin que sugiere que si no devuelve la tabla completa y no hay índices óptimos en la tabla, entonces el número de lecturas sería mucho menor usando la aplicación cruzada (o una subconsulta si top n donde n = 1)
Lee Tickett
Básicamente tengo esa consulta aquí y ciertamente no realiza ninguna subconsulta con bucles anidados. Dado que la tabla de registro tiene un PK de taskID y lastUpdateDate, es una operación muy rápida. ¿Cómo reformaría esa consulta para usar una función de ventana?
BJury
2
seleccione * de la tarea t unión interna (seleccione taskid, logresult, lastupdatedate, rank () over (partición por orden taskid por lastupdatedate desc) _rank) lg en lg.taskid = t.taskid y lg._rank = 1
Lee Tickett
5

Para responder al punto anterior, haga un ejemplo:

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

Y ahora ejecute las dos consultas con un plan de ejecució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

Puede ver que la consulta de aplicación externa es más eficiente. (No se pudo adjuntar el plan ya que soy un nuevo usuario ... Doh.)

Jurado
fuente
el plan de ejecución me interesa: ¿sabe por qué la solución rank () realiza un escaneo de índice y una clasificación costosa en lugar de una aplicación externa que busca un índice y no parece hacer una clasificación (aunque debe hacerlo porque puede ' t hacer un top sin una especie?)
Lee Tickett
1
La aplicación externa no necesita realizar una ordenación, ya que puede usar el índice en la tabla subyacente. Presumiblemente, la consulta con la función rank () necesita procesar toda la tabla para garantizar que sus clasificaciones sean correctas.
BJury
No puedes hacer un top sin una especie. aunque su punto sobre el procesamiento de toda la tabla PODRÍA ser cierto, me sorprendería (sé que el optimizador / compilador sql puede decepcionar de vez en cuando, pero esto sería un comportamiento loco)
Lee Tickett
2
Puede encabezar un encabezado sin ordenar cuando los datos por los que se agrupa están contra un índice, ya que el optimizador sabe que ya está ordenado, por lo que literalmente solo necesita extraer la primera (o última) entrada del índice.
BJury