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

Errores, trampas y mejores prácticas de T-SQL:funciones de ventana

Este artículo es la cuarta entrega de una serie sobre errores, trampas y mejores prácticas de T-SQL. Anteriormente cubrí el determinismo, las subconsultas y las uniones. El enfoque del artículo de este mes son los errores, las trampas y las mejores prácticas relacionadas con las funciones de la ventana. ¡Gracias Erland Sommarskog, Aaron Bertrand, Alejandro Mesa, Umachandar Jayachandran (UC), Fabiano Neves Amorim, Milos Radivojevic, Simon Sabin, Adam Machanic, Thomas Grohser, Chan Ming Man y Paul White por ofrecer sus ideas!

En mis ejemplos, usaré una base de datos de muestra llamada TSQLV5. Puede encontrar el script que crea y completa esta base de datos aquí, y su diagrama ER aquí.

Hay dos errores comunes relacionados con las funciones de ventana, los cuales son el resultado de valores predeterminados implícitos contrarios a la intuición impuestos por el estándar SQL. Un escollo tiene que ver con los cálculos de totales acumulados donde se obtiene un marco de ventana con la opción RANGO implícita. Otro escollo está algo relacionado, pero tiene consecuencias más graves, ya que implica una definición de marco implícita para las funciones FIRST_VALUE y LAST_VALUE.

Marco de ventana con opción RANGE implícita

Nuestro primer escollo implica el cálculo de los totales acumulados utilizando una función de ventana agregada, en la que especifica explícitamente la cláusula de orden de la ventana, pero no especifica explícitamente la unidad del marco de la ventana (FILAS o RANGO) y su extensión de marco de ventana relacionada, por ejemplo, FILAS PRECEDENTE ILIMITADO. El incumplimiento implícito es contrario a la intuición y sus consecuencias podrían ser sorprendentes y dolorosas.

Para demostrar este escollo, usaré una tabla llamada Transacciones que contiene dos millones de transacciones de cuentas bancarias con créditos (valores positivos) y débitos (valores negativos). Ejecute el siguiente código para crear la tabla Transacciones y rellénela con datos de muestra:

 ESTABLECER NOCUENTO EN; USAR TSQLV5; -- http://tsql.solidq.com/SampleDatabases/TSQLV5.zip BOTAR LA TABLA SI EXISTE dbo.Transactions; CREATE TABLE dbo.Transactions (actid INT NOT NULL, tranid INT NOT NULL, val MONEY NOT NULL, CONSTRAINT PK_Transactions PRIMARY KEY(actid, tranid) -- crea el índice POC); DECLARAR @num_partitions COMO INT =100, @rows_per_partition COMO INT =20000; INSERTAR EN dbo. Transacciones CON (TABLOCK) (actid, tranid, val) SELECCIONAR NP.n, RPP.n, (ABS(CHECKSUM(NEWID())%2)*2-1) * (1 + ABS(CHECKSUM( NEWID())%5)) DESDE dbo.GetNums(1, @num_partitions) COMO NP CROSS JOIN dbo.GetNums(1, @rows_per_partition) COMO RPP;

Nuestro escollo tiene tanto un lado lógico con un posible error lógico como un lado de rendimiento con una penalización de rendimiento. La penalización de rendimiento es relevante solo cuando la función de ventana está optimizada con operadores de procesamiento en modo fila. SQL Server 2016 presenta el operador Agregado de ventana en modo por lotes, que elimina la parte de la desventaja de la penalización de rendimiento, pero antes de SQL Server 2019, este operador solo se usa si tiene un índice de almacén de columnas presente en los datos. SQL Server 2019 introduce el modo por lotes en la compatibilidad con el almacén de filas, por lo que puede obtener un procesamiento en modo por lotes incluso si no hay índices de almacén de columnas en los datos. Para demostrar la penalización del rendimiento con el procesamiento en modo de fila, si está ejecutando los ejemplos de código de este artículo en SQL Server 2019 o posterior, o en Azure SQL Database, use el siguiente código para establecer el nivel de compatibilidad de la base de datos en 140 para que no habilitar el modo por lotes en la tienda de filas todavía:

 ALTERAR BASE DE DATOS TSQLV5 ESTABLECER COMPATIBILIDAD_NIVEL =140;

Use el siguiente código para activar el tiempo y las estadísticas de E/S en la sesión:

 CONFIGURAR TIEMPO DE ESTADÍSTICAS, IO ON;

Para evitar esperar a que se impriman dos millones de filas en SSMS, sugiero ejecutar los ejemplos de código en esta sección con la opción Descartar resultados después de la ejecución activada (vaya a Opciones de consulta, Resultados, Cuadrícula y marque Descartar resultados después de la ejecución).

Antes de llegar al escollo, considere la siguiente consulta (llámela Consulta 1) que calcula el saldo de la cuenta bancaria después de cada transacción aplicando un total acumulado usando una función de agregación de ventana con una especificación de marco explícita:

 SELECCIONE actid, tranid, val, SUM(val) SOBRE( PARTICIÓN POR actid ORDEN POR tranid FILAS SIN LÍMITES PRECEDENTES ) COMO saldo DESDE dbo.Transacciones;

El plan para esta consulta, utilizando el procesamiento en modo fila, se muestra en la Figura 1.

Figura 1:Plan para Consulta 1, procesamiento en modo fila

El plan extrae los datos ordenados previamente del índice agrupado de la tabla. Luego, utiliza los operadores Segmento y Proyecto de secuencia para calcular los números de fila para averiguar qué filas pertenecen al marco de la fila actual. A continuación, utiliza los operadores Segmento, Carrete de ventana y Agregado de flujo para calcular la función de agregado de ventana. El operador Window Spool se usa para poner en cola las filas de marcos que luego deben agregarse. Sin ninguna optimización especial, el plan habría tenido que escribir por fila todas sus filas de cuadros aplicables en el spool y luego agregarlas. Esto habría dado como resultado una complejidad cuadrática o N. La buena noticia es que cuando el marco comienza con UNBOUNDED PRECEDING, SQL Server identifica el caso como una vía rápida caso, en el que simplemente toma el total acumulado de la fila anterior y suma el valor de la fila actual para calcular el total acumulado de la fila actual, lo que da como resultado una escala lineal. En este modo de vía rápida, el plan escribe solo dos filas en el spool por fila de entrada:una con el agregado y otra con el detalle.

El Window Spool se puede implementar físicamente en una de dos formas. Ya sea como un spool rápido en memoria diseñado especialmente para funciones de ventana, o como un spool lento en disco, que es esencialmente una tabla temporal en tempdb. Si el número de filas que deben escribirse en el spool por fila subyacente podría superar los 10.000, o si SQL Server no puede predecir el número, utilizará la cola de impresión en disco más lenta. En nuestro plan de consulta, tenemos exactamente dos filas escritas en el spool por fila subyacente, por lo que SQL Server usa el spool en memoria. Desafortunadamente, no hay forma de saber a partir del plan qué tipo de carrete está recibiendo. Hay dos maneras de resolver esto. Una es usar un evento extendido llamado window_spool_ondisk_warning. Otra opción es habilitar STATISTICS IO y verificar la cantidad de lecturas lógicas informadas para una tabla llamada Worktable. Un número mayor que cero significa que obtuvo la cola en disco. Cero significa que obtuvo el carrete en memoria. Aquí están las estadísticas de E/S para nuestra consulta:

Lecturas lógicas de la tabla 'Mesa de trabajo':0. Lecturas lógicas de la tabla 'Transacciones':6208.

Como puede ver, usamos el spool en memoria. Por lo general, ese es el caso cuando usa la unidad de marco de ventana ROWS con UNBOUNDED PRECEDING como primer delimitador.

Aquí están las estadísticas de tiempo para nuestra consulta:

Tiempo de CPU:4297 ms, tiempo transcurrido:4441 ms.

Esta consulta tardó unos 4,5 segundos en completarse en mi máquina y los resultados se descartaron.

Ahora para la captura. Si usa la opción RANGE en lugar de ROWS, con los mismos delimitadores, puede haber una diferencia sutil en el significado, pero una gran diferencia en el rendimiento en el modo fila. La diferencia de significado solo es relevante si no tiene un pedido total, es decir, si está ordenando por algo que no es único. La opción ROWS UNBOUNDED PRECEDING se detiene en la fila actual, por lo que en caso de empate, el cálculo no es determinista. Por el contrario, la opción RANGE UNBOUNDED PRECEDING mira hacia delante de la fila actual e incluye empates si están presentes. Utiliza una lógica similar a la opción TOP WITH TIES. Cuando tiene un pedido total, es decir, está ordenando por algo único, no hay vínculos para incluir y, por lo tanto, FILAS y RANGO se vuelven lógicamente equivalentes en tal caso. El problema es que cuando usa RANGE, SQL Server siempre usa el spool en disco en el procesamiento de modo de fila, ya que al procesar una fila determinada no puede predecir cuántas filas más se incluirán. Esto puede tener una grave penalización de rendimiento.

Considere la siguiente consulta (llámela Consulta 2), que es igual a la Consulta 1, solo que usa la opción RANGO en lugar de FILAS:

 SELECT actid, tranid, val, SUM(val) SOBRE( PARTICIÓN POR actid ORDEN POR tranid RANGO ILIMITADO PRECEDENTE ) COMO saldo DESDE dbo.Transactions;

El plan para esta consulta se muestra en la Figura 2.

Figura 2:Plan para Consulta 2, procesamiento en modo fila

La Consulta 2 es lógicamente equivalente a la Consulta 1 porque tenemos orden total; sin embargo, dado que usa RANGE, se optimiza con el spool en disco. Observe que en el plan de la consulta 2, la ventana Spool se ve igual que en el plan de la consulta 1 y los costos estimados son los mismos.

Aquí están las estadísticas de tiempo y E/S para la ejecución de la Consulta 2:

Tiempo de CPU:19515 ms, tiempo transcurrido:20201 ms.
Lecturas lógicas de la tabla 'Worktable':12044701. Lecturas lógicas de la tabla 'Transactions':6208.

Observe la gran cantidad de lecturas lógicas en Worktable, lo que indica que obtuvo el spool en disco. El tiempo de ejecución es más de cuatro veces mayor que para la Consulta 1.

Si está pensando que si ese es el caso, simplemente evitará usar la opción RANGO, a menos que realmente necesite incluir lazos, eso es una buena idea. El problema es que si usa una función de ventana que admite un marco (agregados, FIRST_VALUE, LAST_VALUE) con una cláusula de orden de ventana explícita, pero sin mencionar la unidad de marco de ventana y su extensión asociada, obtiene RANGE UNBOUNDED PRECEDING de manera predeterminada . Este valor predeterminado está dictado por el estándar SQL, y el estándar lo eligió porque generalmente prefiere opciones más deterministas como valores predeterminados. La siguiente consulta (llámela Consulta 3) es un ejemplo que cae en esta trampa:

 SELECT actid, tranid, val, SUM(val) OVER(PARTITION BY actid ORDER BY tranid ) AS balance FROM dbo.Transactions;

A menudo, las personas escriben así asumiendo que obtienen FILAS SIN LÍMITES PRECEDENTES de forma predeterminada, sin darse cuenta de que en realidad están obteniendo RANGO SIN LÍMITES PRECEDENTES. La cuestión es que, dado que la función usa el orden total, obtienes el mismo resultado que con ROWS, por lo que no puedes decir que hay un problema a partir del resultado. Pero los números de rendimiento que obtendrá son como los de la Consulta 2. Veo personas que caen en esta trampa todo el tiempo.

La mejor práctica para evitar este problema es en los casos en los que usa una función de ventana con un marco, sea explícito sobre la unidad del marco de la ventana y su extensión, y generalmente prefiera FILAS. Reserve el uso de RANGO solo para casos en los que el pedido no sea único y necesite incluir vínculos.

Considere la siguiente consulta que ilustra un caso en el que existe una diferencia conceptual entre ROWS y RANGE:

 SELECT orderdate, orderid, val, SUM(val) OVER( ORDER BY orderdate ROWS UNLIMITED PRECEDING ) AS sumrows, SUM(val) OVER( ORDER BY orderdate RANGE UNLIMITED PRECEDING ) AS sumrange FROM Sales.OrderValues ​​ORDER BY orderdate;

Esta consulta genera el siguiente resultado:

 orderdate orderid val sumrows sumrange ---------- -------- -------- -------- -------- -2017-07-04 10248 440.00 440.00 440.00 2017-07-05 10249 1863.40 2303.40 2303.40 2017-07-08 10250 1552.60 3856.00 4510.06 2017-07-08 10251 654.06 4510.06 4510 /pre> 

Observe la diferencia en los resultados de las filas donde la misma fecha de pedido aparece más de una vez, como es el caso del 8 de julio de 2017. Observe cómo la opción FILAS no incluye vínculos y, por lo tanto, no es determinista, y cómo la opción RANGO sí lo hace. incluyen lazos, y por lo tanto siempre es determinista.

Sin embargo, es cuestionable si en la práctica tiene casos en los que ordena por algo que no es único, y realmente necesita la inclusión de vínculos para que el cálculo sea determinista. Lo que probablemente sea mucho más común en la práctica es hacer una de dos cosas. Una es romper los lazos agregando algo al orden de la ventana para que sea único y de esta manera resulte en un cálculo determinista, así:

 SELECT orderdate, orderid, val, SUM(val) OVER( ORDER BY orderdate, orderid ROWS UNLIMITED PRECEDING ) AS runningsum FROM Sales.OrderValues ​​ORDER BY orderdate;

Esta consulta genera el siguiente resultado:

 orderdate orderid val runningsum ---------- -------- --------- ----------- 2017-07-04 10248 440.00 440.00 2017-07-05 10249 1863.40 2303.40 2017-07-08 10250 1552.60 3856.00 2017-07-08 10251 654.06 4510.06 2017-07-09 10252 3597.90 8107.96 ... 

Otra opción es aplicar una agrupación previa, en nuestro caso, por fecha de pedido, así:

 SELECT orderdate, SUM(val) AS daytotal, SUM(SUM(val)) OVER( ORDER BY orderdate ROWS UNLIMITED PRECEDING ) AS runningsum FROM Sales.OrderValues ​​GROUP BY orderdate ORDER BY orderdate;

Esta consulta genera el siguiente resultado donde cada fecha de pedido aparece solo una vez:

 orderdate daytotal runningsum ---------- --------- ----------- 2017-07-04 440.00 440.00 2017-07-05 1863.40 2303.40 2017-07-08 2206.66 4510.06 2017-07-09 3597.90 8107.96 ...

En cualquier caso, ¡asegúrate de recordar las mejores prácticas aquí!

La buena noticia es que si está ejecutando en SQL Server 2016 o posterior y tiene un índice de almacén de columnas presente en los datos (incluso si es un índice de almacén de columnas filtrado falso), o si está ejecutando en SQL Server 2019 o posterior, o en Azure SQL Database, independientemente de la presencia de índices de almacén de columnas, las tres consultas mencionadas se optimizan con el operador Agregado de ventana en modo por lotes. Con este operador, se eliminan muchas de las ineficiencias del procesamiento en modo fila. Este operador no usa un spool en absoluto, por lo que no hay problema de spool en memoria versus en disco. Utiliza un procesamiento más sofisticado en el que puede aplicar varios pases paralelos sobre la ventana de filas en la memoria tanto para ROWS como para RANGE.

Para demostrar el uso de la optimización en modo por lotes, asegúrese de que el nivel de compatibilidad de su base de datos sea 150 o superior:

 ALTERAR BASE DE DATOS TSQLV5 ESTABLECER COMPATIBILIDAD_NIVEL =150;

Ejecute Consulta 1 de nuevo:

 SELECCIONE actid, tranid, val, SUM(val) SOBRE( PARTICIÓN POR actid ORDEN POR tranid FILAS SIN LÍMITES PRECEDENTES ) COMO saldo DESDE dbo.Transacciones;

El plan para esta consulta se muestra en la Figura 3.

Figura 3:Plan para Consulta 1, procesamiento en modo por lotes

Estas son las estadísticas de rendimiento que obtuve para esta consulta:

Tiempo de CPU:937 ms, tiempo transcurrido:983 ms.
Lecturas lógicas de la tabla "Transacciones":6208.

¡El tiempo de ejecución se redujo a 1 segundo!

Ejecute Consulta 2 con la opción RANGE explícita de nuevo:

 SELECT actid, tranid, val, SUM(val) SOBRE( PARTICIÓN POR actid ORDEN POR tranid RANGO ILIMITADO PRECEDENTE ) COMO saldo DESDE dbo.Transactions;

El plan para esta consulta se muestra en la Figura 4.

Figura 2:Plan para Consulta 2, procesamiento en modo por lotes

Estas son las estadísticas de rendimiento que obtuve para esta consulta:

Tiempo de CPU:969 ms, tiempo transcurrido:1048 ms.
Lecturas lógicas de la tabla "Transacciones":6208.

El rendimiento es el mismo que para la Consulta 1.

Ejecute Consulta 3 de nuevo, con la opción RANGE implícita:

 SELECT actid, tranid, val, SUM(val) OVER(PARTITION BY actid ORDER BY tranid ) AS balance FROM dbo.Transactions;

El plan y los números de rendimiento son, por supuesto, los mismos que para la Consulta 2.

Cuando haya terminado, ejecute el siguiente código para desactivar las estadísticas de rendimiento:

 ESTABLECER HORA DE ESTADÍSTICAS, IO APAGADO;

Además, no olvide desactivar la opción Descartar resultados después de la ejecución en SSMS.

Marco implícito con FIRST_VALUE y LAST_VALUE

Las funciones FIRST_VALUE y LAST_VALUE son funciones de ventana de compensación que devuelven una expresión de la primera o la última fila en el marco de la ventana, respectivamente. La parte complicada de ellos es que, a menudo, cuando las personas los usan por primera vez, no se dan cuenta de que admiten un marco, sino que piensan que se aplican a toda la partición.

Considere el siguiente intento de devolver la información del pedido, más los valores del primer y último pedido del cliente:

 SELECT custid, orderdate, orderid, val, FIRST_VALUE(val) OVER( PARTITION BY custid ORDER BY orderdate, orderid ) AS firstval, LAST_VALUE(val) OVER( PARTITION BY custid ORDER BY orderdate, orderid ) AS lastval FROM Sales. OrderValues ​​ORDER BY custid, orderdate, orderid;

Si cree incorrectamente que estas funciones operan en toda la partición de la ventana, que es la creencia de muchas personas que usan estas funciones por primera vez, naturalmente espera que FIRST_VALUE devuelva el valor del pedido del primer pedido del cliente y LAST_VALUE devuelva el valor de pedido del último pedido del cliente. En la práctica, sin embargo, estas funciones admiten un marco. Como recordatorio, con las funciones que admiten un marco, cuando especifica la cláusula de orden de la ventana pero no la unidad del marco de la ventana y su extensión asociada, obtiene RANGE UNBOUNDED PRECEDING de forma predeterminada. Con la función FIRST_VALUE, obtendrá el resultado esperado, pero si su consulta se optimiza con operadores de modo de fila, pagará la penalización de usar el spool en disco. Con la función LAST_VALUE es aún peor. No solo pagará la penalización del spool en disco, sino que en lugar de obtener el valor de la última fila de la partición, ¡obtendrá el valor de la fila actual!

Aquí está el resultado de la consulta anterior:

 custid orderdate orderid val firstval lastval ------- ---------- -------- ---------- ------ ---- ---------- 1 2018-08-25 10643 814,50 814,50 814,50 1 2018-10-03 10692 878,00 814,50 878,00 1 2018-10-13 10702 330,00 814,50 330,019-1 0 10835 845.80 814.50 845.80 1 2019-03-16 10952 471.20 814.50 471.20 1 2019-04-09 11011 933.50 814.50 933.50 2 2017-09-18 10308 88.80 88.80 88.80 2 2018-08-08 10625 479.75 88.80 479.75 2 2018-11-28 10759 320.00 88.80 320.00 2 2019-03-04 10926 514.40 88.80 514.40 3 2017-11-27 10365 403.20 403.20 403.20 3 2018-04-15 10507 749.06 403.20 749.06 3 2018-05-13 10535 1940. 10573 2082.00 403.20 2082.00 3 2018-09-22 10677 813.37 403.20 813.37 3 2018-09-25 10682 375.50 403.20 375.50 3 2019-01-28 10856 660.00 403.20 660.00 ...

A menudo, cuando las personas ven este resultado por primera vez, piensan que SQL Server tiene un error. Pero, por supuesto, no lo hace; es simplemente el valor predeterminado del estándar SQL. Hay un error en la consulta. Al darse cuenta de que hay un marco involucrado, desea ser explícito acerca de la especificación del marco y usar el marco mínimo que captura la fila que está buscando. Además, asegúrese de utilizar la unidad ROWS. Entonces, para obtener la primera fila en la partición, use la función FIRST_VALUE con el marco FILAS ENTRE LA FILA ACTUAL Y LA PRECEDENTE ILIMITADA. Para obtener la última fila en la partición, use la función LAST_VALUE con el marco FILAS ENTRE LA FILA ACTUAL Y LA SIGUIENTE ILIMITADA.

Aquí está nuestra consulta revisada con el error corregido:

 SELECT custid, orderdate, orderid, val, FIRST_VALUE(val) OVER( PARTITION BY custid ORDER BY orderdate, orderid FILAS ENTRE LA FILA ACTUAL Y LA PRECEDENTE ILIMITADA ) AS firstval, LAST_VALUE(val) OVER( PARTITION BY custid ORDER BY orderdate, orderid FILAS ENTRE LA FILA ACTUAL Y LA SIGUIENTE ILIMITADA ) COMO lastval FROM Sales.OrderValues ​​ORDER BY custid, orderdate, orderid;

Esta vez obtienes el resultado correcto:

 custid orderdate orderid val firstval lastval ------- ---------- -------- ---------- ------ ---- ---------- 1 2018-08-25 10643 814.50 814.50 933.50 1 2018-10-03 10692 878.00 814.50 933.50 1 2018-10-13 10702 330.00 814.50 933.059-1 0 10835 845.80 814.50 933.50 1 2019-03-16 10952 471.20 814.50 933.50 1 2019-04-09 11011 933.50 814.50 933.50 2 2017-09-18 10308 88.80 88.80 514.40 2 2018-08-08 10625 479.75.75 88. 10759 320.00 88.80 514.40 2 2019-03-04 10926 514.40 88.80 514.40 3 2017-11-27 10365 403.20 403.20 660.00 3 2018-04-15 10507 749.06 403.20 660.00 3 2018-05-13 10535 1940.85 403.20 660.00 3 2018-06-19 10573 2082.00 403.20 660.00 3 2018-09-22 10677 813.37 403.20 660.00 3 2018-09-25 10682 375.50 403.20 660.00 3 2019-01-28 10856 660.00 403.20 660.00 ...

Uno se pregunta cuál fue la motivación para que el estándar incluso admitiera un marco con estas funciones. Si lo piensa, los usará principalmente para obtener algo de la primera o la última fila de la partición. Si necesita el valor de, digamos, dos filas antes de la actual, en lugar de usar FIRST_VALUE con un cuadro que comienza con 2 PRECEDING, ¿no es mucho más fácil usar LAG con un desplazamiento explícito de 2, así:

 SELECCIONE custid, orderdate, orderid, val, LAG(val, 2) OVER( PARTITION BY custid ORDER BY orderdate, orderid ) AS prevtwoval FROM Sales.OrderValues ​​ORDER BY custid, orderdate, orderid;

Esta consulta genera el siguiente resultado:

 custid orderdate orderid val prevtwoval ------- ---------- -------- ---------- ------- ---- 1 2018-08-25 10643 814.50 NULL 1 2018-10-03 10692 878.00 NULL 1 2018-10-13 10702 330.00 814.50 1 2019-01-15 10835 845.80 878.00 1 2019-03-16 10952 471.20.00 1 1 1 1 2019-04-09 11011 933.50 845.80 2 2017-09-18 10308 88.80 NULL 2 2018-08-08 10625 479.75 NULL 2 2018-11-28 10759 320.00 88.80 2 2019-03-04 10926 514.40 479.75 3 2017-11-27 10365 403.20 NULL 3 2018-04-15 10507 749.06 NULL 3 2018-05-13 10535 1940.85 403.20 3 2018-06-19 10573 2082.00 749.06 3 2018-09-22 10677 813.37 1940.85 3 2018-25-25 10682 375.50 2082.00 20820 -01-28 10856 660.00 813.37 ...

Aparentemente, existe una diferencia semántica entre el uso anterior de la función LAG y FIRST_VALUE con un marco que comienza con 2 PRECEDING. Con el primero, si una fila no existe en el desplazamiento deseado, obtiene un NULL de forma predeterminada. Con este último, aún obtiene el valor de la primera fila que está presente, es decir, el valor de la primera fila en la partición. Considere la siguiente consulta:

 SELECCIONE custid, orderdate, orderid, val, FIRST_VALUE(val) OVER( PARTITION BY custid ORDER BY orderdate, orderid FILAS ENTRE 2 FILAS ANTERIORES Y ACTUAL ) AS prevtwoval FROM Sales.OrderValues ​​ORDER BY custid, orderdate, orderid;

Esta consulta genera el siguiente resultado:

 custid orderdate orderid val prevtwoval ------- ---------- -------- ---------- ------- ---- 1 2018-08-25 10643 814.50 814.50 1 2018-10-03 10692 878.00 814.50 1 2018-10-13 10702 330.00 814.50 1 2019-01-15 10835 845.80 878.00 1 2019-03-16 10952 471.20 330.00 1 2019-04-09 11011 933.50 845.80 2 2017-09-18 10308 88.80 88.80 2 2018-08-08 10625 479.75 88.80 2 2018-11-28 10759 320.00 88.80 2 2019-03-04 10926 514.40 479.79 3 2017-1111111111111 10365 403.20 403.20 3 2018-04-15 10507 749.06 403.20 3 2018-05-13 10535 1940.85 403.20 3 2018-06-19 10573 2082.00 749.06 3 2018-09-22 10677 813.37 1940.85 3. -01-28 10856 660.00 813.37 ...

Observe que esta vez no hay valores NULL en la salida. Por lo tanto, tiene algún valor admitir un marco con FIRST_VALUE y LAST_VALUE. Solo asegúrese de recordar la mejor práctica para ser siempre explícito sobre la especificación del marco con estas funciones y usar la opción FILAS con el marco mínimo que contiene la fila que está buscando.

Conclusión

Este artículo se centró en los errores, las trampas y las mejores prácticas relacionadas con las funciones de la ventana. Recuerde que tanto las funciones de agregación de ventana como las funciones de desplazamiento de ventana FIRST_VALUE y LAST_VALUE admiten un marco, y que si especifica la cláusula de orden de ventana pero no especifica la unidad de marco de ventana y su extensión asociada, obtiene RANGE UNBOUNDED PRECEDING por defecto. Esto incurre en una penalización de rendimiento cuando la consulta se optimiza con operadores de modo de fila. Con la función LAST_VALUE, esto da como resultado obtener los valores de la fila actual en lugar de la última fila de la partición. Recuerde ser explícito sobre el marco y, en general, preferir la opción FILAS a RANGO. Es genial ver las mejoras en el rendimiento con el operador Agregado de ventana en modo por lotes. Cuando es aplicable, al menos se elimina el escollo de rendimiento.