O padrão ISO/IEC 9075:2016 (SQL:2016) define um recurso chamado funções de janela aninhada. Esse recurso permite aninhar dois tipos de funções de janela como um argumento de uma função de agregação de janela. A ideia é permitir que você faça referência a um número de linha ou a um valor de uma expressão em marcadores estratégicos em elementos de janela. Os marcadores fornecem acesso à primeira ou última linha da partição, à primeira ou última linha do quadro, à linha externa atual e à linha do quadro atual. Essa ideia é muito poderosa, permitindo que você aplique filtragem e outros tipos de manipulações dentro de sua função de janela que às vezes são difíceis de conseguir de outra forma. Você também pode usar funções de janela aninhadas para emular facilmente outros recursos, como quadros baseados em RANGE. Esse recurso não está disponível no momento no T-SQL. Publiquei uma sugestão para melhorar o SQL Server adicionando suporte para funções de janela aninhadas. Certifique-se de adicionar seu voto se achar que esse recurso pode ser benéfico para você.
O que não são as funções de janela aninhada
Na data da redação deste artigo, não há muita informação disponível sobre as verdadeiras funções de janela aninhada padrão. O que dificulta é que ainda não conheço nenhuma plataforma que tenha implementado esse recurso. Na verdade, a execução de uma pesquisa na Web para funções de janela aninhadas retorna principalmente a cobertura e as discussões sobre o aninhamento de funções agregadas agrupadas em funções agregadas em janelas. Por exemplo, suponha que você deseja consultar a exibição Sales.OrderValues no banco de dados de amostra TSQLV5 e retornar para cada cliente e data do pedido, o total diário dos valores do pedido e o total em execução até o dia atual. Tal tarefa envolve tanto o agrupamento quanto a janela. Você agrupa as linhas pelo ID do cliente e pela data do pedido e aplica uma soma acumulada sobre a soma do grupo dos valores do pedido, assim:
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;
Essa consulta gera a seguinte saída, mostrada aqui de 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 ...
Mesmo que essa técnica seja muito legal, e mesmo que as pesquisas na web por funções de janela aninhadas retornem principalmente essas técnicas, não é isso que o padrão SQL significa por funções de janela aninhada. Como não consegui encontrar nenhuma informação sobre o assunto, tive que descobrir a partir do próprio padrão. Esperançosamente, este artigo aumentará a conscientização sobre o verdadeiro recurso de funções de janela aninhada e fará com que as pessoas recorram à Microsoft e peçam para adicionar suporte a ele no SQL Server.
Quais são as funções de janela aninhada
As funções de janela aninhadas incluem duas funções que você pode aninhar como um argumento de uma função de agregação de janela. Essas são a função de número de linha aninhada e a expressão value_of aninhada na função de linha.
Função de número de linha aninhada
A função de número de linha aninhada permite que você faça referência ao número de linha de marcadores estratégicos em elementos de janela. Aqui está a sintaxe da função:
Os marcadores de linha que você pode especificar são:
- BEGIN_PARTITION
- END_PARTITION
- BEGIN_FRAME
- END_FRAME
- CURRENT_ROW
- FRAME_ROW
Os primeiros quatro marcadores são autoexplicativos. Quanto aos dois últimos, o marcador CURRENT_ROW representa a linha externa atual e o FRAME_ROW representa a linha interna do quadro atual.
Como exemplo para usar a função de número de linha aninhada, considere a tarefa a seguir. Você precisa consultar a visualização Sales.OrderValues e retornar para cada pedido alguns de seus atributos, bem como a diferença entre o valor do pedido atual e a média do cliente, mas excluindo o primeiro e o último pedido do cliente da média.
Essa tarefa é possível sem funções de janela aninhadas, mas a solução envolve algumas etapas:
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;
Aqui está a saída desta consulta, mostrada aqui em 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 funções de número de linha aninhadas, a tarefa é alcançável com uma única consulta, assim:
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;
Além disso, a solução atualmente suportada requer pelo menos uma classificação no plano e várias passagens pelos dados. A solução que usa funções de número de linha aninhadas tem todo o potencial de ser otimizada com a dependência da ordem do índice e um número reduzido de passagens pelos dados. Isso, é claro, depende da implementação.
Expressão value_of aninhada na função de linha
A expressão value_of aninhada na função de linha permite que você interaja com um valor de uma expressão nos mesmos marcadores de linha estratégicos mencionados anteriormente em um argumento de uma função de agregação de janela. Aqui está a sintaxe desta função:
>) OVER(
Como você pode ver, você pode especificar um certo delta negativo ou positivo em relação ao marcador de linha e, opcionalmente, fornecer um valor padrão caso uma linha não exista na posição especificada.
Esse recurso oferece muito poder quando você precisa interagir com diferentes pontos em elementos de janelas. Considere o fato de que, por mais poderosas que as funções de janela possam ser comparadas a ferramentas alternativas como subconsultas, o que as funções de janela não suportam é um conceito básico de correlação. Usando o marcador CURRENT_ROW, você obtém acesso à linha externa e, dessa forma, emula as correlações. Ao mesmo tempo, você se beneficia de todas as vantagens que as funções de janela têm em comparação com as subconsultas.
Como exemplo, suponha que você precise consultar a visualização Sales.OrderValues e retornar para cada pedido alguns de seus atributos, bem como a diferença entre o valor do pedido atual e a média do cliente, mas excluindo os pedidos feitos na mesma data que a data atual do pedido. Isso requer uma capacidade semelhante a uma correlação. Com a expressão value_of aninhada na função de linha, usando o marcador CURRENT_ROW, isso é possível facilmente assim:
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;
Essa consulta deve gerar a seguinte saída:
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 ...
Se você está pensando que essa tarefa é alcançável com a mesma facilidade com subconsultas correlacionadas, nesse caso simplista você está certo. O mesmo pode ser feito com a seguinte 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;
No entanto, lembre-se de que uma subconsulta opera em uma exibição independente dos dados, enquanto uma função de janela opera no conjunto fornecido como entrada para a etapa de processamento de consulta lógica que manipula a cláusula SELECT. Normalmente, a consulta subjacente tem lógica extra, como junções, filtros, agrupamento e outros. Com subconsultas, você precisa preparar uma CTE preliminar ou repetir a lógica da consulta subjacente também na subconsulta. Com funções de janela, não há necessidade de repetir nenhuma lógica.
Por exemplo, digamos que você deveria operar apenas em pedidos enviados (onde a data de envio não é NULL) que foram tratadas pelo funcionário 3. A solução com a função window precisa adicionar os predicados de filtro apenas uma vez, assim:
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;
Essa consulta deve gerar a seguinte saída:
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 ...
A solução com a subconsulta precisa adicionar os predicados de filtro duas vezes - uma na consulta externa e outra na subconsulta - assim:
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;
É isso ou adicionar um CTE preliminar que cuida de toda a filtragem e qualquer outra lógica. De qualquer forma, você olha para isso, com subconsultas, há mais camadas de complexidade envolvidas.
O outro benefício em funções de janela aninhadas é que, se tivéssemos suporte para aquelas em T-SQL, teria sido fácil emular o suporte completo ausente para a unidade de quadro de janela RANGE. A opção RANGE deve permitir que você defina quadros dinâmicos baseados em um deslocamento do valor de ordenação na linha atual. Por exemplo, suponha que você precise calcular para cada pedido de cliente na visualização Sales.OrderValues o valor da média móvel dos últimos 14 dias. De acordo com o padrão SQL, você pode conseguir isso usando a opção RANGE e o tipo INTERVAL, assim:
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;
Essa consulta deve gerar a seguinte saída:
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 ...
Na data desta escrita, esta sintaxe não é suportada em T-SQL. Se tivéssemos suporte para funções de janela aninhadas em T-SQL, você poderia emular esta consulta com o seguinte 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;
O que há para não gostar?
Dê seu voto
As funções de janela aninhadas padrão parecem um conceito muito poderoso que permite muita flexibilidade na interação com diferentes pontos em elementos de janela. Estou bastante surpreso por não encontrar nenhuma cobertura do conceito além do próprio padrão e que não vejo muitas plataformas implementando-o. Espero que este artigo aumente a conscientização sobre esse recurso. Se você acha que pode ser útil tê-lo disponível em T-SQL, certifique-se de votar!