GREATEST, LEAST e como fazíamos antes delas existirem

Foto por Pixabay (pexels.com)

min(), max(), least(), greatest(): estas funções permitem obter valores mínimo e máximo de um conjunto de valores. Enquanto que as funções lógicas greatest() e least() atuam na análise de várias colunas de uma linha, as funções min() e max() atuam em uma coluna de um subconjunto de linhas.

1. Introdução

A Microsoft acrescentou vários recursos de programação em t-sql na versão 2012 do SQL Server. Mas, desde então, poucos novos recursos de programação t-sql foram implementados, a cada nova versão. Inclusive no artigo Novos recursos de programação para SQL Server vários recursos de programação t-sql estão descritos e para a versão 2022 do SQL Server destaco neste artigo as funções GREATEST e LEAST .

Quem programa em outras linguagens provavelmente sente falta de função que calcule o menor (ou maior) valor, dentro de um conjunto de valores presentes em um registro. Em várias linguagens de programação é comum a presença das funções MAX e MIN, tendo como parâmetro uma lista (ou um vetor) de valores. Por exemplo:

x:= 10;
y:= 20;
z:= 5;
menorValor:= min (x, y, z);

em que a função min() retorna o menor valor dentre as variáveis x, y e z; no exemplo, é o valor 5.

Quando iniciei meus estudos em sql percebi que funções como min() e max() não retornavam o que eu imaginava que fariam mas sim o menor/maior valor de uma coluna, dado um conjunto de linhas.

Se é assim, então como obter o menor/maior valor de um conjunto de colunas de cada linha?

Plágio. Antes de prosseguir, uma observação. Desenvolver códigos T-SQL exige tempo e dedicação para estudos e testes, afora conhecimento técnico. Sempre que utilizo código T-SQL de outra pessoa como base eu informo o nome do autor e/ou o endereço web da página onde foi publicado, de modo a respeitar o esforço de programação de quem o desenvolveu. Do contrário tem-se o plágio, que além de ser uma violação dos direitos autorais é também uma demonstração de falta de ética profissional. Nesse ponto faço minhas as palavras de Edvaldo Castro e de Erickson Ricci presentes no artigo “PLÁGIO – Sério mesmo?”.

2. Mínimos e máximos em T-SQL

Geralmente coloco no final dos artigos os códigos fonte para criação das tabelas e carga dos dados utilizados na demonstração. Entretanto, para este artigo foi utilizada uma tabelinha; assim, coloco-a aqui no ínício, antes do desenvolvimento.

A tabela de demonstração contém cotação de moedas, sendo coletados os valores em 4 horários para cada dia: às 9, 11, 13 e 15 horas. Os dados são fictícios.

Eis o código sql de criação da tabela:

-- código #2.1
-- © José Diz / Porto SQL

CREATE TABLE Cotação (
     Moeda_Origem char(3) not null,
     Moeda_Destino char(3) not null,
     Operação char(1) not null check (Operação in ('C','V')),
     Data_Cotação date not null,
     Cotação_9h_BRT decimal (8,5) not null,
     Cotação_11h_BRT decimal (8,5) not null,
     Cotação_13h_BRT decimal (8,5) not null,
     Cotação_15h_BRT decimal (8,5) not null
);
go

ALTER TABLE Cotação
   add constraint Cotação_I1 primary key (Moeda_Origem, Moeda_Destino, Data_Cotação, Operação)
go

A cotação envolve duas moedas (origem e destino da conversão) e também o tipo de operação (venda ou compra). Algo bem simples. A carga dos dados para testes é a seguinte:

-- código #2.2
-- © José Diz / Porto SQL

INSERT into Cotação values
  ('USD', 'BRL', 'V', '20240115', 4.8581, 4.8755, 4.8767, 4.8675),
  ('USD', 'BRL', 'V', '20240112', 4.8750, 4.8442, 4.8557, 4.8555);

Os códigos de moeda USD e BRL referem-se ao dólar americano e ao real brasileiro, respectivamente. As datas são informadas no formato AAAAMMDD, que é à prova de tolos (detalhes no e-book Dominando datas e horas no SQL Server).

O objetivo é obter qual foi a menor e maior cotação de cada conversão, por dia, considerando-se as 4 cotações coletadas e registradas em cada linha da tabela.

2.1. Funções LEAST() e GREATEST()

A implementação mais óbvia é utilizando as funções GREATEST (maior valor) e LEAST (menor valor). Como mencionado na introdução deste artigo, elas atuam sobre um conjunto de colunas, para cada linha analisada.

GREATEST . Na documentação consta que “ This function returns the maximum value from a list of one or more expressions. ”.

GREATEST ( expression1 [ , …expressionN ] )

As expressões podem ser de qualquer tipo de dados que permitam comparações e podem existir até 154 argumentos. Cada expressão pode ser uma constante, variável, nome de coluna ou função ou ainda combinação de operadores aritméticos, bitwise e de cadeia de caracteres. Mas é preciso ficar atento ao tipo de dados retornado pela função: é o de maior precedência entre os tipos de dados passados para a função. Atento que quando há diferentes tipos de dados, ocorre conversão automática de todos os argumentos para o de mais alta precedência.

LEAST . Conforme documentação, “This function returns the minimum value from a list of one or more expressions”. As várias observações descritas anteriormente para a função GREATEST se aplicam à função LEAST.

Sugiro a leitura atenta da documentação destas duas funções, para conhecer os detalhes.

Demonstração. E agora vamos à aplicação das duas funções com os dados de nosso tamborete:

-- código #2.3
-- © José Diz / Porto SQL

SELECT Moeda_Origem, Moeda_Destino, Data_Cotação, Operação,
       Cotação_9h_BRT, Cotação_11h_BRT, Cotação_13h_BRT, Cotação_15h_BRT,
       greatest (Cotação_9h_BRT, Cotação_11h_BRT, Cotação_13h_BRT, Cotação_15h_BRT) as Maior_Cotação,
       least (Cotação_9h_BRT, Cotação_11h_BRT, Cotação_13h_BRT, Cotação_15h_BRT) as Menor_Cotação
  from Cotação;

No código sql anterior, foram utilizadas as colunas de cotação como argumento. O resultado é o seguinte

2.2. E antes da versão 2022?

Se essas funções somente estão disponíveis a partir da versão 2022 do SQL Server, então como se implementa em instâncias de versões anteriores do SQL Server? É agora que entram em ação as funções MIN() e MAX(), citadas no início deste artigo. Mas como estas funções atuam em uma coluna de um conjunto de linhas, então para cada linha temos que converter as 4 colunas com cotação da linha em 4 linhas com a cotação de cada horário.

Exemplificando, temos que transformar algo como

para a forma

Observe que além da coluna com a cotação também foi acrescentada a coluna com o horário da cotação. Essa tarefa de normalizar a linha pode ser realizada de várias formas e neste artigo vamos demonstrar duas delas.

2.2.1. Unpivot

Uma das formas de normalizar as colunas de cotação horária, ao transformar as colunas em linhas, é o uso do operador UNPIVOT.

-- código #2.4
-- © José Diz / Porto SQL

SELECT Moeda_Origem, Moeda_Destino, Data_Cotação, Operação,
      Horário_Cotação, Cotação
  from Cotação
      unpivot (Cotação
               for Horário_Cotação in (Cotação_9h_BRT, Cotação_11h_BRT, Cotação_13h_BRT, Cotação_15h_BRT)) as U;

O código sql acima gera duas colunas, Horário_Cotação e Cotação, sendo que a primeira contém o horário da cotação (nome da coluna) e a segunda contém o valor que estava na coluna. Observe que as 4 primeiras colunas foram mantidas, pois são elas que identificam cada linha original.

A execução do código #2.4 gera o seguinte resultado:

O passo seguinte é aplicar as funções de agregação MIN() e MAX() a cada subconjunto de dados:

-- código #2.5
-- © José Diz / Porto SQL

with Normalizado as (
SELECT Moeda_Origem, Moeda_Destino, Data_Cotação, Operação,
       substring (Horário_Cotação, 9, (len (Horário_Cotação) - 13)) as Horário_Cotação, Cotação
  from Cotação
       unpivot (Cotação
                for Horário_Cotação 
                in (Cotação_9h_BRT, Cotação_11h_BRT, 
                    Cotação_13h_BRT, Cotação_15h_BRT)
               ) as U
) 
SELECT Moeda_Origem, Moeda_Destino, Data_Cotação, Operação,
       string_a gg (Cotação, ', ') within group (order by cast (Horário_Cotação as int)) as Cotações,
       min (Cotação) as Menor_Cotação,
       max (Cotação) as Maior_Cotação
  from Normalizado
  group by Moeda_Origem, Moeda_Destino, Data_Cotação, Operação;

O código #2.4 foi transformado em uma CTE e o resultado desse CTE foi agrupado pelas colunas que identificavam cada linha, originalmente: Moeda_Origem, Moeda_Destino, Data_Cotação, Operação. Além disso, os valores de cotação foram reagrupados, de modo a permitir a comparação entre as cotações da linha original e os valores mínimo e máximo obtidos.

O resultado do código #2.5 é o seguinte:

Um pouco mais complexo de fazer mas que permite obter os valores mínimo e máximo de uma lista de valores que estejam distribuídos em várias colunas de uma mesma linha.

2.2.2. Table value constructor (TVC)

Outra forma de normalizar é utilizando o construtor de valor de tabela (TVC, table value constructor). Com ele podemos criar 4 linhas, em cada linha fornecendo o horário e o conteúdo da respectiva cotação.

-- código #2.6
-- © José Diz / Porto SQL

SELECT Moeda_Origem, Moeda_Destino, Data_Cotação, Operação,
       U.Horário_Cotação, U.Cotação
  from Cotação
       cross apply (values (9, Cotação_9h_BRT), 
                           (11, Cotação_11h_BRT), 
                           (13, Cotação_13h_BRT), 
                           (15, Cotação_15h_BRT)
                   ) as U (Horário_Cotação, Cotação);

À semelhança do resultado do código #2.4, o código sql acima gera duas colunas, Horário_Cotação e Cotação, sendo que a primeira contém o horário da cotação e a segunda contém o valor que estava na coluna:

Observe que as 4 primeiras colunas foram mantidas, pois são elas que identificam cada linha original. A seguir, a aplicação do agrupamento é a mesma utilizada na solução com UNPIVOT:

-- código #2.7
-- © José Diz / Porto SQL

with Normalizado as (
SELECT Moeda_Origem, Moeda_Destino, Data_Cotação, Operação,
       U.Horário_Cotação, U.Cotação
  from Cotação
       cross apply (values (9, Cotação_9h_BRT), 
                           (11, Cotação_11h_BRT), 
                           (13, Cotação_13h_BRT), 
                           (15, Cotação_15h_BRT)
                   ) as U (Horário_Cotação, Cotação)
) 
SELECT Moeda_Origem, Moeda_Destino, Data_Cotação, Operação,
       string_agg (Cotação, ', ') within group (order by Horário_Cotação) as Cotações,
       min (Cotação) as Menor_Cotação,
       max (Cotação) as Maior_Cotação
  from Normalizado
  group by Moeda_Origem, Moeda_Destino, Data_Cotação, Operação;

O resultado é o mesmo do código #2.5.

3. Observações finais

Há outras formas de normalizar linhas além das apresentadas no item 2.2, mas considero as duas o  suficiente para demonstrar o processo de normalização em tempo de execução.

Este artigo não teve o propósito de comparar o desempenho das soluções descritas; entretanto, espera-se que as funções nativas GREATEST e LEAST tenham melhor desempenho. Pelo menos o uso delas permite um código sql limpo e de fácil compreensão e manutenção.

Embora essa função tenha sido implementada somente na versão 2022 do SQL Server, ela já estava disponível no Azure SQL desde final de 2020.

No momento em que escrevo este artigo, janeiro de 2024, acho que o percentual de instalações com o SQL Server 2022 ainda seja reduzido. Talvez ainda não seja o momento de utilizar estas novas funções, a não ser que se tenha a certeza de que o código sql somente será rodado em instância com a versão 2022 (ou mais recente) do sql server.

4. Material de estudo

Neste capítulo você encontra agrupados links para documentação de itens que foram mencionados no artigo bem como artigos sobre o mesmo assunto.

4.1. Documentação

GREATEST

LEAST

MAX

MIN

Table value constructor

UNPIVOT

 

4.2. Artigos

 

As novas funções GREATEST e LEAST

Find MAX value from multiple columns in a SQL Server table

 

 

Deixe um comentário

Este site utiliza o Akismet para reduzir spam. Saiba como seus dados em comentários são processados.