Funzione per calcolare la mediana in SQL Server
-
20-09-2019 - |
Domanda
Secondo MSDN, Mediana non è disponibile come funzione aggregata in Transact-SQL.Vorrei però scoprire se è possibile creare questa funzionalità (usando il file Crea aggregato funzione, funzione definita dall'utente o qualche altro metodo).
Quale sarebbe il modo migliore (se possibile) per farlo: consentire il calcolo di un valore mediano (assumendo un tipo di dati numerico) in una query aggregata?
Soluzione
Esistono molti modi per farlo, con prestazioni notevolmente variabili.Ecco una soluzione particolarmente ben ottimizzata, da Mediane, ROW_NUMBER e rendimento.Questa è una soluzione particolarmente ottimale quando si tratta di I/O effettivi generati durante l'esecuzione: sembra più costosa di altre soluzioni, ma in realtà è molto più veloce.
La pagina contiene anche una discussione su altre soluzioni e dettagli sui test delle prestazioni.Da notare l'uso di una colonna univoca come disambiguatore nel caso in cui siano presenti più righe con lo stesso valore della colonna mediana.
Come per tutti gli scenari di prestazioni del database, prova sempre a testare una soluzione con dati reali su hardware reale: non si sa mai quando una modifica all'ottimizzatore di SQL Server o una peculiarità del proprio ambiente renderà più lenta una soluzione normalmente veloce.
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;
Altri suggerimenti
Se stai utilizzando SQL 2005 o versione successiva, questo è un calcolo mediano carino e semplice per una singola colonna in una tabella:
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
In SQL Server 2012 dovresti usare PERCENTILE_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
Guarda anche : http://blog.sqlauthority.com/2011/11/20/sql-server-introduction-to-percentile_cont-analytic-functions-introduced-in-sql-server-2012/
La mia risposta rapida originale era:
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
Questo ti darà la mediana e l'intervallo interquartile in un colpo solo.Se vuoi davvero solo una riga che sia la mediana, decommenta la clausola where.
Quando lo inserisci in un piano di spiegazione, il 60% del lavoro sta nell'ordinare i dati, il che è inevitabile quando si calcolano statistiche dipendenti dalla posizione come questa.
Ho modificato la risposta per seguire l'eccellente suggerimento di Robert Ševčík-Robajz nei commenti seguenti:
;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
Questo dovrebbe calcolare i valori mediani e percentili corretti quando si dispone di un numero pari di elementi di dati.Ancora una volta, rimuovi il commento dalla clausola finale where se desideri solo la mediana e non l'intera distribuzione percentile.
Anche meglio:
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);
Dal maestro stesso, Itzik Ben-Gan!
MS SQL Server 2012 (e versioni successive) dispone della funzione PERCENTILE_DISC che calcola un percentile specifico per i valori ordinati.PERCENTILE_DISC (0,5) calcolerà la mediana - https://msdn.microsoft.com/en-us/library/hh231327.aspx
Semplice, veloce, 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 desideri utilizzare la funzione Crea aggregato in SQL Server, ecco come farlo.Farlo in questo modo ha il vantaggio di poter scrivere query pulite.Tieni presente che questo processo potrebbe essere adattato per calcolare un valore percentile abbastanza facilmente.
Crea un nuovo progetto Visual Studio e imposta il framework di destinazione su .NET 3.5 (questo è per SQL 2008, potrebbe essere diverso in SQL 2012).Quindi crea un file di classe e inserisci il seguente codice o l'equivalente C#:
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
Quindi compilalo e copia il file DLL e PDB sul tuo computer SQL Server ed esegui il seguente comando in 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
Puoi quindi scrivere una query per calcolare la mediana in questo modo:SELEZIONA dbo.Median(Campo) DALLA tabella
Mi sono appena imbattuto in questa pagina mentre cercavo una soluzione basata su set per la mediana.Dopo aver esaminato alcune delle soluzioni qui, ho trovato quanto segue.La speranza è che aiuti/funzioni.
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
La seguente query restituisce il file mediano da un elenco di valori in una colonna.Non può essere utilizzata come o insieme a una funzione aggregata, ma è comunque possibile utilizzarla come sottoquery con una clausola WHERE nella selezione interna.
SQLServer2005+:
SELECT TOP 1 value from
(
SELECT TOP 50 PERCENT value
FROM table_name
ORDER BY value
)for_median
ORDER BY value DESC
Sebbene la soluzione di Justin Grant appaia solida, ho scoperto che quando si dispone di un numero di valori duplicati all'interno di una determinata chiave di partizione, i numeri di riga per i valori duplicati ASC finiscono fuori sequenza, quindi non si allineano correttamente.
Ecco un frammento del mio risultato:
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
Ho usato il codice di Justin come base per questa soluzione.Sebbene non sia così efficiente dato l'uso di più tabelle derivate, risolve il problema di ordinamento delle righe che ho riscontrato.Eventuali miglioramenti sarebbero benvenuti poiché non ho molta esperienza in 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
L'esempio di Justin sopra è molto buono.Ma la necessità della chiave primaria dovrebbe essere affermata molto chiaramente.Ho visto quel codice in natura senza la chiave e i risultati sono pessimi.
La lamentela che ricevo riguardo Percentile_Cont è che non ti fornirà un valore effettivo dal set di dati.Per ottenere una "mediana" che sia un valore effettivo dal set di dati utilizzare Percentile_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
In una UDF, scrivi:
Select Top 1 medianSortColumn from Table T
Where (Select Count(*) from Table
Where MedianSortColumn <
(Select Count(*) From Table) / 2)
Order By medianSortColumn
Vedi altre soluzioni per il calcolo della mediana in SQL qui:"Un modo semplice per calcolare la mediana con MySQL" (le soluzioni sono per lo più indipendenti dal fornitore).
Per una variabile/misura continua 'col1' da 'table1'
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
Volevo trovare una soluzione da solo, ma il mio cervello ha inciampato ed è caduto per strada.IO pensare funziona, ma non chiedermi di spiegartelo domattina.: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]
Funziona con 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
)
Per i principianti come me che stanno imparando le nozioni di base, personalmente trovo questo esempio più facile da seguire, poiché è più facile capire esattamente cosa sta succedendo e da dove provengono i valori mediani...
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
Tuttavia sono assolutamente stupefatto da alcuni dei codici sopra indicati!!!
Questa è la risposta più semplice che potrei trovare.Ha funzionato bene con i miei dati.Se vuoi escludere determinati valori aggiungi semplicemente una clausola where alla selezione 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
La seguente soluzione funziona con questi presupposti:
- Nessun valore duplicato
- Nessun NULL
Codice:
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
Provo con diverse alternative, ma poiché i miei record di dati hanno valori ripetuti, le versioni ROW_NUMBER sembrano non essere una scelta per me.Quindi ecco la query che ho usato (una versione con 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;
Basandosi sulla risposta di Jeff Atwood sopra, eccolo con GROUP BY e una sottoquery correlata per ottenere la mediana per ciascun gruppo.
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
Spesso potrebbe essere necessario calcolare la mediana non solo per l'intera tabella, ma per gli aggregati rispetto ad alcuni ID.In altre parole, calcola la mediana per ogni ID nella nostra tabella, dove ogni ID ha molti record.(basato sulla soluzione modificata da @gdoron:buone prestazioni e funziona con molti 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;
Spero che sia d'aiuto.
Per la tua domanda, Jeff Atwood aveva già fornito la soluzione semplice ed efficace.Ma se stai cercando un approccio alternativo per calcolare la mediana, il codice SQL di seguito ti aiuterà.
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 stai cercando di calcolare la mediana in MySQL, questo collegamento github sarà utile.
Questa è la soluzione ottimale per trovare le mediane che mi viene in mente.I nomi nell'esempio si basano sull'esempio di Justin.Assicurati di avere un indice per la tabella Sales.SalesOrderHeader esiste con le colonne dell'indice CustomerId e TotalDue in quell'ordine.
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
AGGIORNAMENTO
Non ero sicuro di quale metodo avesse le prestazioni migliori, quindi ho fatto un confronto tra il mio metodo Justin Grants e Jeff Atwoods eseguendo query basate su tutti e tre i metodi in un batch e il costo batch di ciascuna query era:
Senza indice:
- Mio 30%
- Justin concede il 13%
- Jeff Atwoods 58%
E con indice
- Il mio 3%.
- Justin concede il 10%
- Jeff Atwood 87%
Ho provato a vedere quanto bene le query si ridimensionano se si dispone di un indice creando più dati da circa 14.000 righe di un fattore da 2 fino a 512, il che significa alla fine circa 7,2 milioni di righe.Nota: mi sono assicurato che il campo CustomeId fosse univoco ogni volta che ho eseguito una singola copia, quindi la proporzione delle righe rispetto all'istanza univoca di CustomerId è stata mantenuta costante.Mentre lo facevo ho eseguito delle esecuzioni in cui ho successivamente ricostruito l'indice e ho notato che i risultati si stabilizzavano attorno a un fattore di 128 con i dati che avevo su questi valori:
- Il mio 3%.
- Justin concede il 5%
- Jeff Atwoods 92%
Mi chiedevo come le prestazioni avrebbero potuto essere influenzate dal ridimensionamento del numero di righe mantenendo costante CustomerId univoco, quindi ho impostato un nuovo test in cui ho fatto proprio questo.Ora invece di stabilizzarsi, il rapporto dei costi batch continuava a divergere, inoltre invece di circa 20 righe per CustomerId in media avevo alla fine circa 10000 righe per tale ID univoco.I numeri dove:
- Il mio 4%
- Justin 60%
- Jeff 35%
Mi sono assicurato di aver implementato correttamente ciascun metodo confrontando i risultati.La mia conclusione è che il metodo che ho utilizzato è generalmente più veloce finché esiste l'indice.Ho notato inoltre che questo metodo è quello consigliato per questo particolare problema in questo articolo https://www.microsoftpressstore.com/articles/article.aspx?p=2314819&seqNum=5
Un modo per migliorare ulteriormente le prestazioni delle chiamate successive a questa query è rendere persistenti le informazioni sul conteggio in una tabella ausiliaria.Potresti anche mantenerlo disponendo di un trigger che si aggiorna e conserva le informazioni relative al conteggio delle righe SalesOrderHeader dipendenti da CustomerId, ovviamente puoi quindi memorizzare semplicemente anche la mediana.
Per set di dati su larga scala, puoi provare questo GIST:
https://gist.github.com/chrisknoll/1b38761ce8c5016ec5b2
Funziona aggregando i valori distinti che potresti trovare nel tuo set (come età, anno di nascita, ecc.) e utilizza le funzioni della finestra SQL per individuare qualsiasi posizione percentile specificata nella query.
Risultato mediano
Questo è il metodo più semplice per trovare la mediana di un attributo.
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)