Fonction calculer Median dans SQL Server
-
20-09-2019 - |
Question
Selon MSDN , médian n'est pas disponible en fonction d'agrégation dans Transact-SQL. Cependant, je voudrais savoir s'il est possible de créer cette fonctionnalité (en utilisant le créer des agrégats fonction , la fonction définie par l'utilisateur, ou une autre méthode).
Quelle serait la meilleure façon (si possible) de le faire - permettre le calcul d'une valeur médiane (en supposant un type de données numériques) dans une requête globale
La solution
Il y a beaucoup de façons de le faire, avec des performances considérablement différents. Voici une solution particulièrement bien optimisé, de médianes, ROW_NUMBERs et la performance . Ceci est une solution particulièrement optimale en matière de E / S réelles générées lors de l'exécution -. Il semble plus coûteux que d'autres solutions, mais il est en réalité beaucoup plus rapide
Cette page contient également une discussion sur d'autres solutions et les détails de tests de performance. Notez l'utilisation d'une colonne unique comme désambiguïsateur au cas où il y a plusieurs lignes avec la même valeur de la colonne médiane.
Comme tous les scénarios de performance de base de données, essayez toujours de tester une solution avec des données réelles sur le matériel réel - vous ne savez jamais quand un changement à l'optimiseur de SQL Server ou une particularité dans votre environnement fera une solution normalement rapide plus lente <. / p>
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;
Autres conseils
Si vous utilisez SQL 2005 ou mieux c'est un joli calcul médian simple ish pour une seule colonne dans une table:
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
Dans SQL Server 2012, vous devez utiliser 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
Ma réponse rapide originale était:
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
Cela vous donnera la gamme médiane et interquartile d'un seul coup. Si vous voulez vraiment qu'une seule ligne qui est la médiane alors décommentez la clause where.
Lorsque vous tenez cela en un expliquer le plan, 60% du travail est le tri des données est inévitable lorsque les statistiques dépendent calcul de position comme celle-ci.
Je l'ai modifié la réponse à suivre l'excellente suggestion de Robert Ševčík-Robajz dans les commentaires ci-dessous:
;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
Cela devrait calculer la médiane correcte et les valeurs percentile lorsque vous avez un nombre pair d'éléments de données. Encore une fois, décommentez la dernière clause where si vous souhaitez que la médiane et non toute la distribution percentile.
Mieux encore:
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);
Du maître lui-même, Itzik Ben-Gan !
MS SQL Server 2012 (et versions ultérieures) a la fonction de PERCENTILE_DISC qui calcule un percentile spécifique pour les valeurs triées. PERCENTILE_DISC (0,5) calcule la valeur médiane - https://msdn.microsoft.com/ fr-fr / bibliothèque / hh231327.aspx
Simple, rapide, précis
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)
Si vous souhaitez utiliser la fonction de création d'agrégat dans SQL Server, voici comment faire. En procédant ainsi, a l'avantage de pouvoir écrire des requêtes propres. Notez que ce processus pourrait être adapté pour calculer une valeur Centile assez facilement.
Créer un nouveau projet Visual Studio et définir le cadre cible .NET 3.5 (ce qui est pour SQL 2008, il peut être différent dans SQL 2012). Ensuite, créez un fichier de classe et de mettre dans le code suivant, ou c # équivalent:
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
Ensuite, compiler et copier la DLL et le fichier PDB sur votre ordinateur SQL Server et exécutez la commande suivante dans 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
Vous pouvez alors écrire une requête pour calculer la médiane comme ceci: SELECT dbo.Median (champ) FROM Table
Je viens suis tombé sur cette page en cherchant une solution à base de jeu à la médiane. Après avoir examiné quelques-unes des solutions ici, je suis venu avec ce qui suit. L'espoir aide / œuvres.
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 requête suivante renvoie le médian à partir d'une liste de valeurs dans une colonne. Il ne peut pas être utilisé comme ou avec une fonction d'agrégation, mais vous pouvez toujours l'utiliser comme une sous-requête avec une clause WHERE dans la sélection intérieure.
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
Bien que la solution de subvention Justin semble solide, j'ai trouvé que lorsque vous avez un certain nombre de valeurs en double dans une clé de partition étant donné les numéros de ligne pour les valeurs en double ASC finissent par sortir de la séquence de sorte qu'ils ne sont pas alignés correctement.
Voici un fragment de mon résultat:
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
J'ai utilisé le code de Justin comme base de cette solution. Bien que pas aussi efficace compte tenu de l'utilisation de plusieurs tables sous-jacentes, il ne résout le problème de commande de la ligne que je rencontrais. Toute amélioration serait bienvenue car je ne suis pas connu dans 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'exemple de Justin ci-dessus est très bon. Mais cette clé primaire besoin convient de préciser très clairement. J'ai vu que le code dans la nature sans la clé et les résultats sont mauvais.
La plainte que je reçois sur le PERCENTILE_CONT est qu'il vous donnera pas une valeur réelle de l'ensemble de données. Pour arriver à une « médiane » qui est une valeur réelle de l'ensemble de données PERCENTILE_DISC utiliser.
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
Dans un UDF, écrire:
Select Top 1 medianSortColumn from Table T
Where (Select Count(*) from Table
Where MedianSortColumn <
(Select Count(*) From Table) / 2)
Order By medianSortColumn
Voir d'autres solutions pour le calcul médian dans SQL ici: « façon simple de calculer la médiane avec MySQL » (les solutions sont pour la plupart fournisseur indépendant).
Pour une variable continue / mesure 'col1' de '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
Je voulais trouver une solution par moi-même, mais mon cerveau trébuché et est tombé sur le chemin. I pense ça marche, mais ne me demandez pas d'expliquer le matin. : 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]
Cela fonctionne avec 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
)
Pour les débutants comme moi qui apprennent les bases, je trouve cet exemple plus facile à suivre, car il est plus facile de comprendre exactement ce qui se passe et où les valeurs médianes sont en provenance de ...
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
Dans la crainte absolue de certains des codes ci-dessus si !!!
Ceci est aussi simple réponse que je pouvais trouver. Bien travaillé avec mes données. Si vous voulez exclure certaines valeurs juste ajouter une clause where à la sélection intérieure.
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 solution suivante fonctionne sous ces hypothèses:
- Aucune valeur en double
- Pas NULLs
Code:
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
J'essaie plusieurs alternatives, mais à cause de mes enregistrements de données a répété les valeurs, les versions ROW_NUMBER semble ne sont pas un choix pour moi. Voici donc la requête je (une version avec 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;
Miser sur la réponse de Jeff Atwood ci-dessus, il est ici avec GROUP BY et une sous-requête corrélée pour obtenir la médiane pour chaque groupe.
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
Souvent, nous devrons peut-être calculer médian non seulement pour la table entière, mais pour les granulats par rapport à une pièce d'identité. En d'autres termes, le calcul médian pour chaque ID dans notre table, où chaque ID a de nombreux enregistrements. (Basé sur la solution éditée par @gdoron: bonnes performances et fonctionne dans de nombreux 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;
it helps.
Pour votre question, Jeff Atwood avait déjà donné la solution simple et efficace. Mais, si vous êtes à la recherche d'une approche alternative pour le calcul de la médiane, au-dessous du code SQL vous aidera.
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;
Si vous cherchez à calculer la médiane dans MySQL, cette github lien sera utile.
Ceci est la solution la plus optimale pour trouver que je peux terre-pleins penser. Les noms dans l'exemple est basé sur exemple Justin. Assurez-vous d'un index pour la table Sales.SalesOrderHeader existe avec des colonnes d'index CustomerId et TotalDue dans cet ordre.
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
UPDATE
J'étais un peu incertain au sujet de la méthode a une meilleure performance, donc je fait une comparaison entre ma méthode subventions Justin et Jeff Atwood par requête en cours d'exécution basé sur les trois méthodes en un seul lot et le coût de traitement par lots de chaque requête sont:
Sans index:
- Mine de 30%
- Subventions Justin 13%
- Jeff Atwoods 58%
Et avec l'index
- Mine de 3%.
- Subventions Justin 10%
- Jeff Atwoods 87%
J'ai essayé de voir comment les requêtes échelle si vous avez l'index en créant davantage de données d'environ 14 000 lignes par un facteur de 2 à 512 ce qui signifie en fin de compte environ 7,2 millions de lignes. Note Je me suis assuré de terrain CustomeId où unique pour chaque fois que je l'ai fait une seule copie, de sorte que la proportion de lignes par rapport à instance unique de CustomerId a été maintenue constante. Pendant que je faisais cela, je courais les exécutions où je Reconstruit l'index après, et j'ai remarqué les résultats stabilisés autour d'un facteur 128 avec les données que je devais ces valeurs:
- Mine de 3%.
- Subventions Justin 5%
- Jeff Atwoods 92%
Je me demandais comment la performance aurait pu être affectée par l'échelle nombre de lignes mais de maintenir constante CustomerId unique, donc je installation d'un nouveau test où je l'ai fait tout cela. Maintenant, au lieu de la stabilisation, le rapport des coûts par lots maintenu divergents, aussi au lieu d'environ 20 lignes par CustomerId par moyen que j'avais à la fin autour de 10000 lignes par exemple Id unique. Les chiffres où:
- Mine de 4%
- Justins 60%
- Jeffs 35%
Je me suis assuré chaque méthode correcte mis en œuvre en comparant les résultats. Ma conclusion est la méthode que j'utilisé est généralement plus rapide aussi longtemps que l'indice existe. A également remarqué que cette méthode est ce qui est recommandé pour ce problème particulier dans cet article https : //www.microsoftpressstore.com/articles/article.aspx p = 2314819 & SEQNUM = 5
Une façon d'améliorer encore les performances des appels ultérieurs à cette question est encore plus pour conserver les informations de comptage dans une table auxiliaire. Vous pouvez même maintenir en ayant un déclencheur qui mise à jour et détient des informations concernant le nombre de lignes SalesOrderHeader dépendant CustomerId, bien sûr, vous pouvez alors simplement stocker la médiane ainsi.
Pour les grands ensembles de données à grande échelle, vous pouvez essayer cette GIST:
https://gist.github.com/chrisknoll/1b38761ce8c5016ec5b2
Il fonctionne en additionnant les valeurs distinctes que vous trouveriez dans votre jeu (comme l'âge ou l'année de naissance, etc.), et utilise des fonctions de fenêtre SQL pour localiser une position percentile spécifiée dans la requête.
médian Finding
Ceci est la méthode la plus simple pour trouver la médiane d'un attribut.
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)