Este artículo es la cuarta entrega de una serie sobre errores, trampas y mejores prácticas en T-SQL. Anteriormente cubrí el determinismo, las subconsultas y los joins. El enfoque del artículo de este mes son los errores, las trampas y las mejores prácticas relacionadas con las funciones de 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 utilizaré una base de datos de ejemplo llamada TSQLV5. Puedes encontrar el script que crea y rellena esta base de datos aquí, y su diagrama ER aquí.
Hay dos escollos comunes que involucran a las funciones de ventana, ambos son el resultado de valores implícitos contraintuitivos que son impuestos por el estándar SQL. Uno de los problemas tiene que ver con los cálculos de los totales en curso en los que se obtiene un marco de ventana con la opción implícita RANGE. 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 la opción RANGE implícita
Nuestro primer escollo tiene que ver con el cálculo de totales corridos utilizando una función de ventana agregada, donde sí se especifica explícitamente la cláusula de orden de la ventana, pero no se especifica explícitamente la unidad de marco de ventana (ROWS o RANGE) y su extensión de marco de ventana relacionada, por ejemplo, FILAS PRECEDENTES NO LIMITADAS. El defecto implícito es contrario a la intuición y sus consecuencias podrían ser sorprendentes y dolorosas.
Para demostrar este escollo, utilizaré 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 rellenarla con datos de ejemplo:
SET NOCOUNT ON; USE TSQLV5; -- http://tsql.solidq.com/SampleDatabases/TSQLV5.zip DROP TABLE IF EXISTS 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) -- creates POC index ); DECLARE @num_partitions AS INT = 100, @rows_per_partition AS INT = 20000; INSERT INTO dbo.Transactions WITH (TABLOCK) (actid, tranid, val) SELECT NP.n, RPP.n, (ABS(CHECKSUM(NEWID())%2)*2-1) * (1 + ABS(CHECKSUM(NEWID())%5)) FROM dbo.GetNums(1, @num_partitions) AS NP CROSS JOIN dbo.GetNums(1, @rows_per_partition) AS RPP;
Nuestra trampa tiene tanto un lado lógico con un potencial error lógico como un lado de rendimiento con una penalización de rendimiento. La penalización de rendimiento es relevante sólo cuando la función de ventana se optimiza con operadores de procesamiento en modo fila. SQL Server 2016 introduce el operador Window Aggregate en modo batch, que elimina la parte de la penalización de rendimiento del escollo, pero antes de SQL Server 2019 este operador solo se utiliza si se tiene un índice columnstore presente en los datos. SQL Server 2019 introduce el modo por lotes en el soporte de rowstore, por lo que puede obtener el procesamiento en modo por lotes incluso si no hay índices columnstore presentes en los datos. Para demostrar la penalización de rendimiento con el procesamiento en modo fila, si ejecuta los ejemplos de código de este artículo en SQL Server 2019 o posterior, o en Azure SQL Database, utilice el siguiente código para establecer el nivel de compatibilidad de la base de datos en 140 para no habilitar aún el modo por lotes en el almacén de filas:
ALTER DATABASE TSQLV5 SET COMPATIBILITY_LEVEL = 140;
Utiliza el siguiente código para activar las estadísticas de tiempo y E/S en la sesión:
SET STATISTICS TIME, IO ON;
Para evitar esperar a que se impriman dos millones de filas en SSMS, sugiero ejecutar los ejemplos de código de 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 a la trampa, 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 en ejecución utilizando una función agregada de ventana con una especificación de marco explícita:
SELECT actid, tranid, val, SUM(val) OVER( PARTITION BY actid ORDER BY tranid ROWS UNBOUNDED PRECEDING ) AS balance FROM dbo.Transactions;
El plan para esta consulta, utilizando el procesamiento en modo fila, se muestra en la Figura 1.
Figura 1: Plan para la consulta 1, procesamiento en modo fila
El plan extrae los datos preordenados del índice agrupado de la tabla. Luego utiliza los operadores de Proyecto de Segmento y 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 de Segmento, Carrete de Ventana y Agregado de Secuencia para calcular la función de agregado de la ventana. El operador Window Spool se utiliza para poner en cola las filas del marco que luego hay que agregar. Sin ninguna optimización especial, el plan habría tenido que escribir por fila todas sus filas de marcos aplicables en el spool, y luego agregarlas. Esto habría dado lugar a una complejidad cuadrática, o N2. La buena noticia es que cuando el marco comienza con UNBOUNDED PRECEDING, SQL Server identifica el caso como un caso de vía rápida, en el que simplemente toma el total en curso de la fila anterior y añade el valor de la fila actual para calcular el total en curso de la fila actual, lo que resulta en un escalado lineal. En este modo de seguimiento rápido, el plan escribe sólo dos filas en el spool por cada fila de entrada: una con el agregado y otra con el detalle.
El Window Spool puede implementarse físicamente de dos maneras. O bien como un spool rápido en memoria que fue especialmente diseñado para las funciones de ventana, o como un spool lento en el disco, que es esencialmente una tabla temporal en tempdb. Si el número de filas que deben escribirse en el spool por fila subyacente puede superar las 10.000, o si SQL Server no puede predecir el número, utilizará el spool en disco más lento. En nuestro plan de consulta, tenemos exactamente dos filas escritas en el spool por fila subyacente, por lo que SQL Server utiliza el spool en memoria. Desgraciadamente, no hay forma de saber a partir del plan qué tipo de spool se está obteniendo. Hay dos formas de averiguarlo. Una es usar un evento extendido llamado window_spool_ondisk_warning. Otra opción es habilitar STATISTICS IO, y comprobar el número de lecturas lógicas reportadas para una tabla llamada Worktable. Un número mayor que cero significa que tienes el spool en disco. Cero significa que tienes el spool en memoria. Aquí están las estadísticas de E/S para nuestra consulta:
Como puedes ver, tenemos el spool en memoria utilizado. Eso es generalmente el caso cuando se utiliza la unidad de marco de la ventana ROWS con UNBOUNDED PRECEDING como primer delimitador.
Aquí están las estadísticas de tiempo para nuestra consulta:
Esta consulta tardó unos 4,5 segundos en completarse en mi máquina con los resultados descartados.
Ahora la trampa. Si utiliza la opción RANGE en lugar de ROWS, con los mismos delimitadores, puede haber una sutil diferencia de significado, pero una gran diferencia de rendimiento en el modo de filas. La diferencia de significado sólo es relevante si no tiene un ordenamiento 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 es no determinista. Por el contrario, la opción RANGE UNBOUNDED PRECEDING mira por delante de la fila actual, e incluye los empates si están presentes. Utiliza una lógica similar a la de la opción SUPERIOR CON VINCULOS. Cuando se tiene un ordenamiento total, es decir, se ordena por algo único, no hay empates que incluir, y por lo tanto FILAS y RANGO se vuelven lógicamente equivalentes en tal caso. El problema es que cuando se utiliza RANGE, SQL Server siempre utiliza el spool en disco bajo el procesamiento en modo fila, ya que al procesar una fila determinada no puede predecir cuántas filas más se incluirán. Esto puede tener una severa penalización en el rendimiento.
Considere la siguiente consulta (llámela Consulta 2), que es la misma que la Consulta 1, sólo que utilizando la opción RANGE en lugar de ROWS:
SELECT actid, tranid, val, SUM(val) OVER( PARTITION BY actid ORDER BY tranid RANGE UNBOUNDED PRECEDING ) AS balance FROM dbo.Transactions;
El plan para esta consulta se muestra en la Figura 2.
Figura 2: Plan para la consulta 2, procesamiento en modo fila
La consulta 2 es lógicamente equivalente a la consulta 1 porque tenemos orden total; sin embargo, como utiliza RANGE, se optimiza con el spool en disco. Obsérvese que en el plan de la Consulta 2 el Window Spool tiene el mismo aspecto que en el plan de la Consulta 1, y los costes estimados son los mismos.
Aquí están las estadísticas de tiempo y E/S para la ejecución de la Consulta 2:
Tabla ‘Worktable’ lecturas lógicas: 12044701. Tabla ‘Transacciones’ lecturas lógicas: 6208.
Nota el gran número de lecturas lógicas contra Worktable, lo que indica que tiene el spool en disco. El tiempo de ejecución es más de cuatro veces más largo que para la Consulta 1.
Si está pensando que si ese es el caso, simplemente evitará usar la opción RANGE, a menos que realmente necesite incluir empates, ese es un buen pensamiento. El problema es que si utiliza 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 la ventana y su extensión asociada, está obteniendo RANGE UNBOUNDED PRECEDING por defecto. Este valor por defecto está dictado por el estándar SQL, y el estándar lo eligió porque generalmente prefiere opciones más deterministas como valores por defecto. La siguiente consulta (llámese 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 la gente escribe así asumiendo que están obteniendo ROWS UNBOUNDED PRECEDING por defecto, sin darse cuenta de que en realidad están obteniendo RANGE UNBOUNDED PRECEDING. La cuestión es que como la función utiliza el orden total, se obtiene el mismo resultado que con ROWS, por lo que no se puede decir que haya un problema por el resultado. Pero los números de rendimiento que obtendrás son como para la consulta 2. Veo que la gente cae en esta trampa todo el tiempo.
La mejor práctica para evitar este problema es en los casos en que se utiliza una función de ventana con un marco, ser explícito acerca de la unidad de marco de la ventana y su extensión, y por lo general prefieren ROWS. Reserve el uso de RANGE sólo para los casos en los que el ordenamiento no es único y necesita incluir empates.
Considere la siguiente consulta que ilustra un caso en el que hay una diferencia conceptual entre ROWS y RANGE:
SELECT orderdate, orderid, val, SUM(val) OVER( ORDER BY orderdate ROWS UNBOUNDED PRECEDING ) AS sumrows, SUM(val) OVER( ORDER BY orderdate RANGE UNBOUNDED PRECEDING ) AS sumrange FROM Sales.OrderValues ORDER BY orderdate;
Esta consulta genera la siguiente salida:
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.06 2017-07-09 10252 3597.90 8107.96 8107.96 ...
Observe la diferencia en los resultados de las filas en las que 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 los empates y por lo tanto es no determinista, y cómo la opción RANGO sí incluye los empates, y por lo tanto es siempre 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 los empates para hacer el cálculo determinista. Lo que es probablemente mucho más común en la práctica es hacer una de dos cosas. Una es romper los empates añadiendo algo a la ordenación de la ventana para hacerla única y de esta manera resultar en un cálculo determinista, así:
SELECT orderdate, orderid, val, SUM(val) OVER( ORDER BY orderdate, orderid ROWS UNBOUNDED PRECEDING ) AS runningsum FROM Sales.OrderValues ORDER BY orderdate;
Esta consulta genera la siguiente salida:
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 UNBOUNDED PRECEDING ) AS runningsum FROM Sales.OrderValues GROUP BY orderdate ORDER BY orderdate;
Esta consulta genera la siguiente salida en la que cada fecha de pedido aparece sólo 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úrese de recordar la mejor práctica 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 Window Aggregate en modo batch. Con este operador, se eliminan muchas de las ineficiencias del procesamiento en modo fila. Este operador no utiliza un spool en absoluto, por lo que no hay problema de spool en memoria o en disco. Utiliza un procesamiento más sofisticado en el que puede aplicar múltiples pases paralelos sobre la ventana de filas en memoria tanto para FILAS como para RANGO.
Para demostrar el uso de la optimización en modo batch, asegúrese de que el nivel de compatibilidad de su base de datos está establecido en 150 o superior:
ALTER DATABASE TSQLV5 SET COMPATIBILITY_LEVEL = 150;
Ejecute la consulta 1 de nuevo:
SELECT actid, tranid, val, SUM(val) OVER( PARTITION BY actid ORDER BY tranid ROWS UNBOUNDED PRECEDING ) AS balance FROM dbo.Transactions;
El plan para esta consulta se muestra en la Figura 3.
Figura 3: Plan para la consulta 1, procesamiento en modo batch
Aquí están las estadísticas de rendimiento que obtuve para esta consulta:
Las lecturas lógicas de la tabla ‘Transacciones’: 6208.
¡El tiempo de ejecución ha bajado a 1 segundo!
Vuelve a ejecutar la consulta 2 con la opción RANGE explícita:
SELECT actid, tranid, val, SUM(val) OVER( PARTITION BY actid ORDER BY tranid RANGE UNBOUNDED PRECEDING ) AS balance FROM dbo.Transactions;
El plan para esta consulta se muestra en la Figura 4.
Figura 2: Plan para la consulta 2, procesamiento en modo batch
Aquí están las estadísticas de rendimiento que obtuve para esta consulta:
Las lecturas lógicas de la tabla ‘Transacciones’: 6208.
El rendimiento es el mismo que para la consulta 1.
Ejecutar de nuevo la consulta 3, 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:
SET STATISTICS TIME, IO OFF;
Además, no olvide desactivar la opción Descartar resultados tras 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 desplazamiento que devuelven una expresión de la primera o última fila del marco de la ventana, respectivamente. Lo complicado de ellas es que, a menudo, cuando la gente las utiliza por primera vez, no se da cuenta de que admiten un marco, sino que piensa que se aplican a toda la partición.
Considere el siguiente intento de devolver la información de los pedidos, además de los valores de los primeros y últimos pedidos 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 sobre toda la partición de la ventana, que es la creencia de muchas personas que utilizan estas funciones por primera vez, naturalmente espera que FIRST_VALUE devuelva el valor del primer pedido del cliente, y LAST_VALUE devuelva el valor del último pedido del cliente. En la práctica, sin embargo, estas funciones admiten un marco. Como recordatorio, con las funciones que soportan un marco, cuando se especifica la cláusula de orden de la ventana pero no la unidad de marco de la ventana y su extensión asociada, se obtiene RANGE UNBOUNDED PRECEDING por defecto. Con la función FIRST_VALUE, obtendrá el resultado esperado, pero si su consulta se optimiza con operadores en modo fila, pagará la penalización de utilizar el spool en disco. Con la función LAST_VALUE es aún peor. No sólo 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á la salida 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.00 1 2019-01-15 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.85 403.20 1940.85 3 2018-06-19 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 la gente ve esta salida por primera vez, piensa que SQL Server tiene un error. Pero, por supuesto, no lo tiene; es simplemente el estándar de SQL por defecto. Hay un error en la consulta. Al darse cuenta de que hay un marco involucrado, usted quiere ser explícito acerca de la especificación del marco, y utilizar el marco mínimo que captura la fila que está buscando. Además, asegúrate de utilizar la unidad ROWS. Así, para obtener la primera fila de la partición, utilice la función PRIMER_VALOR con el marco FILAS ENTRE LA FILA PRECEDENTE Y LA FILA ACTUAL. Para obtener la última fila de la partición, utilice la función VALOR_ÚLTIMO con el marco FILAS ENTRE LA FILA ACTUAL Y LA FILA SIGUIENTE NO LIMITADA.
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 ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW ) AS firstval, LAST_VALUE(val) OVER( PARTITION BY custid ORDER BY orderdate, orderid ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING ) AS lastval FROM Sales.OrderValues ORDER BY custid, orderdate, orderid;
Esta vez se obtiene 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.50 1 2019-01-15 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 88.80 514.40 2 2018-11-28 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 de la norma para soportar incluso un marco con estas funciones. Si se piensa en ello, la mayoría de las veces se utilizarán para obtener algo de las primeras o últimas filas de la partición. Si necesita el valor de, digamos, dos filas antes de la actual, en lugar de utilizar FIRST_VALUE con un marco que comienza con 2 PRECEDENTES, ¿no es mucho más fácil utilizar LAG con un desplazamiento explícito de 2, así:
SELECT 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 la siguiente salida:
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 330.00 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-09-25 10682 375.50 2082.00 3 2019-01-28 10856 660.00 813.37 ...
Aparentemente, hay una diferencia semántica entre el uso anterior de la función LAG y FIRST_VALUE con un marco que comienza con 2 PRECEDENTES. Con la primera, si no existe una fila en el desplazamiento deseado, se obtiene un NULL por defecto. Con la segunda, sigue obteniendo el valor de la primera fila que está presente, es decir, el valor de la primera fila de la partición. Considere la siguiente consulta:
SELECT custid, orderdate, orderid, val, FIRST_VALUE(val) OVER( PARTITION BY custid ORDER BY orderdate, orderid ROWS BETWEEN 2 PRECEDING AND CURRENT ROW ) AS prevtwoval FROM Sales.OrderValues ORDER BY custid, orderdate, orderid;
Esta consulta genera la siguiente salida:
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.75 3 2017-11-27 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 2018-09-25 10682 375.50 2082.00 3 2019-01-28 10856 660.00 813.37 ...
Observe que esta vez no hay NULLs en la salida. Así que hay algún valor en el apoyo a un marco con FIRST_VALUE y LAST_VALUE. Sólo asegúrese de recordar la mejor práctica de ser siempre explícito acerca de la especificación del marco con estas funciones, y utilizar 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 ventana. Recuerde que tanto las funciones agregadas 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 la ventana pero no especifica la unidad de marco de la ventana y su extensión asociada, obtendrá RANGE UNBOUNDED PRECEDING por defecto. Esto conlleva una penalización de rendimiento cuando la consulta se optimiza con operadores en modo fila. Con la función LAST_VALUE se obtienen los valores de la fila actual en lugar de la última fila de la partición. Recuerde que debe ser explícito sobre el marco y que generalmente prefiere la opción FILAS a RANGO. Es genial ver las mejoras de rendimiento con el operador Window Aggregate en modo batch. Cuando es aplicable, al menos se elimina el escollo del rendimiento.