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:
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:
>) 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!