sql >> Base de Datos >  >> RDS >> Database

Funciones de ventana anidadas en SQL

El estándar ISO/IEC 9075:2016 (SQL:2016) define una función denominada funciones de ventana anidadas. Esta función le permite anidar dos tipos de funciones de ventana como argumento de una función de agregación de ventana. La idea es permitirle hacer referencia a un número de fila o al valor de una expresión, en marcadores estratégicos en elementos de ventanas. Los marcadores le dan acceso a la primera o la última fila de la partición, la primera o la última fila del marco, la fila exterior actual y la fila del marco actual. Esta idea es muy poderosa y le permite aplicar filtros y otros tipos de manipulaciones dentro de su función de ventana que a veces son difíciles de lograr de otra manera. También puede usar funciones de ventana anidada para emular fácilmente otras características, como marcos basados ​​en RANGE. Esta función no está disponible actualmente en T-SQL. Publiqué una sugerencia para mejorar SQL Server agregando soporte para funciones de ventana anidadas. Asegúrese de agregar su voto si cree que esta característica podría ser beneficiosa para usted.

De qué no se tratan las funciones de ventana anidada

A la fecha de este escrito, no hay mucha información disponible sobre las verdaderas funciones de ventana anidada estándar. Lo que lo hace más difícil es que aún no conozco ninguna plataforma que haya implementado esta función. De hecho, ejecutar una búsqueda en la web de funciones de ventana anidadas devuelve principalmente cobertura y debates sobre el anidamiento de funciones agregadas agrupadas dentro de funciones agregadas en ventanas. Por ejemplo, suponga que desea consultar la vista Sales.OrderValues ​​en la base de datos de muestra TSQLV5 y devolver para cada cliente y fecha de pedido, el total diario de los valores del pedido y el total acumulado hasta el día actual. Tal tarea implica tanto la agrupación como la creación de ventanas. Agrupa las filas por el ID del cliente y la fecha del pedido, y aplica una suma acumulada sobre la suma del grupo de los valores del pedido, así:

  USE TSQLV5; -- http://tsql.solidq.com/SampleDatabases/TSQLV5.zip
 
  SELECT custid, orderdate, SUM(val) AS daytotal,
    SUM(SUM(val)) OVER(PARTITION BY custid
                       ORDER BY orderdate
                       ROWS UNBOUNDED PRECEDING) AS runningsum
  FROM Sales.OrderValues
  GROUP BY custid, orderdate;

Esta consulta genera el siguiente resultado, que se muestra aquí en forma abreviada:

  custid  orderdate   daytotal runningsum
  ------- ----------  -------- ----------
  1       2018-08-25    814.50     814.50
  1       2018-10-03    878.00    1692.50
  1       2018-10-13    330.00    2022.50
  1       2019-01-15    845.80    2868.30
  1       2019-03-16    471.20    3339.50
  1       2019-04-09    933.50    4273.00
  2       2017-09-18     88.80      88.80
  2       2018-08-08    479.75     568.55
  2       2018-11-28    320.00     888.55
  2       2019-03-04    514.40    1402.95
  ...

Aunque esta técnica es bastante buena, y aunque las búsquedas web de funciones de ventana anidada devuelven principalmente dichas técnicas, eso no es lo que el estándar SQL quiere decir con funciones de ventana anidada. Como no pude encontrar ninguna información sobre el tema, solo tuve que averiguarlo a partir del estándar mismo. Con suerte, este artículo aumentará el conocimiento de la verdadera función de funciones de ventana anidada y hará que las personas se dirijan a Microsoft y soliciten agregar soporte en SQL Server.

De qué se tratan las funciones de ventana anidada

Las funciones de ventana anidadas incluyen dos funciones que puede anidar como argumento de una función agregada de ventana. Esas son la función de número de fila anidada y la expresión value_of anidada en la función de fila.

Función de número de fila anidado

La función de número de fila anidado le permite hacer referencia al número de fila de marcadores estratégicos en elementos de ventana. Esta es la sintaxis de la función:

(ROW_NUMBER()>) SOBRE()

Los marcadores de fila que puede especificar son:

  • BEGIN_PARTICIÓN
  • END_PARTICIÓN
  • BEGIN_FRAME
  • END_FRAME
  • FILA_ACTUAL
  • FILA_MARCO

Los primeros cuatro marcadores se explican por sí mismos. En cuanto a los dos últimos, el marcador CURRENT_ROW representa la fila exterior actual y FRAME_ROW representa la fila del marco interior actual.

Como ejemplo del uso de la función de número de fila anidado, considere la siguiente tarea. Debe consultar la vista Sales.OrderValues ​​y devolver para cada pedido algunos de sus atributos, así como la diferencia entre el valor del pedido actual y el promedio del cliente, pero excluyendo el primer y el último pedido del cliente del promedio.

Esta tarea se puede lograr sin funciones de ventana anidadas, pero la solución implica algunos pasos:

  WITH C1 AS
  (
    SELECT custid, val,
      ROW_NUMBER() OVER( PARTITION BY custid
                         ORDER BY orderdate, orderid ) AS rownumasc,
      ROW_NUMBER() OVER( PARTITION BY custid
                         ORDER BY orderdate DESC, orderid DESC ) AS rownumdesc
    FROM Sales.OrderValues
  ),
  C2 AS
  (
    SELECT custid, AVG(val) AS avgval
    FROM C1
    WHERE 1 NOT IN (rownumasc, rownumdesc)
    GROUP BY custid
  )
  SELECT O.orderid, O.custid, O.orderdate, O.val,
    O.val - C2.avgval AS diff
  FROM Sales.OrderValues AS O
    LEFT OUTER JOIN C2
      ON O.custid = C2.custid;

Aquí está el resultado de esta consulta, que se muestra aquí en forma abreviada:

  orderid  custid  orderdate  val       diff
  -------- ------- ---------- --------  ------------
  10411    10      2018-01-10   966.80   -570.184166
  10743    4       2018-11-17   319.20   -809.813636
  11075    68      2019-05-06   498.10  -1546.297500
  10388    72      2017-12-19  1228.80   -358.864285
  10720    61      2018-10-28   550.00   -144.744285
  11052    34      2019-04-27  1332.00  -1164.397500
  10457    39      2018-02-25  1584.00   -797.999166
  10789    23      2018-12-22  3687.00   1567.833334
  10434    24      2018-02-03   321.12  -1329.582352
  10766    56      2018-12-05  2310.00   1015.105000
  ...

Usando funciones de número de fila anidadas, la tarea se puede lograr con una sola consulta, así:

  SELECT orderid, custid, orderdate, val,
    val - AVG( CASE
                 WHEN ROW_NUMBER(FRAME_ROW) NOT IN
                        ( ROW_NUMBER(BEGIN_PARTITION), ROW_NUMBER(END_PARTITION) ) THEN val
               END )
            OVER( PARTITION BY custid
                  ORDER BY orderdate, orderid
                  ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING ) AS diff
  FROM Sales.OrderValues;

Además, la solución admitida actualmente requiere al menos una ordenación en el plan y varias pasadas sobre los datos. La solución que utiliza funciones de número de fila anidadas tiene todo el potencial para optimizarse con la confianza en el orden del índice y una cantidad reducida de pases sobre los datos. Sin embargo, esto, por supuesto, depende de la implementación.

Expresión value_of anidada en la función de fila

La función anidada value_of expression at row le permite interactuar con un valor de una expresión en los mismos marcadores de fila estratégicos mencionados anteriormente en un argumento de una función de agregación de ventana. Esta es la sintaxis de esta función:

( VALOR DE AT [] [, ]
>) SOBRE()

Como puede ver, puede especificar un cierto delta negativo o positivo con respecto al marcador de fila y, opcionalmente, proporcionar un valor predeterminado en caso de que no exista una fila en la posición especificada.

Esta capacidad le brinda mucho poder cuando necesita interactuar con diferentes puntos en elementos de ventanas. Considere el hecho de que tan poderosas como las funciones de ventana pueden compararse con herramientas alternativas como las subconsultas, lo que las funciones de ventana no admiten es un concepto básico de correlación. Usando el marcador CURRENT_ROW obtienes acceso a la fila exterior y de esta manera emulas las correlaciones. Al mismo tiempo, puede beneficiarse de todas las ventajas que tienen las funciones de ventana en comparación con las subconsultas.

Como ejemplo, suponga que necesita consultar la vista Sales.OrderValues ​​y devolver para cada pedido algunos de sus atributos, así como la diferencia entre el valor del pedido actual y el promedio del cliente, pero excluyendo los pedidos realizados en la misma fecha que la fecha del pedido actual. Esto requiere una capacidad similar a una correlación. Con la expresión anidada value_of en la función de fila, usando el marcador CURRENT_ROW, esto se puede lograr fácilmente así:

  SELECT orderid, custid, orderdate, val,
    val - AVG( CASE WHEN orderdate <> VALUE OF orderdate AT CURRENT_ROW THEN val END )
            OVER( PARTITION BY custid ) AS diff
  FROM Sales.OrderValues;

Se supone que esta consulta genera el siguiente resultado:

  orderid  custid  orderdate  val       diff
  -------- ------- ---------- --------  ------------
  10248    85      2017-07-04   440.00    180.000000
  10249    79      2017-07-05  1863.40   1280.452000
  10250    34      2017-07-08  1552.60   -854.228461
  10251    84      2017-07-08   654.06   -293.536666
  10252    76      2017-07-09  3597.90   1735.092728
  10253    34      2017-07-10  1444.80   -970.320769
  10254    14      2017-07-11   556.62  -1127.988571
  10255    68      2017-07-12  2490.50    617.913334
  10256    88      2017-07-15   517.80   -176.000000
  10257    35      2017-07-16  1119.90   -153.562352
  ...

Si está pensando que esta tarea se puede lograr con la misma facilidad con subconsultas correlacionadas, en este caso simplista tendría razón. Lo mismo se puede lograr con la siguiente consulta:

  SELECT O1.orderid, O1.custid, O1.orderdate, O1.val,
    O1.val - ( SELECT AVG(O2.val)
               FROM Sales.OrderValues AS O2
               WHERE O2.custid = O1.custid
                 AND O2.orderdate <> O1.orderdate ) AS diff
  FROM Sales.OrderValues AS O1;

Sin embargo, recuerde que una subconsulta opera en una vista independiente de los datos, mientras que una función de ventana opera en el conjunto que se proporciona como entrada al paso de procesamiento de consulta lógica que maneja la cláusula SELECT. Por lo general, la consulta subyacente tiene una lógica adicional como uniones, filtros, agrupación, etc. Con las subconsultas, debe preparar un CTE preliminar o repetir la lógica de la consulta subyacente también en la subconsulta. Con las funciones de ventana, no hay necesidad de repetir la lógica.

Por ejemplo, digamos que se suponía que debía operar solo en pedidos enviados (donde la fecha de envío no es NULL) que fueron manejados por el empleado 3. La solución con la función de ventana necesita agregar los predicados de filtro solo una vez, así:

   SELECT orderid, custid, orderdate, val,
    val - AVG( CASE WHEN orderdate <> VALUE OF orderdate AT CURRENT_ROW THEN val END )
            OVER( PARTITION BY custid ) AS diff
  FROM Sales.OrderValues
  WHERE empid = 3 AND shippeddate IS NOT NULL;

Se supone que esta consulta genera el siguiente resultado:

  orderid  custid  orderdate  val      diff
  -------- ------- ---------- -------- -------------
  10251    84      2017-07-08   654.06   -459.965000
  10253    34      2017-07-10  1444.80    531.733334
  10256    88      2017-07-15   517.80  -1022.020000
  10266    87      2017-07-26   346.56          NULL
  10273    63      2017-08-05  2037.28  -3149.075000
  10283    46      2017-08-16  1414.80    534.300000
  10309    37      2017-09-19  1762.00  -1951.262500
  10321    38      2017-10-03   144.00          NULL
  10330    46      2017-10-16  1649.00    885.600000
  10332    51      2017-10-17  1786.88    495.830000
  ...

La solución con la subconsulta necesita agregar los predicados de filtro dos veces, una en la consulta externa y otra en la subconsulta, así:

  SELECT O1.orderid, O1.custid, O1.orderdate, O1.val,
    O1.val - ( SELECT AVG(O2.val)
               FROM Sales.OrderValues AS O2
               WHERE O2.custid = O1.custid
                 AND O2.orderdate <> O1.orderdate
                 AND empid = 3
                 AND shippeddate IS NOT NULL) AS diff
  FROM Sales.OrderValues AS O1
  WHERE empid = 3 AND shippeddate IS NOT NULL;

Es esto, o agregar un CTE preliminar que se encargue de todo el filtrado y cualquier otra lógica. De cualquier forma que lo mires, con las subconsultas, hay más capas de complejidad involucradas.

El otro beneficio de las funciones de ventana anidadas es que si tuviéramos soporte para las de T-SQL, habría sido fácil emular el soporte completo faltante para la unidad de marco de ventana RANGE. Se supone que la opción RANGO le permite definir marcos dinámicos que se basan en un desplazamiento del valor de ordenación en la fila actual. Por ejemplo, suponga que necesita calcular para cada pedido de cliente desde la vista Sales.OrderValues ​​el valor promedio móvil de los últimos 14 días. De acuerdo con el estándar SQL, puede lograr esto usando la opción RANGO y el tipo INTERVALO, así:

  SELECT orderid, custid, orderdate, val,
    AVG(val) OVER( PARTITION BY custid
                   ORDER BY orderdate
                   RANGE BETWEEN INTERVAL '13' DAY PRECEDING
                             AND CURRENT ROW ) AS movingavg14days
  FROM Sales.OrderValues;

Se supone que esta consulta genera el siguiente resultado:

  orderid  custid  orderdate  val     movingavg14days
  -------- ------- ---------- ------- ---------------
  10643    1       2018-08-25  814.50      814.500000
  10692    1       2018-10-03  878.00      878.000000
  10702    1       2018-10-13  330.00      604.000000
  10835    1       2019-01-15  845.80      845.800000
  10952    1       2019-03-16  471.20      471.200000
  11011    1       2019-04-09  933.50      933.500000
  10308    2       2017-09-18   88.80       88.800000
  10625    2       2018-08-08  479.75      479.750000
  10759    2       2018-11-28  320.00      320.000000
  10926    2       2019-03-04  514.40      514.400000
  10365    3       2017-11-27  403.20      403.200000
  10507    3       2018-04-15  749.06      749.060000
  10535    3       2018-05-13 1940.85     1940.850000
  10573    3       2018-06-19 2082.00     2082.000000
  10677    3       2018-09-22  813.37      813.370000
  10682    3       2018-09-25  375.50      594.435000
  10856    3       2019-01-28  660.00      660.000000
  ...

A la fecha de este escrito, esta sintaxis no es compatible con T-SQL. Si tuviéramos soporte para funciones de ventana anidadas en T-SQL, habría podido emular esta consulta con el siguiente código:

  SELECT orderid, custid, orderdate, val,
    AVG( CASE WHEN DATEDIFF(day, orderdate, VALUE OF orderdate AT CURRENT_ROW) 
                     BETWEEN 0 AND 13
                THEN val END )
      OVER( PARTITION BY custid
            ORDER BY orderdate
            RANGE UNBOUNDED PRECEDING ) AS movingavg14days
  FROM Sales.OrderValues;

¿Qué es lo que no te gusta?

Emite tu voto

Las funciones estándar de la ventana anidada parecen un concepto muy poderoso que permite mucha flexibilidad al interactuar con diferentes puntos en los elementos de la ventana. Estoy bastante sorprendido de que no pueda encontrar ninguna cobertura del concepto que no sea en el estándar mismo, y que no veo muchas plataformas implementándolo. Esperemos que este artículo aumente la conciencia sobre esta función. Si cree que podría serle útil tenerlo disponible en T-SQL, ¡asegúrese de emitir su voto!