Função para calcular mediana no SQL Server
-
20-09-2019 - |
Pergunta
De acordo com MSDN, Median não está disponível como uma função agregada no Transact-SQL.Porém, gostaria de saber se é possível criar esta funcionalidade (usando o Criar agregado função, função definida pelo usuário ou algum outro método).
Qual seria a melhor maneira (se possível) de fazer isso - permitir o cálculo de um valor mediano (assumindo um tipo de dados numérico) em uma consulta agregada?
Solução
Existem muitas maneiras de fazer isso, com desempenho dramaticamente variável. Aqui está uma solução particularmente bem otimizada, de Medianas, row_numbers e desempenho. Esta é uma solução particularmente ótima quando se trata de E/SAs reais geradas durante a execução - parece mais caro do que outras soluções, mas na verdade é muito mais rápido.
Essa página também contém uma discussão sobre outras soluções e detalhes de teste de desempenho. Observe o uso de uma coluna exclusiva como um desambiguador, caso haja várias linhas com o mesmo valor da coluna mediana.
Como em todos os cenários de desempenho do banco de dados, sempre tente testar uma solução com dados reais sobre hardware real-você nunca sabe quando uma alteração no otimizador do SQL Server ou uma peculiaridade em seu ambiente tornará uma solução normalmente lenta.
SELECT
CustomerId,
AVG(TotalDue)
FROM
(
SELECT
CustomerId,
TotalDue,
-- SalesOrderId in the ORDER BY is a disambiguator to break ties
ROW_NUMBER() OVER (
PARTITION BY CustomerId
ORDER BY TotalDue ASC, SalesOrderId ASC) AS RowAsc,
ROW_NUMBER() OVER (
PARTITION BY CustomerId
ORDER BY TotalDue DESC, SalesOrderId DESC) AS RowDesc
FROM Sales.SalesOrderHeader SOH
) x
WHERE
RowAsc IN (RowDesc, RowDesc - 1, RowDesc + 1)
GROUP BY CustomerId
ORDER BY CustomerId;
Outras dicas
Se você estiver usando o SQL 2005 ou melhor, este é um cálculo mediano agradável e simples para uma única coluna em uma tabela:
SELECT
(
(SELECT MAX(Score) FROM
(SELECT TOP 50 PERCENT Score FROM Posts ORDER BY Score) AS BottomHalf)
+
(SELECT MIN(Score) FROM
(SELECT TOP 50 PERCENT Score FROM Posts ORDER BY Score DESC) AS TopHalf)
) / 2 AS Median
No SQL Server 2012, você deve usar Percentil_cont:
SELECT SalesOrderID, OrderQty,
PERCENTILE_CONT(0.5)
WITHIN GROUP (ORDER BY OrderQty)
OVER (PARTITION BY SalesOrderID) AS MedianCont
FROM Sales.SalesOrderDetail
WHERE SalesOrderID IN (43670, 43669, 43667, 43663)
ORDER BY SalesOrderID DESC
Minha resposta rápida original foi:
select max(my_column) as [my_column], quartile
from (select my_column, ntile(4) over (order by my_column) as [quartile]
from my_table) i
--where quartile = 2
group by quartile
Isso lhe dará a mediana e o intervalo interquartil de uma só vez.Se você realmente deseja apenas uma linha que seja a mediana, remova o comentário da cláusula where.
Quando você coloca isso em um plano de explicação, 60% do trabalho é classificar os dados, o que é inevitável ao calcular estatísticas dependentes de posição como esta.
Alterei a resposta para seguir a excelente sugestão de Robert Ševčík-Robajz nos comentários abaixo:
;with PartitionedData as
(select my_column, ntile(10) over (order by my_column) as [percentile]
from my_table),
MinimaAndMaxima as
(select min(my_column) as [low], max(my_column) as [high], percentile
from PartitionedData
group by percentile)
select
case
when b.percentile = 10 then cast(b.high as decimal(18,2))
else cast((a.low + b.high) as decimal(18,2)) / 2
end as [value], --b.high, a.low,
b.percentile
from MinimaAndMaxima a
join MinimaAndMaxima b on (a.percentile -1 = b.percentile) or (a.percentile = 10 and b.percentile = 10)
--where b.percentile = 5
Isso deve calcular os valores corretos de mediana e percentil quando você tiver um número par de itens de dados.Novamente, remova o comentário da cláusula where final se desejar apenas a mediana e não toda a distribuição percentil.
Melhor ainda:
SELECT @Median = AVG(1.0 * val)
FROM
(
SELECT o.val, rn = ROW_NUMBER() OVER (ORDER BY o.val), c.c
FROM dbo.EvenRows AS o
CROSS JOIN (SELECT c = COUNT(*) FROM dbo.EvenRows) AS c
) AS x
WHERE rn IN ((c + 1)/2, (c + 2)/2);
Do próprio mestre, Itzik Ben-Gan!
O MS SQL Server 2012 (e posterior) possui a função percentil_disc que calcula um percentil específico para valores classificados. Percentil_disc (0,5) calculará a mediana - https://msdn.microsoft.com/en-us/library/hh231327.aspx
Simples, rápido, preciso
SELECT x.Amount
FROM (SELECT amount,
Count(1) OVER (partition BY 'A') AS TotalRows,
Row_number() OVER (ORDER BY Amount ASC) AS AmountOrder
FROM facttransaction ft) x
WHERE x.AmountOrder = Round(x.TotalRows / 2.0, 0)
Se você deseja usar a função Create Agregate no SQL Server, é assim que fazê -lo. Fazer dessa maneira tem o benefício de poder escrever consultas limpas. Observe que esse processo pode ser adaptado para calcular um valor percentual com bastante facilidade.
Crie um novo projeto do Visual Studio e defina a estrutura de destino para .NET 3.5 (isso é para o SQL 2008, pode ser diferente no SQL 2012). Em seguida, crie um arquivo de classe e coloque o código a seguir, ou C# equivalente:
Imports Microsoft.SqlServer.Server
Imports System.Data.SqlTypes
Imports System.IO
<Serializable>
<SqlUserDefinedAggregate(Format.UserDefined, IsInvariantToNulls:=True, IsInvariantToDuplicates:=False, _
IsInvariantToOrder:=True, MaxByteSize:=-1, IsNullIfEmpty:=True)>
Public Class Median
Implements IBinarySerialize
Private _items As List(Of Decimal)
Public Sub Init()
_items = New List(Of Decimal)()
End Sub
Public Sub Accumulate(value As SqlDecimal)
If Not value.IsNull Then
_items.Add(value.Value)
End If
End Sub
Public Sub Merge(other As Median)
If other._items IsNot Nothing Then
_items.AddRange(other._items)
End If
End Sub
Public Function Terminate() As SqlDecimal
If _items.Count <> 0 Then
Dim result As Decimal
_items = _items.OrderBy(Function(i) i).ToList()
If _items.Count Mod 2 = 0 Then
result = ((_items((_items.Count / 2) - 1)) + (_items(_items.Count / 2))) / 2@
Else
result = _items((_items.Count - 1) / 2)
End If
Return New SqlDecimal(result)
Else
Return New SqlDecimal()
End If
End Function
Public Sub Read(r As BinaryReader) Implements IBinarySerialize.Read
'deserialize it from a string
Dim list = r.ReadString()
_items = New List(Of Decimal)
For Each value In list.Split(","c)
Dim number As Decimal
If Decimal.TryParse(value, number) Then
_items.Add(number)
End If
Next
End Sub
Public Sub Write(w As BinaryWriter) Implements IBinarySerialize.Write
'serialize the list to a string
Dim list = ""
For Each item In _items
If list <> "" Then
list += ","
End If
list += item.ToString()
Next
w.Write(list)
End Sub
End Class
Em seguida, compile -o e copie o arquivo DLL e PDB para sua máquina SQL Server e execute o seguinte comando no SQL Server:
CREATE ASSEMBLY CustomAggregate FROM '{path to your DLL}'
WITH PERMISSION_SET=SAFE;
GO
CREATE AGGREGATE Median(@value decimal(9, 3))
RETURNS decimal(9, 3)
EXTERNAL NAME [CustomAggregate].[{namespace of your DLL}.Median];
GO
Você pode escrever uma consulta para calcular a mediana como esta: selecione dbo.median (campo) da tabela
Acabei de me deparar com esta página enquanto procurava uma solução baseada em conjunto para a mediana. Depois de olhar para algumas das soluções aqui, criei o seguinte. A esperança é ajuda/obras.
DECLARE @test TABLE(
i int identity(1,1),
id int,
score float
)
INSERT INTO @test (id,score) VALUES (1,10)
INSERT INTO @test (id,score) VALUES (1,11)
INSERT INTO @test (id,score) VALUES (1,15)
INSERT INTO @test (id,score) VALUES (1,19)
INSERT INTO @test (id,score) VALUES (1,20)
INSERT INTO @test (id,score) VALUES (2,20)
INSERT INTO @test (id,score) VALUES (2,21)
INSERT INTO @test (id,score) VALUES (2,25)
INSERT INTO @test (id,score) VALUES (2,29)
INSERT INTO @test (id,score) VALUES (2,30)
INSERT INTO @test (id,score) VALUES (3,20)
INSERT INTO @test (id,score) VALUES (3,21)
INSERT INTO @test (id,score) VALUES (3,25)
INSERT INTO @test (id,score) VALUES (3,29)
DECLARE @counts TABLE(
id int,
cnt int
)
INSERT INTO @counts (
id,
cnt
)
SELECT
id,
COUNT(*)
FROM
@test
GROUP BY
id
SELECT
drv.id,
drv.start,
AVG(t.score)
FROM
(
SELECT
MIN(t.i)-1 AS start,
t.id
FROM
@test t
GROUP BY
t.id
) drv
INNER JOIN @test t ON drv.id = t.id
INNER JOIN @counts c ON t.id = c.id
WHERE
t.i = ((c.cnt+1)/2)+drv.start
OR (
t.i = (((c.cnt+1)%2) * ((c.cnt+2)/2))+drv.start
AND ((c.cnt+1)%2) * ((c.cnt+2)/2) <> 0
)
GROUP BY
drv.id,
drv.start
A consulta a seguir retorna o mediana de uma lista de valores em uma coluna. Ele não pode ser usado como ou junto com uma função agregada, mas você ainda pode usá-lo como uma sub-quadra com uma cláusula onde na seleção interna.
SQL Server 2005+:
SELECT TOP 1 value from
(
SELECT TOP 50 PERCENT value
FROM table_name
ORDER BY value
)for_median
ORDER BY value DESC
Embora a solução de Justin Grant pareça sólida, descobri que, quando você tem vários valores duplicados em uma determinada chave de partição, os números da linha para os valores duplicados ASC acabam fora da sequência, para que eles não se alinhem adequadamente.
Aqui está um fragmento do meu resultado:
KEY VALUE ROWA ROWD
13 2 22 182
13 1 6 183
13 1 7 184
13 1 8 185
13 1 9 186
13 1 10 187
13 1 11 188
13 1 12 189
13 0 1 190
13 0 2 191
13 0 3 192
13 0 4 193
13 0 5 194
Eu usei o código de Justin como base para esta solução. Embora não seja tão eficiente, dado o uso de várias tabelas derivadas, ele resolve o problema de pedidos de linha que encontrei. Quaisquer melhorias seriam bem-vindas, pois não sou tão experiente no T-SQL.
SELECT PKEY, cast(AVG(VALUE)as decimal(5,2)) as MEDIANVALUE
FROM
(
SELECT PKEY,VALUE,ROWA,ROWD,
'FLAG' = (CASE WHEN ROWA IN (ROWD,ROWD-1,ROWD+1) THEN 1 ELSE 0 END)
FROM
(
SELECT
PKEY,
cast(VALUE as decimal(5,2)) as VALUE,
ROWA,
ROW_NUMBER() OVER (PARTITION BY PKEY ORDER BY ROWA DESC) as ROWD
FROM
(
SELECT
PKEY,
VALUE,
ROW_NUMBER() OVER (PARTITION BY PKEY ORDER BY VALUE ASC,PKEY ASC ) as ROWA
FROM [MTEST]
)T1
)T2
)T3
WHERE FLAG = '1'
GROUP BY PKEY
ORDER BY PKEY
O exemplo de Justin acima é muito bom. Mas essa necessidade principal principal deve ser declarada com muita clareza. Eu vi esse código na natureza sem a chave e os resultados são ruins.
A reclamação que recebo sobre o percentil_cont é que ela não fornece um valor real do conjunto de dados. Para chegar a uma "mediana", que é um valor real do conjunto de dados, use percentil_disc.
SELECT SalesOrderID, OrderQty,
PERCENTILE_DISC(0.5)
WITHIN GROUP (ORDER BY OrderQty)
OVER (PARTITION BY SalesOrderID) AS MedianCont
FROM Sales.SalesOrderDetail
WHERE SalesOrderID IN (43670, 43669, 43667, 43663)
ORDER BY SalesOrderID DESC
Em um UDF, escreva:
Select Top 1 medianSortColumn from Table T
Where (Select Count(*) from Table
Where MedianSortColumn <
(Select Count(*) From Table) / 2)
Order By medianSortColumn
Veja outras soluções para o cálculo mediano no SQL aqui: "Maneira simples de calcular a mediana com o mysql"(As soluções são principalmente independentes do fornecedor).
Para uma variável contínua/medida 'col1' da 'Tabela1'
select col1
from
(select top 50 percent col1,
ROW_NUMBER() OVER(ORDER BY col1 ASC) AS Rowa,
ROW_NUMBER() OVER(ORDER BY col1 DESC) AS Rowd
from table1 ) tmp
where tmp.Rowa = tmp.Rowd
Eu queria encontrar uma solução sozinha, mas meu cérebro tropeçou e caiu no caminho. EU acho Funciona, mas não me peça para explicar de manhã. : P
DECLARE @table AS TABLE
(
Number int not null
);
insert into @table select 2;
insert into @table select 4;
insert into @table select 9;
insert into @table select 15;
insert into @table select 22;
insert into @table select 26;
insert into @table select 37;
insert into @table select 49;
DECLARE @Count AS INT
SELECT @Count = COUNT(*) FROM @table;
WITH MyResults(RowNo, Number) AS
(
SELECT RowNo, Number FROM
(SELECT ROW_NUMBER() OVER (ORDER BY Number) AS RowNo, Number FROM @table) AS Foo
)
SELECT AVG(Number) FROM MyResults WHERE RowNo = (@Count+1)/2 OR RowNo = ((@Count+1)%2) * ((@Count+2)/2)
--Create Temp Table to Store Results in
DECLARE @results AS TABLE
(
[Month] datetime not null
,[Median] int not null
);
--This variable will determine the date
DECLARE @IntDate as int
set @IntDate = -13
WHILE (@IntDate < 0)
BEGIN
--Create Temp Table
DECLARE @table AS TABLE
(
[Rank] int not null
,[Days Open] int not null
);
--Insert records into Temp Table
insert into @table
SELECT
rank() OVER (ORDER BY DATEADD(mm, DATEDIFF(mm, 0, DATEADD(ss, SVR.close_date, '1970')), 0), DATEDIFF(day,DATEADD(ss, SVR.open_date, '1970'),DATEADD(ss, SVR.close_date, '1970')),[SVR].[ref_num]) as [Rank]
,DATEDIFF(day,DATEADD(ss, SVR.open_date, '1970'),DATEADD(ss, SVR.close_date, '1970')) as [Days Open]
FROM
mdbrpt.dbo.View_Request SVR
LEFT OUTER JOIN dbo.dtv_apps_systems vapp
on SVR.category = vapp.persid
LEFT OUTER JOIN dbo.prob_ctg pctg
on SVR.category = pctg.persid
Left Outer Join [mdbrpt].[dbo].[rootcause] as [Root Cause]
on [SVR].[rootcause]=[Root Cause].[id]
Left Outer Join [mdbrpt].[dbo].[cr_stat] as [Status]
on [SVR].[status]=[Status].[code]
LEFT OUTER JOIN [mdbrpt].[dbo].[net_res] as [net]
on [net].[id]=SVR.[affected_rc]
WHERE
SVR.Type IN ('P')
AND
SVR.close_date IS NOT NULL
AND
[Status].[SYM] = 'Closed'
AND
SVR.parent is null
AND
[Root Cause].[sym] in ( 'RC - Application','RC - Hardware', 'RC - Operational', 'RC - Unknown')
AND
(
[vapp].[appl_name] in ('3PI','Billing Rpts/Files','Collabrent','Reports','STMS','STMS 2','Telco','Comergent','OOM','C3-BAU','C3-DD','DIRECTV','DIRECTV Sales','DIRECTV Self Care','Dealer Website','EI Servlet','Enterprise Integration','ET','ICAN','ODS','SB-SCM','SeeBeyond','Digital Dashboard','IVR','OMS','Order Services','Retail Services','OSCAR','SAP','CTI','RIO','RIO Call Center','RIO Field Services','FSS-RIO3','TAOS','TCS')
OR
pctg.sym in ('Systems.Release Health Dashboard.Problem','DTV QA Test.Enterprise Release.Deferred Defect Log')
AND
[Net].[nr_desc] in ('3PI','Billing Rpts/Files','Collabrent','Reports','STMS','STMS 2','Telco','Comergent','OOM','C3-BAU','C3-DD','DIRECTV','DIRECTV Sales','DIRECTV Self Care','Dealer Website','EI Servlet','Enterprise Integration','ET','ICAN','ODS','SB-SCM','SeeBeyond','Digital Dashboard','IVR','OMS','Order Services','Retail Services','OSCAR','SAP','CTI','RIO','RIO Call Center','RIO Field Services','FSS-RIO3','TAOS','TCS')
)
AND
DATEADD(mm, DATEDIFF(mm, 0, DATEADD(ss, SVR.close_date, '1970')), 0) = DATEADD(mm, DATEDIFF(mm,0,DATEADD(mm,@IntDate,getdate())), 0)
ORDER BY [Days Open]
DECLARE @Count AS INT
SELECT @Count = COUNT(*) FROM @table;
WITH MyResults(RowNo, [Days Open]) AS
(
SELECT RowNo, [Days Open] FROM
(SELECT ROW_NUMBER() OVER (ORDER BY [Days Open]) AS RowNo, [Days Open] FROM @table) AS Foo
)
insert into @results
SELECT
DATEADD(mm, DATEDIFF(mm,0,DATEADD(mm,@IntDate,getdate())), 0) as [Month]
,AVG([Days Open])as [Median] FROM MyResults WHERE RowNo = (@Count+1)/2 OR RowNo = ((@Count+1)%2) * ((@Count+2)/2)
set @IntDate = @IntDate+1
DELETE FROM @table
END
select *
from @results
order by [Month]
Isso funciona com o SQL 2000:
DECLARE @testTable TABLE
(
VALUE INT
)
--INSERT INTO @testTable -- Even Test
--SELECT 3 UNION ALL
--SELECT 5 UNION ALL
--SELECT 7 UNION ALL
--SELECT 12 UNION ALL
--SELECT 13 UNION ALL
--SELECT 14 UNION ALL
--SELECT 21 UNION ALL
--SELECT 23 UNION ALL
--SELECT 23 UNION ALL
--SELECT 23 UNION ALL
--SELECT 23 UNION ALL
--SELECT 29 UNION ALL
--SELECT 40 UNION ALL
--SELECT 56
--
--INSERT INTO @testTable -- Odd Test
--SELECT 3 UNION ALL
--SELECT 5 UNION ALL
--SELECT 7 UNION ALL
--SELECT 12 UNION ALL
--SELECT 13 UNION ALL
--SELECT 14 UNION ALL
--SELECT 21 UNION ALL
--SELECT 23 UNION ALL
--SELECT 23 UNION ALL
--SELECT 23 UNION ALL
--SELECT 23 UNION ALL
--SELECT 29 UNION ALL
--SELECT 39 UNION ALL
--SELECT 40 UNION ALL
--SELECT 56
DECLARE @RowAsc TABLE
(
ID INT IDENTITY,
Amount INT
)
INSERT INTO @RowAsc
SELECT VALUE
FROM @testTable
ORDER BY VALUE ASC
SELECT AVG(amount)
FROM @RowAsc ra
WHERE ra.id IN
(
SELECT ID
FROM @RowAsc
WHERE ra.id -
(
SELECT MAX(id) / 2.0
FROM @RowAsc
) BETWEEN 0 AND 1
)
Para iniciantes como eu que estão aprendendo o básico, eu pessoalmente acho este exemplo mais fácil de seguir, pois é mais fácil entender exatamente o que está acontecendo e de onde vêm os valores medianos ...
select
( max(a.[Value1]) + min(a.[Value1]) ) / 2 as [Median Value1]
,( max(a.[Value2]) + min(a.[Value2]) ) / 2 as [Median Value2]
from (select
datediff(dd,startdate,enddate) as [Value1]
,xxxxxxxxxxxxxx as [Value2]
from dbo.table1
)a
Em absoluta admiração de alguns dos códigos acima, no entanto, !!!
Esta é uma resposta tão simples quanto eu poderia inventar. Funcionou bem com meus dados. Se você deseja excluir certos valores, basta adicionar uma cláusula WHERE à seleção interna.
SELECT TOP 1
ValueField AS MedianValue
FROM
(SELECT TOP(SELECT COUNT(1)/2 FROM tTABLE)
ValueField
FROM
tTABLE
ORDER BY
ValueField) A
ORDER BY
ValueField DESC
A solução a seguir funciona sob essas suposições:
- Sem valores duplicados
- Sem nulos
Código:
IF OBJECT_ID('dbo.R', 'U') IS NOT NULL
DROP TABLE dbo.R
CREATE TABLE R (
A FLOAT NOT NULL);
INSERT INTO R VALUES (1);
INSERT INTO R VALUES (2);
INSERT INTO R VALUES (3);
INSERT INTO R VALUES (4);
INSERT INTO R VALUES (5);
INSERT INTO R VALUES (6);
-- Returns Median(R)
select SUM(A) / CAST(COUNT(A) AS FLOAT)
from R R1
where ((select count(A) from R R2 where R1.A > R2.A) =
(select count(A) from R R2 where R1.A < R2.A)) OR
((select count(A) from R R2 where R1.A > R2.A) + 1 =
(select count(A) from R R2 where R1.A < R2.A)) OR
((select count(A) from R R2 where R1.A > R2.A) =
(select count(A) from R R2 where R1.A < R2.A) + 1) ;
DECLARE @Obs int
DECLARE @RowAsc table
(
ID INT IDENTITY,
Observation FLOAT
)
INSERT INTO @RowAsc
SELECT Observations FROM MyTable
ORDER BY 1
SELECT @Obs=COUNT(*)/2 FROM @RowAsc
SELECT Observation AS Median FROM @RowAsc WHERE ID=@Obs
Eu tento com várias alternativas, mas, devido aos meus registros de dados, têm valores repetidos, as versões row_number parecem não ser uma escolha para mim. Então, aqui a consulta que usei (uma versão com ntile):
SELECT distinct
CustomerId,
(
MAX(CASE WHEN Percent50_Asc=1 THEN TotalDue END) OVER (PARTITION BY CustomerId) +
MIN(CASE WHEN Percent50_desc=1 THEN TotalDue END) OVER (PARTITION BY CustomerId)
)/2 MEDIAN
FROM
(
SELECT
CustomerId,
TotalDue,
NTILE(2) OVER (
PARTITION BY CustomerId
ORDER BY TotalDue ASC) AS Percent50_Asc,
NTILE(2) OVER (
PARTITION BY CustomerId
ORDER BY TotalDue DESC) AS Percent50_desc
FROM Sales.SalesOrderHeader SOH
) x
ORDER BY CustomerId;
Com base na resposta de Jeff Atwood acima, aqui está com o grupo e uma subconsulta correlacionada para obter a mediana para cada grupo.
SELECT TestID,
(
(SELECT MAX(Score) FROM
(SELECT TOP 50 PERCENT Score FROM Posts WHERE TestID = Posts_parent.TestID ORDER BY Score) AS BottomHalf)
+
(SELECT MIN(Score) FROM
(SELECT TOP 50 PERCENT Score FROM Posts WHERE TestID = Posts_parent.TestID ORDER BY Score DESC) AS TopHalf)
) / 2 AS MedianScore,
AVG(Score) AS AvgScore, MIN(Score) AS MinScore, MAX(Score) AS MaxScore
FROM Posts_parent
GROUP BY Posts_parent.TestID
Freqüentemente, podemos precisar calcular a mediana não apenas para toda a tabela, mas para agregados em relação a algum ID. Em outras palavras, calcule a mediana para cada ID em nossa tabela, onde cada ID tem muitos registros. (Com base na solução editada por @gdoron: bom desempenho e funciona em muitos SQL)
SELECT our_id, AVG(1.0 * our_val) as Median
FROM
( SELECT our_id, our_val,
COUNT(*) OVER (PARTITION BY our_id) AS cnt,
ROW_NUMBER() OVER (PARTITION BY our_id ORDER BY our_val) AS rnk
FROM our_table
) AS x
WHERE rnk IN ((cnt + 1)/2, (cnt + 2)/2) GROUP BY our_id;
Espero que ajude.
Para sua pergunta, Jeff Atwood já havia dado a solução simples e eficaz. Mas, se você estiver procurando uma abordagem alternativa para calcular a mediana, abaixo do código SQL o ajudará.
create table employees(salary int);
insert into employees values(8); insert into employees values(23); insert into employees values(45); insert into employees values(123); insert into employees values(93); insert into employees values(2342); insert into employees values(2238);
select * from employees;
declare @odd_even int; declare @cnt int; declare @middle_no int;
set @cnt=(select count(*) from employees); set @middle_no=(@cnt/2)+1; select @odd_even=case when (@cnt%2=0) THEN -1 ELse 0 END ;
select AVG(tbl.salary) from (select salary,ROW_NUMBER() over (order by salary) as rno from employees group by salary) tbl where tbl.rno=@middle_no or tbl.rno=@middle_no+@odd_even;
Se você deseja calcular a mediana em MySQL, este link do github será útil.
Esta é a solução mais ideal para encontrar medianas que eu consigo pensar. Os nomes no exemplo são baseados no exemplo de Justin. Certifique -se de que exista um índice para a tabela Sales.SalesOrderHeader com as colunas de índice CustomerId e TotalDue nesse pedido.
SELECT
sohCount.CustomerId,
AVG(sohMid.TotalDue) as TotalDueMedian
FROM
(SELECT
soh.CustomerId,
COUNT(*) as NumberOfRows
FROM
Sales.SalesOrderHeader soh
GROUP BY soh.CustomerId) As sohCount
CROSS APPLY
(Select
soh.TotalDue
FROM
Sales.SalesOrderHeader soh
WHERE soh.CustomerId = sohCount.CustomerId
ORDER BY soh.TotalDue
OFFSET sohCount.NumberOfRows / 2 - ((sohCount.NumberOfRows + 1) % 2) ROWS
FETCH NEXT 1 + ((sohCount.NumberOfRows + 1) % 2) ROWS ONLY
) As sohMid
GROUP BY sohCount.CustomerId
ATUALIZAR
Eu estava um pouco inseguro sobre qual método tem o melhor desempenho, então fiz uma comparação entre meu método Justin Grants e Jeff Atwoods executando a consulta com base nos três métodos em um lote e o custo do lote de cada consulta foram:
Sem índice:
- Meu 30%
- Justin concede 13%
- Jeff Atwoods 58%
E com índice
- Meu 3%.
- Justin concede 10%
- Jeff Atwoods 87%
Tentei ver quão bem a escala de consultas se você tiver índice, criando mais dados de cerca de 14.000 linhas em um fator de 2 a 512, o que significa no final cerca de 7,2 milhões de linhas. Observe que certifiquei -me de que o Customeid Field, onde exclusivo, para cada vez que fazia uma única cópia; portanto, a proporção de linhas em comparação com a instância exclusiva do CustomerID era mantida constante. Enquanto fazia isso, realizei execuções onde reconstruí depois, e notei os resultados estabilizados em cerca de um fator de 128 com os dados que eu tinha com esses valores:
- Meu 3%.
- Justin concede 5%
- Jeff Atwoods 92%
Gostaria de saber como o desempenho poderia ter sido afetado pela escalonamento do número de linhas, mas mantendo o cliente exclusivo constante, então configurei um novo teste em que fiz exatamente isso. Agora, em vez de se estabilizar, a taxa de custo do lote continuava divergindo, também em vez de cerca de 20 linhas por cliente por média, eu tinha no final cerca de 10000 linhas por ID tão exclusivo. Os números onde:
- Meu 4%
- Justins 60%
- Jeffs 35%
Eu certifiquei -me de implementar cada método correto comparando os resultados. Minha conclusão é que o método que usei geralmente é mais rápido, desde que exista índice. Também notei que este método é o que é recomendado para este problema em particular neste artigo https://www.microsoftpressstore.com/articles/article.aspx?p=2314819&seqnum=5
Uma maneira de melhorar ainda mais o desempenho das chamadas subsequentes para essa consulta ainda mais é persistir as informações de contagem em uma tabela auxiliar. Você pode até mantê -lo com um gatilho que atualiza e mantém informações sobre a contagem de linhas de vendas de vendedores dependentes do ClienteID, é claro que você também pode armazenar a mediana.
Para conjuntos de dados em larga escala, você pode experimentar esta essência:
https://gist.github.com/chrisknoll/1b38761ce8c5016ec5b2
Ele funciona agregando os valores distintos que você encontraria em seu conjunto (como idades, ano de nascimento, etc.) e usa funções da janela SQL para localizar qualquer posição percentual que você especificar na consulta.
Descoberta mediana
Este é o método mais simples para encontrar a mediana de um atributo.
Select round(S.salary,4) median from employee S where (select count(salary) from station where salary < S.salary ) = (select count(salary) from station where salary > S.salary)