Vamos começar nossa jornada SQL para entender a agregação de dados em SQL e tipos de agregações, incluindo agregações simples e deslizantes.
Antes de passarmos para as agregações, vale a pena considerar fatos interessantes muitas vezes esquecidos por alguns desenvolvedores quando se trata de SQL em geral e da agregação em particular.
Neste artigo, SQL se refere ao T-SQL, que é a versão do SQL da Microsoft e possui mais recursos do que o SQL padrão.
Matemática por trás do SQL
É muito importante entender que o T-SQL é baseado em alguns conceitos matemáticos sólidos, embora não seja uma linguagem baseada em matemática rígida.
De acordo com o livro “Microsoft_SQL_Server_2008_T_SQL_Fundamentals” de Itzik Ben-Gan, o SQL foi projetado para consultar e gerenciar dados em um sistema de gerenciamento de banco de dados relacional (RDBMS).
O próprio sistema de gerenciamento de banco de dados relacional é baseado em dois sólidos ramos matemáticos:
- Teoria dos Conjuntos
- Lógica de predicados
Teoria dos Conjuntos
A teoria dos conjuntos, como o nome indica, é um ramo da matemática sobre conjuntos que também pode ser chamado de coleções de objetos distintos definidos.
Em suma, na teoria dos conjuntos, pensamos em coisas ou objetos como um todo da mesma forma que pensamos em um item individual.
Por exemplo, um livro é um conjunto de todos os livros distintos definidos, então, tomamos um livro como um todo que é suficiente para obter detalhes de todos os livros nele contidos.
Lógica de predicados
A lógica de predicados é uma lógica booleana que retorna verdadeiro ou falso dependendo da condição ou dos valores das variáveis.
A lógica de predicado pode ser usada para impor regras de integridade (o preço deve ser maior que 0,00) ou filtrar dados (onde o preço é maior que 10,00), porém, no contexto do T-SQL, temos três valores lógicos da seguinte forma:
- Verdadeiro
- Falso
- Desconhecido (Nulo)
Isso pode ser ilustrado da seguinte forma:
Um exemplo de predicado é “Onde Price of Book é maior que 10,00”.
Isso é o suficiente sobre matemática, mas lembre-se de que vou me referir a isso mais tarde no artigo.
Por que agregar dados em SQL é fácil
Agregar dados em SQL em sua forma mais simples é conhecer os totais de uma só vez.
Por exemplo, se tivermos uma tabela de clientes que contém uma lista de todos os clientes junto com seus detalhes, os dados agregados da tabela de clientes podem nos fornecer o número total de clientes que temos.
Conforme discutido anteriormente, pensamos em um conjunto como um único item, então simplesmente aplicamos uma função de agregação à tabela para obter os totais.
Como o SQL é originalmente uma linguagem baseada em conjunto (como discutido anteriormente), é relativamente mais fácil aplicar funções agregadas a ela em comparação com outras linguagens.
Por exemplo, se tivermos uma tabela de produtos que tenha registros de todos os produtos no banco de dados, podemos aplicar imediatamente a função de contagem a uma tabela de produtos para obter o número total de produtos em vez de contá-los um a um em um loop.
Receita de agregação de dados
Para agregar dados no SQL, precisamos no mínimo do seguinte:
- Dados (tabela) com colunas que, quando agregadas, fazem sentido
- Uma função agregada a ser aplicada aos dados
Preparando dados de amostra (tabela)
Vamos dar um exemplo de uma tabela de pedidos simples que contém três coisas (colunas):
- Número do pedido (OrderId)
- Data em que o pedido foi feito (OrderDate)
- Valor do pedido (TotalAmount)
Vamos criar o banco de dados AggregateSample para prosseguir:
-- Create aggregate sample database CREATE DATABASE AggregateSample
Agora crie a tabela de pedidos no banco de dados de exemplo da seguinte forma:
-- Create order table in the aggregate sample database USE AggregateSample CREATE TABLE SimpleOrder (OrderId INT PRIMARY KEY IDENTITY(1,1), OrderDate DATETIME2, TotalAmount DECIMAL(10,2) )
Preenchendo dados de amostra
Preencha a tabela adicionando uma linha:
INSERT INTO dbo.SimpleOrder ( OrderDate ,TotalAmount ) VALUES ( '20180101' -- OrderDate - datetime2 ,20.50 -- TotalAmount - decimal(10, 2) ); GO
Vejamos agora a tabela:
-- View order table SELECT OrderId ,OrderDate ,TotalAmount FROM SimpleOrder
Observe que estou usando o dbForge Studio para SQL Server neste artigo, portanto, apenas a aparência da saída pode ser diferente se você executar o mesmo código no SSMS (SQL Server Management Studio), não há diferença no que diz respeito aos scripts e seus resultados.
Funções básicas de agregação
As funções agregadas básicas que podem ser aplicadas à tabela são as seguintes:
- Soma
- Contagem
- Mínimo
- Máx.
- Média
Agregando Tabela de Registro Único
Agora a pergunta interessante é:“podemos agregar (somar ou contar) dados (registros) em uma tabela se ela tiver apenas uma linha como no nosso caso?” A resposta é “Sim”, podemos, embora não faça muito sentido, mas pode nos ajudar a entender como os dados ficam prontos para agregação.
Para obter o número total de pedidos, usamos a função count() com a tabela, como discutido anteriormente, podemos simplesmente aplicar a função agregada à tabela, pois SQL é uma linguagem baseada em conjuntos e as operações podem ser aplicadas a um conjunto diretamente.
-- Getting total number of orders placed so far SELECT COUNT(*) AS Total_Orders FROM SimpleOrder
Agora, e o pedido com valor mínimo, máximo e médio para um único registro:
-- Getting order with minimum amount, maximum amount, average amount and total orders SELECT COUNT(*) AS Total_Orders ,MIN(TotalAmount) AS Min_Amount ,MAX(TotalAmount) AS Max_Amount ,AVG(TotalAmount) Average_Amount FROM SimpleOrder
Como podemos ver na saída, o valor mínimo, máximo e médio é o mesmo se tivermos um único registro, portanto, é possível aplicar uma função de agregação a um único registro, mas nos dá os mesmos resultados.
Precisamos de pelo menos mais de um registro para entender os dados agregados.
Agregando a tabela de vários registros
Vamos agora adicionar mais quatro registros da seguinte forma:
INSERT INTO dbo.SimpleOrder ( OrderDate ,TotalAmount ) VALUES ( '20180101' -- OrderDate - datetime2 ,20.50 -- TotalAmount - decimal(10, 2) ), ( '20180102' -- OrderDate - datetime2 ,30.50 -- TotalAmount - decimal(10, 2) ), ( '20180103' -- OrderDate - datetime2 ,10.50 -- TotalAmount - decimal(10, 2) ), ( '20180110' -- OrderDate - datetime2 ,100.50 -- TotalAmount - decimal(10, 2) ); GO
A tabela agora tem a seguinte aparência:
Se aplicarmos as funções agregadas à tabela agora, obteremos bons resultados:
-- Getting order with minimum amount, maximum amount, average amount and total orders SELECT COUNT(*) AS Total_Orders ,MIN(TotalAmount) AS Min_Amount ,MAX(TotalAmount) AS Max_Amount ,AVG(TotalAmount) Average_Amount FROM SimpleOrder
Agrupando dados agregados
Podemos agrupar os dados agregados por qualquer coluna ou conjunto de colunas para obter agregados com base nessa coluna.
Por exemplo, se quisermos saber o número total de pedidos por data, temos que agrupar a tabela por data usando a cláusula Group by da seguinte forma:
-- Getting total orders per date SELECT OrderDate ,COUNT(*) AS Total_Orders FROM SimpleOrder GROUP BY OrderDate
A saída é a seguinte:
Então, se quisermos ver a soma de todo o valor do pedido, podemos simplesmente aplicar a função soma à coluna de valor total sem nenhum agrupamento da seguinte forma:
-- Sum of all the orders amount SELECT SUM(TotalAmount) AS Sum_of_Orders_Amount FROM SimpleOrder
Para obter a soma do valor dos pedidos por data, simplesmente adicionamos group by date à instrução SQL acima da seguinte forma:
-- Sum of all the orders amount per date SELECT OrderDate ,SUM(TotalAmount) AS Sum_of_Orders FROM SimpleOrder GROUP BY OrderDate
Como obter totais sem agrupar dados
Podemos obter imediatamente totais como total de pedidos, valor máximo do pedido, valor mínimo do pedido, soma do valor dos pedidos, valor médio do pedido sem a necessidade de agrupá-lo se a agregação for destinada a todas as tabelas.
-- Getting order with minimum amount, maximum amount, average amount, sum of amount and total orders SELECT COUNT(*) AS Total_Orders ,MIN(TotalAmount) AS Min_Amount ,MAX(TotalAmount) AS Max_Amount ,AVG(TotalAmount) AS Average_Amount ,SUM(TotalAmount) AS Sum_of_Amount FROM SimpleOrder
Adicionando clientes aos pedidos
Vamos adicionar um pouco de diversão adicionando clientes em nossa tabela. Podemos fazer isso criando outra tabela de clientes e passando o ID do cliente para a tabela de pedidos, no entanto, para simplificar e simular o estilo de data warehouse (onde as tabelas são desnormalizadas), estou adicionando a coluna de nome do cliente na tabela de pedidos da seguinte maneira :
-- Adding CustomerName column and data to the order table ALTER TABLE SimpleOrder ADD CustomerName VARCHAR(40) NULL GO UPDATE SimpleOrder SET CustomerName = 'Eric' WHERE OrderId = 1 GO UPDATE SimpleOrder SET CustomerName = 'Sadaf' WHERE OrderId = 2 GO UPDATE SimpleOrder SET CustomerName = 'Peter' WHERE OrderId = 3 GO UPDATE SimpleOrder SET CustomerName = 'Asif' WHERE OrderId = 4 GO UPDATE SimpleOrder SET CustomerName = 'Peter' WHERE OrderId = 5 GO
Como obter o total de pedidos por cliente
Você consegue adivinhar agora como obter o total de pedidos por cliente? Você precisa agrupar por cliente (CustomerName) e aplicar a função agregada count() a todos os registros da seguinte forma:
-- Total orders per customer SELECT CustomerName,COUNT(*) AS Total_Orders FROM SimpleOrder GROUP BY CustomerName
Adicionando mais cinco registros à tabela de pedidos
Agora vamos adicionar mais cinco linhas à tabela de pedidos simples da seguinte forma:
-- Adding 5 more records to order table INSERT INTO SimpleOrder (OrderDate, TotalAmount, CustomerName) VALUES ('01-Jan-2018', 70.50, 'Sam'), ('02-Jan-2018', 170.50, 'Adil'), ('03-Jan-2018',50.00,'Sarah'), ('04-Jan-2018',50.00,'Asif'), ('11-Jan-2018',50.00,'Peter') GO
Confira agora os dados:
-- Viewing order table after adding customer name and five more rows SELECT OrderId,CustomerName,OrderDate,TotalAmount FROM SimpleOrder GO
Obtendo o total de pedidos por cliente classificados por pedidos do máximo para o mínimo
Se você estiver interessado no total de pedidos por cliente classificados do máximo ao mínimo, não é uma má ideia dividir isso em etapas menores da seguinte forma:
-- (1) Getting total orders SELECT COUNT(*) AS Total_Orders FROM SimpleOrder
-- (2) Getting total orders per customer SELECT CustomerName,COUNT(*) AS Total_Orders FROM SimpleOrder GROUP BY CustomerName
Para classificar a contagem de pedidos do máximo para o mínimo, precisamos usar a cláusula Order By DESC (ordem decrescente) com count() no final da seguinte forma:
-- (3) Getting total orders per customer from maximum to minimum orders SELECT CustomerName,COUNT(*) AS Total_Orders FROM SimpleOrder GROUP BY CustomerName ORDER BY COUNT(*) DESC
Obtendo o total de pedidos por data ordenados primeiro pelo pedido mais recente
Usando o método acima, agora podemos descobrir o total de pedidos por data classificados primeiro pelo pedido mais recente da seguinte maneira:
-- Getting total orders per date from most recent first SELECT CAST(OrderDate AS DATE) AS OrderDate,COUNT(*) AS Total_Orders FROM SimpleOrder GROUP BY OrderDate ORDER BY OrderDate DESC
A função CAST nos ajuda a obter apenas a parte da data. A saída é a seguinte:
Você pode usar quantas combinações forem possíveis, desde que façam sentido.
Executando agregações
Agora que estamos familiarizados com a aplicação de funções agregadas aos nossos dados, vamos passar para a forma avançada de agregações e uma dessas agregações é a agregação em execução.
As agregações em execução são as agregações aplicadas a um subconjunto de dados, e não a todo o conjunto de dados, o que nos ajuda a criar pequenas janelas nos dados.
Até agora vimos que todas as funções agregadas são aplicadas a todas as linhas da tabela que podem ser agrupadas por alguma coluna como data do pedido ou nome do cliente, mas com agregações em execução temos a liberdade de aplicar as funções agregadas sem agrupar o todo conjunto de dados.
Obviamente, isso significa que podemos aplicar a função agregada sem usar a cláusula Group By, que é um pouco estranha para aqueles iniciantes em SQL (ou às vezes alguns desenvolvedores ignoram isso) que não estão familiarizados com as funções de janelas e agregações em execução.
Windows em dados
Como dito anteriormente, a agregação em execução é aplicada a um subconjunto de conjunto de dados ou (em outras palavras) em pequenas janelas de dados.
Pense nas janelas como um(s) conjunto(s) dentro de um conjunto ou uma(s) mesa(s) dentro de uma mesa. Um bom exemplo de janela de dados em nosso caso é que temos a tabela de pedidos que contém pedidos feitos em datas diferentes, então, e se cada data for uma janela separada, podemos aplicar funções agregadas em cada janela da mesma maneira que aplicamos a a mesa.
Se ordenarmos a tabela de pedidos (SimpleOrder) pela data do pedido (OrderDate) da seguinte forma:
-- View order table sorted by order date SELECT so.OrderId ,so.OrderDate ,so.TotalAmount ,so.CustomerName FROM SimpleOrder so ORDER BY so.OrderDate
Windows em dados prontos para execução de agregações pode ser visto abaixo:
Também podemos considerar essas janelas ou subconjuntos como seis mini tabelas baseadas em datas de pedidos e agregados podem ser aplicados em cada uma dessas minitabelas.
Uso de Partition By dentro da cláusula OVER()
As agregações em execução podem ser aplicadas particionando a tabela usando “Partition by” dentro da cláusula OVER().
Por exemplo, se quisermos particionar a tabela de pedidos por datas, como cada data é uma subtabela ou janela no conjunto de dados, temos que Particionar os dados por data de pedido e isso pode ser feito usando uma função agregada como COUNT( ) com OVER() e Partition por dentro de OVER() da seguinte forma:
-- Running Aggregation on Order table by partitioning by dates SELECT OrderDate, Total_Orders=COUNT(*) OVER(PARTITION BY OrderDate) FROM SimpleOrder
Janela de totais em execução por data (partição)
A execução de agregações nos ajuda a limitar o escopo de agregação apenas à janela definida e podemos obter totais em execução por janela da seguinte maneira:
-- Getting total orders, minimum amount, maximum amount, average amount and sum of all amounts per date window (partition by date) SELECT CAST (OrderDate AS DATE) AS OrderDate, Count=COUNT(*) OVER (PARTITION BY OrderDate), Min_Amount=MIN(TotalAmount) OVER (PARTITION BY OrderDate) , Max_Amount=MAX(TotalAmount) OVER (PARTITION BY OrderDate) , Average_Amount=AVG(TotalAmount) OVER (PARTITION BY OrderDate), Sum_Amount=SUM(TotalAmount) OVER (PARTITION BY OrderDate) FROM SimpleOrder
Como obter totais em execução por janela do cliente (partição)
Assim como os totais em execução por janela de data, também podemos calcular os totais em execução por janela de cliente particionando o conjunto de pedidos (tabela) em pequenos subconjuntos de clientes (partições) da seguinte forma:
-- Getting total orders, minimum amount, maximum amount, average amount and sum of all amounts per customer window (partition by customer) SELECT CustomerName, CAST (OrderDate AS DATE) AS OrderDate, Count=COUNT(*) OVER (PARTITION BY CustomerName), Min_Amount=MIN(TotalAmount) OVER (PARTITION BY CustomerName) , Max_Amount=MAX(TotalAmount) OVER (PARTITION BY CustomerName) , Average_Amount=AVG(TotalAmount) OVER (PARTITION BY CustomerName), Sum_Amount=SUM(TotalAmount) OVER (PARTITION BY CustomerName) FROM SimpleOrder ORDER BY Count DESC,OrderDate
Agregações deslizantes
As agregações deslizantes são as agregações que podem ser aplicadas aos quadros dentro de uma janela, o que significa restringir ainda mais o escopo dentro da janela (partição).
Em outras palavras, totais em execução nos dão totais (soma, média, min, max, count) para toda a janela (subconjunto) que criamos dentro de uma tabela, enquanto totais deslizantes nos dão totais (soma, média, min, max, count) para o quadro (subconjunto do subconjunto) dentro da janela (subconjunto) da tabela.
Por exemplo, se criarmos uma janela de dados com base no cliente (partição por cliente), podemos ver que o cliente “Peter” tem três registros em sua janela e todas as agregações são aplicadas a esses três registros. Agora, se quisermos criar um quadro para duas linhas apenas de cada vez, isso significa que a agregação é ainda mais restrita e é aplicada à primeira e segunda linhas e depois à segunda e terceira linhas e assim por diante.
Uso de ROWS PRECEDENTES com Order By dentro da cláusula OVER()
Agregações deslizantes podem ser aplicadas adicionando ROWS
Por exemplo, se quisermos agregar dados de apenas duas linhas por vez para cada cliente, precisamos que as agregações deslizantes sejam aplicadas à tabela de pedidos da seguinte forma:
-- Getting minimum amount, maximum amount, average amount per frame per customer window SELECT CustomerName, Min_Amount=Min(TotalAmount) OVER (PARTITION BY CustomerName ORDER BY OrderDate ROWS 1 PRECEDING), Max_Amount=Max(TotalAmount) OVER (PARTITION BY CustomerName ORDER BY OrderDate ROWS 1 PRECEDING) , Average_Amount=AVG(TotalAmount) OVER (PARTITION BY CustomerName ORDER BY OrderDate ROWS 1 PRECEDING) FROM SimpleOrder so ORDER BY CustomerName
Para entender como funciona, vejamos a tabela original no contexto de molduras e janelas:
Na primeira linha da janela do cliente Peter, ele fez um pedido com o valor de 30,50, pois este é o início do quadro dentro da janela do cliente, portanto, min e max são os mesmos, pois não há linha anterior para comparar.
Em seguida, o valor mínimo permanece o mesmo, mas o máximo se torna 100,50, pois o valor da linha anterior (primeira linha) é 30,50 e o valor dessa linha é 100,50, portanto, o máximo dos dois é 100,50.
Em seguida, passando para a terceira linha, a comparação ocorrerá com a segunda linha, de modo que o valor mínimo das duas seja 50,00 e o valor máximo das duas linhas seja 100,50.
Função MDX até a data (YTD) e agregações em execução
MDX é uma linguagem de expressão multidimensional usada para consultar dados multidimensionais (como cubo) e é usada em soluções de business intelligence (BI).
De acordo com https://docs.microsoft.com/en-us/sql/mdx/ytd-mdx, a função Year to Date (YTD) no MDX funciona da mesma maneira que as agregações em execução ou deslizantes. Por exemplo, YTD frequentemente usado em combinação com nenhum parâmetro fornecido exibe um total acumulado até o momento.
Isso significa que, se aplicarmos essa função no ano, ela fornecerá todos os dados do ano, mas se detalharmos até março, ela nos dará todos os totais desde o início do ano até março e assim por diante.
Isso é muito útil em relatórios do SSRS.
Coisas para fazer
É isso! Você está pronto para fazer algumas análises básicas de dados depois de ler este artigo e pode melhorar ainda mais suas habilidades com as seguintes coisas:
- Tente escrever um script de agregados em execução criando janelas em outras colunas, como Valor total.
- Tente também escrever um script de agregados deslizantes criando quadros em outras colunas, como Valor total.
- Você pode adicionar mais colunas e registros à tabela (ou até mais tabelas) para tentar outras combinações de agregação.
- Os scripts de exemplo mencionados neste artigo podem ser transformados em procedimentos armazenados para serem usados em relatórios do SSRS por trás de conjuntos de dados.
Referências:
- Ano (MDX)
- dbForge Studio para SQL Server