Database
 sql >> Base de Dados >  >> RDS >> Database

Funções de janela aninhadas em SQL


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:
(ROW_NUMBER()>) OVER()
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:
( VALUE OF AT [] [, ]
>) 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!