Em 2012, escrevi um post aqui destacando abordagens para calcular uma mediana. Nesse post, eu lidei com o caso muito simples:queríamos encontrar a mediana de uma coluna em uma tabela inteira. Foi-me mencionado várias vezes desde então que um requisito mais prático é calcular uma mediana particionada . Como no caso básico, existem várias maneiras de resolver isso em várias versões do SQL Server; não surpreendentemente, alguns têm um desempenho muito melhor do que outros.
No exemplo anterior, tínhamos apenas as colunas genéricas id e val. Vamos tornar isso mais realista e dizer que temos vendedores e o número de vendas que eles fizeram em algum período. Para testar nossas consultas, vamos primeiro criar um heap simples com 17 linhas e verificar se todas elas produzem os resultados esperados (SalesPerson 1 tem uma mediana de 7,5 e SalesPerson 2 tem uma mediana de 6,0):
CREATE TABLE dbo.Vendas(Vendedor INT, Valor INT);GO INSERT dbo.Vendas COM (TABLOCKX)(Vendedor, Valor) VALUES(1, 6 ),(1, 11),(1, 4 ),( 1,4),(1,15),(1,14),(1,4),(1,9),(2,6),(2,11),(2,4),(2, 4 ),(2, 15),(2, 14),(2, 4);
Aqui estão as consultas, que vamos testar (com muito mais dados!) no heap acima, bem como com os índices de suporte. Descartei algumas consultas do teste anterior, que não eram dimensionadas ou não mapeavam muito bem para medianas particionadas (ou seja, 2000_B, que usava uma tabela #temp, e 2005_A, que usava linhas opostas números). No entanto, adicionei algumas ideias interessantes de um artigo recente de Dwain Camps (@DwainCSQL), que se baseou no meu post anterior.
SQL Server 2000+
O único método da abordagem anterior que funcionou bem o suficiente no SQL Server 2000 para incluí-lo neste teste foi a abordagem "min of one half, max of the other":
SELECT DISTINCT s.SalesPerson, Median =((SELECT MAX(Amount) FROM (SELECT TOP 50 PERCENT Amount FROM dbo.Sales WHERE SalesPerson =s.SalesPerson ORDER BY Amount) AS t) + (SELECT MIN(Amount) FROM (SELECT TOP 50 PERCENT Amount FROM dbo.Sales WHERE SalesPerson =s.SalesPerson ORDER BY Amount DESC) AS b)) / 2.0FROM dbo.Sales AS s;
Honestamente, tentei imitar a versão da tabela #temp que usei no exemplo mais simples, mas não escalou bem. Em 20 ou 200 linhas funcionou bem; em 2000, levou quase um minuto; em 1.000.000 eu desisti depois de uma hora. Eu o incluí aqui para a posteridade (clique para revelar).
CREATE TABLE #x( i INT IDENTITY(1,1), SalesPerson INT, Valor INT, i2 INT); CRIAR ÍNDICE CLUSTERED v ON #x(SalesPerson, Amount); INSERT #x(SalesPerson, Amount) SELECT SalesPerson, Amount FROM dbo.Sales ORDER BY SalesPerson,Amount OPTION (MAXDOP 1); UPDATE x SET i2 =i-( SELECT COUNT(*) FROM #x WHERE i <=x.i AND SalesPersonSQL Server 2005+ 1
Isso usa duas funções de janela diferentes para derivar uma sequência e contagem geral de valores por vendedor.
SELECT SalesPerson, Median =AVG(1.0*Amount)FROM( SELECT SalesPerson, Amount, rn =ROW_NUMBER() OVER (PARTITION BY SalesPerson ORDER BY Amount), c =COUNT(*) OVER (PARTITION BY SalesPerson) FROM dbo .Sales)AS xWHERE rn IN ((c + 1)/2, (c + 2)/2)GROUP BY SalesPerson;SQL Server 2005+ 2
Isso veio do artigo de Dwain Camps, que faz o mesmo que acima, de uma maneira um pouco mais elaborada. Isso basicamente desarticula a(s) linha(s) interessante(s) em cada grupo.
;WITH conta AS( SELECT Vendedor, c FROM ( SELECT Vendedor, c1 =(c+1)/2, c2 =CASE c%2 WHEN 0 THEN 1+c/2 ELSE 0 END FROM ( SELECT Vendedor, c =COUNT(*) FROM dbo.Sales GROUP BY SalesPerson ) a ) a CROSS APPLY (VALUES(c1),(c2)) b(c))SELECT a.SalesPerson, Median=AVG(0.+b.Amount)FROM (SELECT SalesPerson, Amount, rn =ROW_NUMBER() OVER (PARTITION BY SalesPerson ORDER BY Amount) FROM dbo.Sales a) aCROSS APPLY( SELECT Amount FROM Counts b WHERE a.SalesPerson =b.SalesPerson AND a.rn =b.c) bGROUP POR um.Vendedor;SQL Server 2005+ 3
Isso foi baseado em uma sugestão de Adam Machanic nos comentários do meu post anterior e também aprimorado por Dwain em seu artigo acima.
;WITH Count AS( SELECT SalesPerson, c =COUNT(*) FROM dbo.Sales GROUP BY SalesPerson)SELECT a.SalesPerson, Median =AVG(0.+Amount)FROM Counts aCROSS APPLY( SELECT TOP (((a.c - 1) / 2) + (1 + (1 - a.c % 2))) b.Amount, r =ROW_NUMBER() OVER (ORDER BY b.Amount) FROM dbo.Sales b WHERE a.SalesPerson =b.SalesPerson ORDER POR b.Valor) pWHERE r ENTRE ((a.c - 1) / 2) + 1 E (((a.c - 1) / 2) + (1 + (1 - a.c % 2)))GRUPO POR a.Vendedor;SQL Server 2005+ 4
Isso é semelhante a "2005+ 1" acima, mas em vez de usarCOUNT(*) OVER()
para derivar as contagens, ele executa uma autojunção em uma agregação isolada em uma tabela derivada.
SELECT SalesPerson, Median =AVG(1.0 * Amount)FROM( SELECT s.SalesPerson, s.Amount, rn =ROW_NUMBER() OVER (PARTITION BY s.SalesPerson ORDER BY s.Amount), c.c FROM dbo.Sales AS s INNER JOIN ( SELECT SalesPerson, c =COUNT(*) FROM dbo.Sales GROUP BY SalesPerson ) AS c ON s.SalesPerson =c.SalesPerson) AS xWHERE rn IN ((c + 1)/2, (c + 2) /2)GRUPO POR Vendedor;SQL Server 2012+ 1
Esta foi uma contribuição muito interessante do colega MVP do SQL Server Peter "Peso" Larsson (@SwePeso) nos comentários do artigo de Dwain; ele usaCROSS APPLY
e o novoOFFSET / FETCH
funcionalidade de uma forma ainda mais interessante e surpreendente do que a solução de Itzik para o cálculo da mediana mais simples.
SELECT d.Vendedor, w.MedianFROM( SELECT SalesPerson, COUNT(*) AS y FROM dbo.Sales GROUP BY SalesPerson) AS dCROSS APPLY( SELECT AVG(0E + Amount) FROM ( SELECT z.Amount FROM dbo.Sales AS z WHERE z.SalesPerson =d.SalesPerson ORDER BY z.Amount OFFSET (d.y - 1) / 2 ROWS FETCH NEXT 2 - d.y % 2 Rows ONLY ) AS f) AS w(Median);SQL Server 2012+ 2
Finalmente, temos o novoPERCENTILE_CONT()
função introduzida no SQL Server 2012.
SELECT SalesPerson, Median =MAX(Median)FROM( SELECT SalesPerson,Median =PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY Amount) OVER (PARTITION BY SalesPerson) FROM dbo.Sales) AS xGROUP BY SalesPerson;Os testes reais
Para testar o desempenho das consultas acima, vamos construir uma tabela muito mais substancial. Teremos 100 vendedores únicos, com 10.000 valores de vendas cada um, para um total de 1.000.000 de linhas. Também executaremos cada consulta no heap como está, com um índice não clusterizado adicionado em(SalesPerson, Amount)
, e com um índice clusterizado nas mesmas colunas. Aqui está a configuração:
CREATE TABLE dbo.Sales(SalesPerson INT, Amount INT);GO --CREATE CLUSTERED INDEX x ON dbo.Sales(SalesPerson, Amount);--CREATE NONCLUSTERED INDEX x ON dbo.Sales(SalesPerson, Amount);- -DROP INDEX x ON dbo.vendas;;WITH x AS ( SELECT TOP (100) número FROM master.dbo.spt_values GROUP BY número)INSERT dbo.Sales WITH (TABLOCKX) (Vendedor, Valor) SELECT x.number, ABS(CHECKSUM(NEWID())) % 99 FROM x CROSS JOIN x AS x2 CROSS JOIN x AS x3;
E aqui estão os resultados das consultas acima, em relação ao heap, ao índice não clusterizado e ao índice clusterizado:
Duração, em milissegundos, de várias abordagens medianas agrupadas (contra um pilha)
Duração, em milissegundos, de várias abordagens medianas agrupadas (contra um heap com um índice não clusterizado)
Duração, em milissegundos, de várias abordagens medianas agrupadas (contra um índice clusterizado)
E quanto a Hekaton?
Naturalmente, fiquei curioso para saber se esse novo recurso do SQL Server 2014 poderia ajudar em alguma dessas consultas. Então eu criei um banco de dados In-Memory, duas versões In-Memory da tabela Sales (uma com um índice de hash em(SalesPerson, Amount)
, e o outro apenas em(SalesPerson)
), e executei novamente os mesmos testes:
CREATE DATABASE Hekaton;GOALTER DATABASE Hekaton ADD FILEGROUP xtp CONTAINS MEMORY_OPTIMIZED_DATA;GOALTER DATABASE Hekaton ADD FILE (name ='xtp', filename ='c:\temp\hek.mod') TO FILEGROUP xtp;GOALTER DATABASE Hekaton SET MEMORY_OPTIMIZED_ELEVATE_TO_SNAPSHOT ON;GO USE Hekaton;GO CREATE TABLE dbo.Sales1( ID INT IDENTITY(1,1) PRIMARY KEY NONCLUSTERED, SalesPerson INT NOT NULL, Amount INT NOT NULL, INDEX x NONCLUSTERED HASH (Vendedor, Amount) WITH (BUCKET_COUNT =256) )WITH (MEMORY_OPTIMIZED =ON, DURABILITY =SCHEMA_AND_DATA);GO CREATE TABLE dbo.Sales2( ID INT IDENTITY(1,1) PRIMARY KEY NONCLUSTERED, SalesPerson INT NOT NULL, Amount INT NOT NULL, INDEX x NONCLUSTERED HASH (Vendedor) WITH ( BUCKET_COUNT =256))WITH (MEMORY_OPTIMIZED =ON, DURABILITY =SCHEMA_AND_DATA);GO;WITH x AS ( SELECT TOP (100) número FROM master.dbo.spt_values GROUP BY number)INSERT dbo.Sales1 (Vendedor, Valor) -- TABLOCK /TABLOCKX não permitido aqui SELECT x.number, ABS(CHECKSUM(NEWID())) % 99 DE x CROSS JOIN x AS x2 CROSS JOIN x AS x3; INSERT dbo.Vendas2 (Vendedor, Valor) SELECT Vendedor, Valor FROM dbo.Vendas1;
Os resultados:
Duração, em milissegundos, para vários cálculos de mediana em relação à memória tabelas
Mesmo com o índice de hash correto, não vemos melhorias significativas em relação a uma tabela tradicional. Além disso, tentar resolver o problema da mediana usando um procedimento armazenado compilado nativamente não será uma tarefa fácil, pois muitas das construções de linguagem usadas acima não são válidas (também fiquei surpreso com algumas delas). A tentativa de compilar todas as variações de consulta acima gerou essa série de erros; alguns ocorreram várias vezes em cada procedimento e, mesmo após a remoção de duplicatas, isso ainda é meio cômico:
Msg 10794, Level 16, State 47, Procedure GroupedMedian_2000
A opção 'DISTINCT' não é suportada com procedimentos armazenados compilados nativamente.
Msg 12311, Level 16, State 37, Procedure GroupedMedian_2000
Subconsultas ( consultas aninhadas dentro de outra consulta) não são compatíveis com procedimentos armazenados compilados nativamente.
Mensagem 10794, Nível 16, Estado 48, Procedimento GroupedMedian_2000
A opção 'PERCENT' não é compatível com procedimentos armazenados compilados nativamente.
Mensagem 12311, Nível 16, Estado 37, Procedimento GroupedMedian_2005_1
Subconsultas (consultas aninhadas dentro de outra consulta) não são suportadas com procedimentos armazenados compilados nativamente.
Mensagem 10794, Nível 16, Estado 91 , Procedimento GroupedMedian_2005_1
A função agregada 'ROW_NUMBER' não é suportada com procedimentos armazenados compilados nativamente.
Mensagem 10794, Nível 16, Estado 56, Procedimento GroupedMedian_2005_1
O operador 'IN' não é suportado com procedimentos armazenados compilados nativamente.
Msg 12310, Nível 16, estado 36, procedimento GroupedMedian_2005_2
Common Table Expressions (CTE) não são suportados com procedimentos armazenados compilados nativamente.
Mensagem 12309, nível 16, estado 35, procedimento GroupedMedian_2005_2
Instruções do formulário INSERT…VALUES… que inserem várias linhas não são suportados com procedimentos armazenados compilados nativamente.
Mensagem 10794, Nível 16, Estado 53, Procedimento GroupedMedian_2005_2
O operador 'APPLY' não é suportado com procedimentos armazenados compilados nativamente.
Mensagem 12311, Nível 16, Estado 37, Procedimento GroupedMedian_2005_2
Subconsultas (consultas aninhadas dentro de outra consulta) não são suportadas com procedimentos armazenados compilados nativamente.
Mensagem 10794, Nível 16, Estado 91, Procedimento GroupedMedian_2005_2
A função agregada 'ROW_NUMBER' não é suportada com procedimentos armazenados compilados nativamente.
Mensagem 12310, Nível 16, Estado 36, Procedimento GroupedMedian_2005_3
Common Table Expressions (CTE) são não suportado com armazenamento compilado nativamente procedimentos.
Mensagem 12311, Nível 16, Estado 37, Procedimento GroupedMedian_2005_3
Subconsultas (consultas aninhadas dentro de outra consulta) não são suportadas com procedimentos armazenados compilados nativamente.
Mensagem 10794, Nível 16, Estado 91 , Procedimento GroupedMedian_2005_3
A função agregada 'ROW_NUMBER' não é suportada com procedimentos armazenados compilados nativamente.
Mensagem 10794, Nível 16, Estado 53, Procedimento GroupedMedian_2005_3
O operador 'APPLY' não é suportado com procedimentos armazenados compilados nativamente.
Mensagem 12311, Nível 16, Estado 37, Procedimento GroupedMedian_2005_4
Subconsultas (consultas aninhadas dentro de outra consulta) não são suportadas com procedimentos armazenados compilados nativamente.
Mensagem 10794, Nível 16, Estado 91, Procedimento GroupedMedian_2005_4
A função agregada 'ROW_NUMBER' não é suportada com procedimentos armazenados compilados nativamente.
Mensagem 10794, Nível 16, Estado 56, Procedimento GroupedMedian_2005_4
O operador 'IN' não é suportado com armazenamento compilado nativamente ed.
Mensagem 12311, Nível 16, Estado 37, Procedimento GroupedMedian_2012_1
Subconsultas (consultas aninhadas dentro de outra consulta) não são suportadas com procedimentos armazenados compilados nativamente.
Mensagem 10794, Nível 16, Estado 38, Procedimento GroupedMedian_2012_1
O operador 'OFFSET' não é suportado com procedimentos armazenados compilados nativamente.
Mensagem 10794, Nível 16, Estado 53, Procedimento GroupedMedian_2012_1
O operador 'APPLY' não é suportado com procedimentos armazenados compilados nativamente.
Mensagem 12311, Nível 16, Estado 37, Procedimento GroupedMedian_2012_2
Subconsultas (consultas aninhadas dentro de outra consulta) não são suportadas com procedimentos armazenados compilados nativamente.
Msg 10794, Level 16, State 90, Procedure GroupedMedian_2012_2
A função agregada 'PERCENTILE_CONT' não é suportada com procedimentos armazenados compilados nativamente.
Conforme escrito atualmente, nenhuma dessas consultas pode ser portada para um procedimento armazenado compilado nativamente. Talvez algo para olhar para outro post de acompanhamento.
Conclusão
Descartando os resultados do Hekaton e quando um índice de suporte estiver presente, a consulta de Peter Larsson ("2012+ 1") usandoOFFSET/FETCH
saiu como o vencedor de longe nestes testes. Embora um pouco mais complexo do que a consulta equivalente nos testes não particionados, isso correspondeu aos resultados que observei da última vez.
Nesses mesmos casos, os 2.000MIN/MAX
abordagem ePERCENTILE_CONT()
de 2012 saíram como cães reais; novamente, assim como meus testes anteriores contra o caso mais simples.
Se você ainda não estiver no SQL Server 2012, sua próxima melhor opção será "2005+ 3" (se você tiver um índice de suporte) ou "2005+ 2" se estiver lidando com um heap. Desculpe, eu tive que criar um novo esquema de nomenclatura para eles, principalmente para evitar confusão com os métodos no meu post anterior.
Obviamente, esses são meus resultados em relação a um esquema e conjunto de dados muito específicos – como em todas as recomendações, você deve testar essas abordagens em relação ao seu esquema e dados, pois outros fatores podem influenciar resultados diferentes.
Outra observação
Além de ter um desempenho ruim e não ter suporte em procedimentos armazenados compilados nativamente, um outro ponto problemático dePERCENTILE_CONT()
é que ele não pode ser usado em modos de compatibilidade mais antigos. Se você tentar, receberá este erro:
Msg 10762, Level 15, State 1
A função PERCENTILE_CONT não é permitida no modo de compatibilidade atual. Só é permitido no modo 110 ou superior.