Este artigo é a quarta parte de uma série sobre bugs, armadilhas e práticas recomendadas do T-SQL. Anteriormente eu cobri determinismo, subconsultas e junções. O foco do artigo deste mês são bugs, armadilhas e melhores práticas relacionadas às funções da janela. Obrigado Erland Sommarskog, Aaron Bertrand, Alejandro Mesa, Umachandar Jayachandran (UC), Fabiano Neves Amorim, Milos Radivojevic, Simon Sabin, Adam Machanic, Thomas Grohser, Chan Ming Man e Paul White por oferecerem suas ideias!
Em meus exemplos, usarei um banco de dados de exemplo chamado TSQLV5. Você pode encontrar o script que cria e preenche esse banco de dados aqui e seu diagrama ER aqui.
Existem duas armadilhas comuns envolvendo funções de janela, ambas resultado de padrões implícitos contra-intuitivos impostos pelo padrão SQL. Uma armadilha tem a ver com cálculos de totais em execução em que você obtém um quadro de janela com a opção RANGE implícita. Outra armadilha está um pouco relacionada, mas tem consequências mais graves, envolvendo uma definição de quadro implícita para as funções FIRST_VALUE e LAST_VALUE.
Quadro de janela com opção RANGE implícita
Nossa primeira armadilha envolve o cálculo de totais em execução usando uma função de janela agregada, onde você especifica explicitamente a cláusula de ordem da janela, mas não especifica explicitamente a unidade do quadro da janela (ROWS ou RANGE) e sua extensão de quadro de janela relacionada, por exemplo, ROWS ANTECEDENTE ILIMITADO. O default implícito é contra-intuitivo e suas consequências podem ser surpreendentes e dolorosas.
Para demonstrar essa armadilha, usarei uma tabela chamada Transações com dois milhões de transações bancárias com créditos (valores positivos) e débitos (valores negativos). Execute o seguinte código para criar a tabela de transações e preenchê-la com dados de exemplo:
SET NOCOUNT ON; USE TSQLV5; -- http://tsql.solidq.com/SampleDatabases/TSQLV5.zip DROP TABLE SE 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) -- cria índice POC ); DECLARE @num_partitions AS INT =100, @rows_per_partition AS INT =20000; INSERT INTO dbo.Transações COM (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;
Nossa armadilha tem um lado lógico com um possível bug lógico, bem como um lado de desempenho com uma penalidade de desempenho. A penalidade de desempenho é relevante apenas quando a função de janela é otimizada com operadores de processamento de modo de linha. O SQL Server 2016 apresenta o operador Window Aggregate de modo em lote, que remove a parte de penalidade de desempenho da armadilha, mas antes do SQL Server 2019 esse operador é usado somente se você tiver um índice columnstore presente nos dados. O SQL Server 2019 apresenta o modo em lote no suporte ao armazenamento de linhas, para que você possa obter o processamento no modo em lote, mesmo que não haja índices columnstore presentes nos dados. Para demonstrar a penalidade de desempenho com o processamento de modo de linha, se você estiver executando os exemplos de código neste artigo no SQL Server 2019 ou posterior, ou no Banco de Dados SQL do Azure, use o código a seguir para definir o nível de compatibilidade do banco de dados como 140 para para não habilitar o modo de lote no armazenamento de linha ainda:
ALTER DATABASE TSQLV5 SET COMPATIBILITY_LEVEL =140;
Use o código a seguir para ativar as estatísticas de tempo e E/S na sessão:
SET STATISTICS TIME, IO ON;
Para evitar esperar que dois milhões de linhas sejam impressas no SSMS, sugiro executar os exemplos de código nesta seção com a opção Descartar resultados após execução ativada (vá para Opções de consulta, Resultados, Grade e verifique Descartar resultados após execução).
Antes de chegarmos à armadilha, considere a seguinte consulta (chame-a de Consulta 1) que calcula o saldo da conta bancária após cada transação aplicando um total em execução usando uma função de agregação de janela com uma especificação de quadro explícita:
SELECT actid, tranid, val, SUM(val) OVER( PARTITION BY actid ORDER BY tranid ROWS UNBOUNDED PRECEDING ) AS balance FROM dbo.Transactions;
O plano para esta consulta, usando processamento em modo de linha, é mostrado na Figura 1.
Figura 1:plano para consulta 1, processamento em modo de linha
O plano extrai os dados pré-ordenados do índice clusterizado da tabela. Em seguida, ele usa os operadores Segment e Sequence Project para calcular os números das linhas para descobrir quais linhas pertencem ao quadro da linha atual. Em seguida, ele usa os operadores Segment, Window Spool e Stream Aggregate para calcular a função de agregação da janela. O operador Window Spool é usado para colocar em spool as linhas do quadro que precisam ser agregadas. Sem nenhuma otimização especial, o plano teria que gravar por linha todas as linhas de quadros aplicáveis no spool e agregá-las. Isso teria resultado em complexidade quadrática, ou N. A boa notícia é que quando o quadro começa com UNBOUNDED PRECEDING, o SQL Server identifica o caso como um fast track caso, em que ele simplesmente pega o total de execução da linha anterior e adiciona o valor da linha atual para calcular o total de execução da linha atual, resultando em escala linear. Nesse modo fast track, o plano grava apenas duas linhas no spool por linha de entrada — uma com a agregação e outra com os detalhes.
O Window Spool pode ser implementado fisicamente de duas maneiras. Seja como um spool rápido na memória que foi especialmente projetado para funções de janela, ou como um spool lento no disco, que é essencialmente uma tabela temporária no tempdb. Se o número de linhas que precisam ser gravadas no spool por linha subjacente pode exceder 10.000, ou se o SQL Server não puder prever o número, ele usará o spool em disco mais lento. Em nosso plano de consulta, temos exatamente duas linhas gravadas no spool por linha subjacente, portanto, o SQL Server usa o spool na memória. Infelizmente, não há como saber pelo plano que tipo de carretel você está recebendo. Existem duas maneiras de descobrir isso. Uma é usar um evento estendido chamado window_spool_ondisk_warning. Outra opção é habilitar STATISTICS IO e verificar o número de leituras lógicas relatadas para uma tabela chamada Worktable. Um número maior que zero significa que você tem o spool no disco. Zero significa que você tem o carretel na memória. Aqui estão as estatísticas de E/S para nossa consulta:
Leituras lógicas da tabela 'Tabela de trabalho':0. Leituras lógicas da tabela 'Transações':6208.
Como você pode ver, usamos o carretel na memória. Esse é geralmente o caso quando você usa a unidade de moldura de janela ROWS com UNBOUNDED PRECEDING como o primeiro delimitador.
Aqui estão as estatísticas de tempo para nossa consulta:
Tempo de CPU:4297 ms, tempo decorrido:4441 ms.
Essa consulta levou cerca de 4,5 segundos para ser concluída na minha máquina com os resultados descartados.
Agora para a captura. Se você usar a opção RANGE em vez de ROWS, com os mesmos delimitadores, pode haver uma sutil diferença de significado, mas uma grande diferença de desempenho no modo de linha. A diferença de significado só é relevante se você não tiver uma ordenação total, ou seja, se estiver ordenando por algo que não é único. A opção ROWS UNBOUNDED PRECEDING para com a linha atual, portanto, em caso de empate, o cálculo não é determinístico. Por outro lado, a opção RANGE UNBOUNDED PRECEDING olha para a frente da linha atual e inclui empates, se houver. Ele usa uma lógica semelhante à opção TOP WITH TIES. Quando você tem ordenação total, ou seja, você está ordenando por algo único, não há vínculos a serem incluídos e, portanto, ROWS e RANGE se tornam logicamente equivalentes nesse caso. O problema é que quando você usa RANGE, o SQL Server sempre usa o spool em disco no processamento de modo de linha, pois ao processar uma determinada linha, ele não pode prever quantas linhas mais serão incluídas. Isso pode ter uma penalidade de desempenho severa.
Considere a seguinte consulta (chame-a de Consulta 2), que é igual à Consulta 1, apenas usando a opção RANGE em vez de ROWS:
SELECT actid, tranid, val, SUM(val) OVER( PARTITION BY actid ORDER BY tranid RANGE UNBOUNDED PRECEDING ) AS balance FROM dbo.Transactions;
O plano para esta consulta é mostrado na Figura 2.
Figura 2:plano para consulta 2, processamento em modo de linha
A Consulta 2 é logicamente equivalente à Consulta 1 porque temos ordem total; no entanto, como ele usa RANGE, ele é otimizado com o spool no disco. Observe que no plano da Consulta 2 o Window Spool tem a mesma aparência do plano da Consulta 1 e os custos estimados são os mesmos.
Aqui estão as estatísticas de tempo e E/S para a execução da Consulta 2:
Tempo de CPU:19515 ms, tempo decorrido:20201 ms.
Leituras lógicas da tabela 'Worktable':12044701. Leituras lógicas da tabela 'Transações':6208.
Observe o grande número de leituras lógicas na tabela de trabalho, indicando que você obteve o spool no disco. O tempo de execução é mais de quatro vezes maior do que para a Consulta 1.
Se você está pensando que, se for esse o caso, você simplesmente evitará usar a opção RANGE, a menos que realmente precise incluir empates, é uma boa ideia. O problema é que, se você usar uma função de janela que suporte um quadro (agregados, FIRST_VALUE, LAST_VALUE) com uma cláusula de ordem de janela explícita, mas nenhuma menção à unidade do quadro de janela e sua extensão associada, você receberá RANGE UNBOUNDED PRECEDING por padrão . Esse padrão é ditado pelo padrão SQL, e o padrão o escolheu porque geralmente prefere opções mais determinísticas como padrões. A consulta a seguir (chame-a de Consulta 3) é um exemplo que cai nessa armadilha:
SELECT actid, tranid, val, SUM(val) OVER( PARTITION BY actid ORDER BY tranid ) AS balance FROM dbo.Transactions;
Muitas vezes as pessoas escrevem assim assumindo que estão recebendo LINHAS UNBOUNDED PRECEDING por padrão, sem perceber que na verdade estão recebendo RANGE UNBOUNDED PRECEDING. O problema é que, como a função usa ordem total, você obtém o mesmo resultado como com ROWS, então você não pode dizer que há um problema no resultado. Mas os números de desempenho que você obterá são como para a Consulta 2. Vejo pessoas caindo nessa armadilha o tempo todo.
A melhor prática para evitar esse problema é nos casos em que você usa uma função de janela com um quadro, seja explícito sobre a unidade do quadro da janela e sua extensão e geralmente prefere ROWS. Reserve o uso de RANGE apenas para os casos em que o pedido não é único e você precisa incluir empates.
Considere a seguinte consulta que ilustra um caso em que há uma diferença conceitual entre ROWS e 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;
Essa consulta gera a seguinte saída:
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 a diferença nos resultados para as linhas em que a mesma data do pedido aparece mais de uma vez, como é o caso de 8 de julho de 2017. Observe como a opção ROWS não inclui empates e, portanto, não é determinística, e como a opção RANGE o faz incluem laços e, portanto, é sempre determinístico.
É questionável, porém, se na prática você tem casos em que você ordena por algo que não é único, e você realmente precisa de inclusão de laços para tornar o cálculo determinístico. O que provavelmente é muito mais comum na prática é fazer uma de duas coisas. Uma é desempate adicionando algo à ordenação da janela para torná-la única e, assim, resultar em um cálculo determinístico, assim:
SELECT orderdate, orderid, val, SUM(val) OVER( ORDER BY orderdate, orderid ROWS UNBOUNDED PRECEDING ) AS runningsum FROM Sales.OrderValues ORDER BY orderdate;
Essa consulta gera a seguinte saída:
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 ...
Outra opção é aplicar o agrupamento preliminar, no nosso caso, por data do pedido, assim:
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;
Essa consulta gera a seguinte saída em que cada data do pedido aparece apenas uma vez:
orderdate daytotal runningsum ---------- ------------- ----------- 2017-07-04 440,00 440,00 2017-07-05 1863,40 2303,40 08-07-2017 2206,66 4510,06 09-07-2017 3597,90 8107,96 ...
De qualquer forma, lembre-se das melhores práticas aqui!
A boa notícia é que, se você estiver executando no SQL Server 2016 ou posterior e tiver um índice columnstore presente nos dados (mesmo que seja um índice columnstore filtrado falso), ou se estiver executando no SQL Server 2019 ou posterior, ou no Banco de Dados SQL do Azure, independentemente da presença de índices columnstore, todas as três consultas mencionadas acima são otimizadas com o operador Window Aggregate em modo de lote. Com esse operador, muitas das ineficiências de processamento no modo de linha são eliminadas. Este operador não usa um spool, então não há problema de spool na memória versus no disco. Ele usa um processamento mais sofisticado onde pode aplicar várias passagens paralelas sobre a janela de linhas na memória para ROWS e RANGE.
Para demonstrar o uso da otimização do modo em lote, verifique se o nível de compatibilidade do banco de dados está definido como 150 ou superior:
ALTER DATABASE TSQLV5 SET COMPATIBILITY_LEVEL =150;
Execute a Consulta 1 novamente:
SELECT actid, tranid, val, SUM(val) OVER( PARTITION BY actid ORDER BY tranid ROWS UNBOUNDED PRECEDING ) AS balance FROM dbo.Transactions;
O plano para esta consulta é mostrado na Figura 3.
Figura 3:plano para consulta 1, processamento em lote
Aqui estão as estatísticas de desempenho que obtive para esta consulta:
Tempo de CPU:937 ms, tempo decorrido:983 ms.
Leituras lógicas da tabela 'Transações':6208.
O tempo de execução caiu para 1 segundo!
Execute a Consulta 2 com a opção RANGE explícita novamente:
SELECT actid, tranid, val, SUM(val) OVER( PARTITION BY actid ORDER BY tranid RANGE UNBOUNDED PRECEDING ) AS balance FROM dbo.Transactions;
O plano para esta consulta é mostrado na Figura 4.
Figura 2:plano para consulta 2, processamento em lote
Aqui estão as estatísticas de desempenho que obtive para esta consulta:
Tempo de CPU:969 ms, tempo decorrido:1048 ms.
Leituras lógicas da tabela 'Transações':6208.
O desempenho é o mesmo da Consulta 1.
Execute a Consulta 3 novamente, com a opção RANGE implícita:
SELECT actid, tranid, val, SUM(val) OVER( PARTITION BY actid ORDER BY tranid ) AS balance FROM dbo.Transactions;
O plano e os números de desempenho são, obviamente, os mesmos da Consulta 2.
Quando terminar, execute o seguinte código para desativar as estatísticas de desempenho:
SET STATISTICS TIME, IO OFF;
Além disso, não se esqueça de desativar a opção Descartar resultados após execução no SSMS.
Quadro implícito com FIRST_VALUE e LAST_VALUE
As funções FIRST_VALUE e LAST_VALUE são funções de janela de deslocamento que retornam uma expressão da primeira ou última linha no quadro da janela, respectivamente. A parte complicada sobre eles é que, muitas vezes, quando as pessoas os usam pela primeira vez, eles não percebem que eles suportam um quadro, mas pensam que eles se aplicam a toda a partição.
Considere a seguinte tentativa de devolver as informações do pedido, mais os valores do primeiro e do último pedido do cliente:
SELECT custid, orderid, 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;
Se você acredita incorretamente que essas funções operam em toda a partição da janela, o que é a crença de muitas pessoas que usam essas funções pela primeira vez, você naturalmente espera que FIRST_VALUE retorne o valor do pedido do primeiro pedido do cliente e LAST_VALUE retorne o valor valor do pedido do último pedido do cliente. Na prática, porém, essas funções suportam um quadro. Como um lembrete, com funções que suportam um quadro, quando você especifica a cláusula de ordem de janela, mas não a unidade de quadro de janela e sua extensão associada, você obtém RANGE UNBOUNDED PRECEDING por padrão. Com a função FIRST_VALUE, você obterá o resultado esperado, mas se sua consulta for otimizada com operadores de modo de linha, você pagará a penalidade de usar o spool em disco. Com a função LAST_VALUE é ainda pior. Além de pagar a penalidade do spool no disco, em vez de obter o valor da última linha da partição, você obterá o valor da linha atual!
Aqui está a saída da consulta acima:
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 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 406 4060606060.2018-04-15 10507 749.06 406 749.06 3 2018-18-04-15 10507 74906 406 10573 2082,00 403,20 2082,00 3 22-09-2018 10677 813,37 403,20 813,37 3 25-09-2018 10682 375,50 403,20 375,50 3 28-01-2019 10856 660,00 403,20 660,00 ...
Muitas vezes, quando as pessoas veem essa saída pela primeira vez, pensam que o SQL Server tem um bug. Mas é claro que não; é simplesmente o padrão do padrão SQL. Há um bug na consulta. Percebendo que há um quadro envolvido, você deseja ser explícito sobre a especificação do quadro e usar o quadro mínimo que captura a linha que você procura. Além disso, certifique-se de usar a unidade ROWS. Portanto, para obter a primeira linha na partição, use a função FIRST_VALUE com o quadro ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW. Para obter a última linha na partição, use a função LAST_VALUE com o quadro ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING.
Aqui está nossa consulta revisada com o bug corrigido:
SELECT custid, orderid, 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 ENTRE CURRENT ROW E UNBOUNDED FOLLOWING ) AS lastval FROM Sales.OrderValues ORDER BY custid, orderdate, orderid;
Desta vez você obtém o resultado correto:
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 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 22-09-2018 10677 813,37 403,20 660,00 3 25-09-2018 10682 375,50 403,20 660,00 3 28-01-2019 10856 660,00 403,20 660,00 ...
É de se perguntar qual foi a motivação para o padrão até mesmo suportar um quadro com essas funções. Se você pensar sobre isso, você os usará principalmente para obter algo da primeira ou da última linha da partição. Se você precisar do valor de, digamos, duas linhas antes da atual, em vez de usar FIRST_VALUE com um quadro que começa com 2 PRECEDING, não é muito mais fácil usar LAG com um deslocamento explícito de 2, assim:
SELECT custid, orderdate, orderid, val, LAG(val, 2) OVER( PARTITION BY custid ORDER BY orderdate, orderid ) AS prevtwoval FROM Sales.OrderValues ORDER BY custid, orderid, orderid;
Essa consulta gera a seguinte saída:
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 3190 1 2019-03-01 10952 47. 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-03304 10926 51440 420.00 880.80 2 201903-03404 10926 514440 420.00 88.80 2 2019030404 10926 514.40 420 420.00 880 2 2019-0304 10926 514.40. 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 1067770 813.37 1940.855 306-09-09-22 1067777777777777 1940.855 306-09-22208777777777777 1940.855 306-09-22087777777777777 1940.855 3 2018-09-2220877777777777 1940.85506 309-09-22. -01-28 10856 660,00 813,37 ...
Aparentemente, há uma diferença semântica entre o uso acima da função LAG e FIRST_VALUE com um quadro que começa com 2 PRECEDING. Com o primeiro, se uma linha não existir no deslocamento desejado, você obtém um NULL por padrão. Com o último, você ainda obtém o valor da primeira linha presente, ou seja, o valor da primeira linha da partição. Considere a seguinte consulta:
SELECT custid, orderid, 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, orderid, orderid;
Essa consulta gera a seguinte saída:
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-152010 10835 845,80 878. 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.80.80 2 2019-03-442666660 1075 320,00 880 20 2 2019-03-4449266660 1075. 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.855 3 2018-09-222227777777777 1940.855 -01-28 10856 660,00 813,37 ...
Observe que desta vez não há NULLs na saída. Portanto, há algum valor em oferecer suporte a um quadro com FIRST_VALUE e LAST_VALUE. Apenas certifique-se de lembrar a melhor prática de sempre ser explícito sobre a especificação do quadro com essas funções e usar a opção ROWS com o quadro mínimo que contém a linha que você procura.
Conclusão
Este artigo se concentrou em bugs, armadilhas e práticas recomendadas relacionadas às funções da janela. Lembre-se de que as funções de agregação de janela e as funções de deslocamento de janela FIRST_VALUE e LAST_VALUE suportam um quadro e que, se você especificar a cláusula de ordem da janela, mas não especificar a unidade do quadro da janela e sua extensão associada, obterá RANGE UNBOUNDED PRECEDING por predefinição. Isso incorre em uma penalidade de desempenho quando a consulta é otimizada com operadores de modo de linha. Com a função LAST_VALUE, isso resulta na obtenção dos valores da linha atual em vez da última linha na partição. Lembre-se de ser explícito sobre o quadro e geralmente preferir a opção ROWS a RANGE. É ótimo ver as melhorias de desempenho com o operador Window Aggregate em modo de lote. Quando aplicável, pelo menos a armadilha de desempenho é eliminada.