Fonction pour calculer la médiane dans SQL Server

227

Selon MSDN , Median n'est pas disponible en tant que fonction d'agrégation dans Transact-SQL. Cependant, je voudrais savoir s'il est possible de créer cette fonctionnalité (en utilisant la fonction Créer un agrégat , une 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érique) dans une requête agrégée?

Yaakov Ellis
la source

Réponses:

145

MISE À JOUR 2019: Au cours des 10 années écoulées depuis que j'ai écrit cette réponse, plus de solutions ont été découvertes qui pourraient donner de meilleurs résultats. De plus, les versions de SQL Server depuis lors (en particulier SQL 2012) ont introduit de nouvelles fonctionnalités T-SQL qui peuvent être utilisées pour calculer les médianes. Les versions de SQL Server ont également amélioré son optimiseur de requêtes, ce qui peut affecter les performances de diverses solutions médianes. Net-net, mon article d'origine de 2009 est toujours correct, mais il peut y avoir de meilleures solutions pour les applications SQL Server modernes. Jetez un œil à cet article de 2012 qui est une excellente ressource: https://sqlperformance.com/2012/08/t-sql-queries/median

Cet article a constaté que le modèle suivant est beaucoup, beaucoup plus rapide que toutes les autres alternatives, au moins sur le schéma simple qu'ils ont testé. Cette solution était 373 fois plus rapide (!!!) que la PERCENTILE_CONTsolution la plus lente ( ) testée. Notez que cette astuce nécessite deux requêtes distinctes qui peuvent ne pas être pratiques dans tous les cas. Il nécessite également SQL 2012 ou une version ultérieure.

DECLARE @c BIGINT = (SELECT COUNT(*) FROM dbo.EvenRows);

SELECT AVG(1.0 * val)
FROM (
    SELECT val FROM dbo.EvenRows
     ORDER BY val
     OFFSET (@c - 1) / 2 ROWS
     FETCH NEXT 1 + (1 - @c % 2) ROWS ONLY
) AS x;

Bien sûr, juste parce qu'un test sur un schéma en 2012 a donné d'excellents résultats, votre kilométrage peut varier, surtout si vous utilisez SQL Server 2014 ou une version ultérieure. Si la perf est importante pour votre calcul médian, je vous suggère fortement d'essayer et de tester plusieurs des options recommandées dans cet article pour vous assurer que vous avez trouvé la meilleure pour votre schéma.

Je serais également particulièrement prudent en utilisant la fonction (nouvelle dans SQL Server 2012) PERCENTILE_CONTrecommandée dans l'une des autres réponses à cette question, car l'article lié ci-dessus a trouvé que cette fonction intégrée était 373 fois plus lente que la solution la plus rapide. Il est possible que cette disparité se soit améliorée depuis 7 ans, mais personnellement, je n'utiliserais pas cette fonction sur une grande table avant d'avoir vérifié ses performances par rapport à d'autres solutions.

L'ORIGINAL 2009 POST EST CI-DESSOUS:

Il existe de nombreuses façons de le faire, avec des performances très variables. Voici une solution particulièrement bien optimisée, à partir des médianes, des ROW_NUMBERs et des performances . Il s'agit d'une solution particulièrement optimale en ce qui concerne les E / S réelles générées lors de l'exécution - elle semble plus coûteuse que les autres solutions, mais elle est en fait beaucoup plus rapide.

Cette page contient également une discussion sur d'autres solutions et des détails sur les tests de performances. Notez l'utilisation d'une colonne unique comme désambiguïsateur dans le cas où plusieurs lignes ont la même valeur que la colonne médiane.

Comme pour tous les scénarios de performances de base de données, essayez toujours de tester une solution avec des données réelles sur du matériel réel - vous ne savez jamais quand une modification de l'optimiseur de SQL Server ou une particularité de votre environnement rendra une solution normalement rapide plus lente.

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;
Justin Grant
la source
12
Je ne pense pas que cela fonctionne si vous avez des dupes, en particulier beaucoup de dupes, dans vos données. Vous ne pouvez pas garantir que les numéros de ligne seront alignés. Vous pouvez obtenir des réponses vraiment folles pour votre médiane, ou pire encore, aucune médiane.
Jonathan Beerhalter
26
C'est pourquoi avoir un ambiguïté (SalesOrderId dans l'exemple de code ci-dessus) est important, afin que vous puissiez vous assurer que l'ordre des lignes de l'ensemble de résultats est cohérent à la fois en arrière et en avant. Souvent, une clé primaire unique constitue un désambiguïsateur idéal car elle est disponible sans recherche d'index distincte. S'il n'y a pas de colonne de désambiguïsation disponible (par exemple, si la table n'a pas de clé uniquifiante), alors une autre approche doit être utilisée pour calculer la médiane, car comme vous le signalez correctement, si vous ne pouvez pas garantir que les numéros de ligne DESC sont des images miroir de Numéros de ligne ASC, puis les résultats sont imprévisibles.
Justin Grant
4
Merci, lors du basculement des colonnes vers ma base de données, j'ai laissé tomber l'ambiguïté, pensant que ce n'était pas pertinent. Dans ce cas, cette solution fonctionne vraiment très bien.
Jonathan Beerhalter
8
Je suggère d'ajouter un commentaire au code lui-même, décrivant la nécessité du désambiguïsateur.
hoffmanc
4
Impressionnant! depuis longtemps je connais son importance mais maintenant je peux lui donner un nom ... le désambiguïsateur! Merci Justin!
CodeMonkey
204

Si vous utilisez SQL 2005 ou mieux, il s'agit d'un calcul médian simple et simple pour une seule colonne d'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
Jeff Atwood
la source
62
C'est intelligent et relativement simple étant donné qu'il n'existe aucune fonction d'agrégation Median (). Mais comment se fait-il qu'aucune fonction Median () n'existe!? Je suis un peu FLOOR () ed, franchement.
Charlie Kilian
Eh bien, c'est simple et agréable, mais d'habitude, vous avez besoin d'une médiane pour une certaine catégorie de groupe, c'est-à-dire comme select gid, median(score) from T group by gid. Avez-vous besoin d'une sous-requête corrélée pour cela?
TMS
1
... Je veux dire comme dans ce cas (la 2e requête nommée "Utilisateurs avec le score de réponse médian le plus élevé").
TMS
Tomas - avez-vous réussi à résoudre votre problème "par catégorie de groupe"? Comme j'ai le même problème. Merci.
Stu Harper
3
Comment utiliser cette solution avec un GROUP BY?
Przemyslaw Remin,
82

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

Voir également: http://blog.sqlauthority.com/2011/11/20/sql-server-introduction-to-percentile_cont-analytic-functions-introduced-in-sql-server-2012/

Simon_Weaver
la source
12
Cette analyse experte fait un argument convaincant contre les fonctions PERCENTILE en raison de performances médiocres. sqlperformance.com/2012/08/t-sql-queries/median
carl.anderson
4
Vous n'avez pas besoin d'ajouter un DISTINCTou GROUPY BY SalesOrderID? Sinon, vous aurez beaucoup de lignes en double.
Konstantin
1
c'est la réponse. Je ne sais pas pourquoi j'ai dû faire défiler jusqu'ici
FistOfFury
Il existe également une version discrète utilisantPERCENTILE_DISC
johnDanger
en soulignant le point de @ carl.anderson ci-dessus: une solution PERCENTILE_CONT a été mesurée comme étant 373x plus lente (!!!!) par rapport à la solution la plus rapide qu'ils ont testée sur SQL Server 2012 sur leur schéma de test particulier. Lisez l'article que carl a lié pour plus de détails.
Justin Grant
21

Ma réponse rapide d'origine é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 plage médiane et interquartile d'un seul coup. Si vous ne voulez vraiment qu'une seule ligne qui est la médiane, décommentez la clause where.

Lorsque vous collez cela dans un plan d'explication, 60% du travail consiste à trier les données, ce qui est inévitable lors du calcul de statistiques dépendantes de la position comme celle-ci.

J'ai modifié la réponse pour 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 les valeurs médiane et centile correctes lorsque vous avez un nombre pair d'éléments de données. Encore une fois, décommentez la clause where finale si vous voulez uniquement la médiane et non la distribution centile entière.

Sir Wobin
la source
1
Cela fonctionne plutôt bien et permet le partitionnement des données.
Jonathan Beerhalter
3
Si c'est OK d'être éteint par un, alors la requête ci-dessus est très bien. Mais si vous avez besoin de la médiane exacte, vous aurez des problèmes. Par exemple, pour la séquence (1,3,5,7) la médiane est 4 mais la requête ci-dessus renvoie 3. Pour (1,2,3,503,603,703) la médiane est 258 mais la requête ci-dessus renvoie 503.
Justin Grant
1
Vous pouvez corriger le défaut d'imprécision en prenant max et min de chaque quartile dans une sous-requête, puis en AVGant le MAX du précédent et le MIN du suivant?
Rbjz
18

Encore mieux:

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 !

l --''''''---------------- '' '' '' '' '' '' '
la source
8

MS SQL Server 2012 (et versions ultérieures) possède la fonction PERCENTILE_DISC qui calcule un centile spécifique pour les valeurs triées. PERCENTILE_DISC (0,5) calculera la médiane - https://msdn.microsoft.com/en-us/library/hh231327.aspx

enkryptor
la source
4

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)  
Tobbi
la source
4

Si vous souhaitez utiliser la fonction Créer un agrégat dans SQL Server, voici comment procéder. Le faire de cette façon a l'avantage de pouvoir écrire des requêtes propres. Notez que ce processus pourrait être adapté pour calculer une valeur de centile assez facilement.

Créez un nouveau projet Visual Studio et définissez le framework cible sur .NET 3.5 (c'est pour SQL 2008, il peut être différent dans SQL 2012). Créez ensuite un fichier de classe et insérez le code suivant, ou l'équivalent 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

Ensuite, compilez-le et copiez le fichier DLL et PDB sur votre machine 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 ensuite écrire une requête pour calculer la médiane comme ceci: SELECT dbo.Median (Field) FROM Table

Rono
la source
3

Je suis juste tombé sur cette page en cherchant une solution basée sur un ensemble de médiane. Après avoir examiné certaines des solutions ici, j'ai trouvé ce qui suit. L'espoir est aide / fonctionne.

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
Brian
la source
3

La requête suivante renvoie la médiane d'une liste de valeurs dans une colonne. Il ne peut pas être utilisé en tant que ou avec une fonction d'agrégation, mais vous pouvez toujours l'utiliser comme sous-requête avec une clause WHERE dans la sélection interne.

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
PyQL
la source
3

Bien que la solution de Justin Grant semble solide, j'ai trouvé que lorsque vous avez un certain nombre de valeurs en double dans une clé de partition donnée, les numéros de ligne pour les valeurs en double ASC finissent dans le désordre afin qu'ils ne s'alignent pas 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 qu'il ne soit pas aussi efficace compte tenu de l'utilisation de plusieurs tables dérivées, il résout le problème de classement des lignes que j'ai rencontré. Toute amélioration serait la bienvenue car je ne suis pas très expérimenté en 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
Jeff Sisson
la source
2

L'exemple de Justin ci-dessus est très bon. Mais ce besoin de clé primaire doit être énoncé très clairement. J'ai vu ce code dans la nature sans la clé et les résultats sont mauvais.

La plainte que je reçois au sujet du Percentile_Cont est qu'il ne vous donnera pas une valeur réelle de l'ensemble de données. Pour obtenir une "médiane" qui est une valeur réelle de l'ensemble de données, utilisez 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
Brian Nordberg
la source
2

Dans un UDF, écrivez:

 Select Top 1 medianSortColumn from Table T
  Where (Select Count(*) from Table
         Where MedianSortColumn <
           (Select Count(*) From Table) / 2)
  Order By medianSortColumn
Charles Bretana
la source
7
Dans le cas d'un nombre pair d'articles, la médiane est la moyenne des deux articles du milieu, qui n'est pas couverte par cette FDU.
Yaakov Ellis
1
Pouvez-vous le réécrire dans l'ensemble de l'UDF?
Przemyslaw Remin
2

Constatation médiane

Il s'agit de 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)
Nivesh Krishna
la source
comment va gérer le cas lorsque le nombre de lignes est pair?
priojeet priyom
1

Pour une variable / mesure continue '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
Karishma kavle
la source
1

En utilisant l'agrégat COUNT, vous pouvez d'abord compter le nombre de lignes et les stocker dans une variable appelée @cnt. Ensuite, vous pouvez calculer les paramètres du filtre OFFSET-FETCH pour spécifier, en fonction de l'ordre de quantité, le nombre de lignes à ignorer (valeur de décalage) et le nombre de lignes à filtrer (valeur de récupération).

Le nombre de lignes à ignorer est (@cnt - 1) / 2. Il est clair que pour un nombre impair, ce calcul est correct car vous soustrayez d'abord 1 pour la valeur intermédiaire unique, avant de diviser par 2.

Cela fonctionne également correctement pour un nombre pair car la division utilisée dans l'expression est une division entière; donc, lorsque vous soustrayez 1 d'un nombre pair, vous vous retrouvez avec une valeur impaire.

Lors de la division de cette valeur impaire par 2, la partie fraction du résultat (0,5) est tronquée. Le nombre de lignes à récupérer est de 2 - (@cnt% 2). L'idée est que lorsque le nombre est impair, le résultat de l'opération modulo est 1, et vous devez récupérer 1 ligne. Lorsque le nombre est égal, le résultat de l'opération modulo est 0 et vous devez récupérer 2 lignes. En soustrayant le résultat 1 ou 0 de l'opération modulo de 2, vous obtenez respectivement le 1 ou le 2 souhaité. Enfin, pour calculer la quantité médiane, prenez une ou deux quantités de résultat et appliquez une moyenne après avoir converti la valeur entière d'entrée en une valeur numérique comme suit:

DECLARE @cnt AS INT = (SELECT COUNT(*) FROM [Sales].[production].[stocks]);
SELECT AVG(1.0 * quantity) AS median
FROM ( SELECT quantity
FROM [Sales].[production].[stocks]
ORDER BY quantity
OFFSET (@cnt - 1) / 2 ROWS FETCH NEXT 2 - @cnt % 2 ROWS ONLY ) AS D;
Amira Bedhiafi
la source
0

Je voulais trouver une solution par moi-même, mais mon cerveau a trébuché et est tombé en chemin. Je pense que cela fonctionne, mais ne me demandez pas de l'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)
Gavin
la source
0
--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]
Gregg Silverman
la source
0

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

)
SQLMason
la source
0

Pour les débutants comme moi qui apprennent les bases, je trouve personnellement cet exemple plus facile à suivre, car il est plus facile de comprendre exactement ce qui se passe et d'où viennent les valeurs médianes ...

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 cependant !!!

Justine
la source
0

C'est une réponse aussi simple que j'ai pu trouver. A bien fonctionné avec mes données. Si vous souhaitez exclure certaines valeurs, ajoutez simplement une clause where à la sélection interne.

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
John P.
la source
0

La solution suivante fonctionne sous ces hypothèses:

  • Aucune valeur en double
  • Pas de NULL

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) ; 
Maria Ines Parnisari
la source
0
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
Arie Yehieli
la source
0

J'essaie avec plusieurs alternatives, mais étant donné que mes enregistrements de données ont des valeurs répétées, les versions ROW_NUMBER ne semblent pas être un choix pour moi. Voici donc la requête que j'ai utilisée (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;
Grippé
la source
0

En s'appuyant sur la réponse de Jeff Atwood ci-dessus, c'est avec GROUP BY et une sous-requête corrélée pour obtenir la médiane de 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
Jim B
la source
0

Souvent, nous pouvons avoir besoin de calculer la médiane non seulement pour la table entière, mais pour les agrégats par rapport à certains ID. En d'autres termes, calculez la médiane de chaque ID dans notre tableau, 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;

J'espère que ça aide.

Danylo Zherebetskyy
la source
0

Pour votre question, Jeff Atwood avait déjà donné la solution simple et efficace. Mais, si vous cherchez une autre approche pour calculer la médiane, le code SQL ci-dessous 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, ce lien github sera utile.

Veeramani Natarajan
la source
0

C'est la solution la plus optimale pour trouver des médianes auxquelles je puisse penser. Les noms dans l'exemple sont basés sur l'exemple de Justin. Assurez-vous qu'il existe un index pour la table Sales.SalesOrderHeader avec les 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

METTRE À JOUR

Je ne savais pas trop quelle méthode avait les meilleures performances, j'ai donc fait une comparaison entre ma méthode Justin Grants et Jeff Atwoods en exécutant une requête basée sur les trois méthodes dans un lot et le coût du lot de chaque requête était:

Sans index:

  • Mine 30%
  • Justin accorde 13%
  • Jeff Atwoods 58%

Et avec index

  • Mine 3%.
  • Justin accorde 10%
  • Jeff Atwoods 87%

J'ai essayé de voir à quel point les requêtes évoluent si vous avez un index en créant plus de données à partir d'environ 14 000 lignes par un facteur de 2 à 512, ce qui signifie au final environ 7,2 millions de lignes. Remarque J'ai vérifié que le champ CustomeId était unique pour chaque fois que je faisais une seule copie, de sorte que la proportion de lignes par rapport à l'instance unique de CustomerId était maintenue constante. Pendant que je faisais cela, j'ai exécuté des exécutions où j'ai reconstruit l'index par la suite, et j'ai remarqué que les résultats se stabilisaient autour d'un facteur 128 avec les données que j'avais sur ces valeurs:

  • Mine 3%.
  • Justin accorde 5%
  • Jeff Atwoods 92%

Je me demandais comment les performances auraient pu être affectées par la mise à l'échelle du nombre de lignes mais en maintenant une constante CustomerId constante, alors j'ai configuré un nouveau test où je l'ai fait. Maintenant, au lieu de se stabiliser, le rapport des coûts par lots a continué de diverger, également au lieu d'environ 20 lignes par CustomerId par moyenne que j'avais à la fin environ 10000 lignes par un ID unique. Les chiffres où:

  • Mine 4%
  • Justins 60%
  • Jeffs 35%

Je me suis assuré d'avoir implémenté chaque méthode correctement en comparant les résultats. Ma conclusion est que la méthode que j'ai utilisée est généralement plus rapide tant que l'index 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

Un moyen d'améliorer encore davantage les performances des appels ultérieurs à cette requête consiste à conserver les informations de comptage dans une table auxiliaire. Vous pouvez même le maintenir en ayant un déclencheur qui se met à jour et contient des informations concernant le nombre de lignes SalesOrderHeader dépendant de CustomerId, bien sûr, vous pouvez également simplement stocker la médiane.

Kaveh Hadjari
la source
0

Pour les jeux de données à grande échelle, vous pouvez essayer ce GIST:

https://gist.github.com/chrisknoll/1b38761ce8c5016ec5b2

Il fonctionne en agrégeant les valeurs distinctes que vous trouveriez dans votre ensemble (telles que l'âge ou l'année de naissance, etc.) et utilise les fonctions de la fenêtre SQL pour localiser toute position de centile que vous spécifiez dans la requête.

Chris Knoll
la source