Funktion zum Berechnen des Medians in SQL Server


227

Laut MSDN ist Median in Agact-SQL nicht als Aggregatfunktion verfügbar. Ich möchte jedoch herausfinden, ob es möglich ist, diese Funktionalität zu erstellen (mithilfe der Funktion " Aggregat erstellen" , einer benutzerdefinierten Funktion oder einer anderen Methode).

Was wäre der beste Weg (wenn möglich), dies zu tun - die Berechnung eines Medianwerts (unter der Annahme eines numerischen Datentyps) in einer aggregierten Abfrage zu ermöglichen?


Antworten:


144

2019 UPDATE: In den 10 Jahren, seit ich diese Antwort geschrieben habe, wurden mehr Lösungen gefunden, die zu besseren Ergebnissen führen können. Außerdem haben SQL Server-Versionen seitdem (insbesondere SQL 2012) neue T-SQL-Funktionen eingeführt, mit denen Mediane berechnet werden können. SQL Server-Versionen haben auch das Abfrageoptimierungsprogramm verbessert, das sich auf die Leistung verschiedener Medianlösungen auswirken kann. Net-net, mein ursprünglicher Beitrag von 2009 ist noch in Ordnung, aber es gibt möglicherweise bessere Lösungen für moderne SQL Server-Apps. Schauen Sie sich diesen Artikel aus dem Jahr 2012 an, der eine großartige Ressource darstellt: https://sqlperformance.com/2012/08/t-sql-queries/median

In diesem Artikel wurde festgestellt, dass das folgende Muster viel, viel schneller ist als alle anderen Alternativen, zumindest in Bezug auf das einfache Schema, das sie getestet haben. Diese Lösung war 373x schneller (!!!) als die langsamste ( PERCENTILE_CONT) getestete Lösung. Beachten Sie, dass für diesen Trick zwei separate Abfragen erforderlich sind, die möglicherweise nicht in allen Fällen praktikabel sind. Es erfordert auch SQL 2012 oder höher.

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;

Nur weil ein Test mit einem Schema im Jahr 2012 großartige Ergebnisse erbracht hat, kann Ihr Kilometerstand natürlich variieren, insbesondere wenn Sie mit SQL Server 2014 oder höher arbeiten. Wenn perf für Ihre Medianberechnung wichtig ist, würde ich dringend empfehlen, mehrere der in diesem Artikel empfohlenen Optionen auszuprobieren und zu testen, um sicherzustellen, dass Sie die beste für Ihr Schema gefunden haben.

Ich würde auch besonders vorsichtig sein, wenn ich die Funktion (neu in SQL Server 2012) verwende PERCENTILE_CONT, die in einer der anderen Antworten auf diese Frage empfohlen wird , da der oben verlinkte Artikel feststellte, dass diese integrierte Funktion 373-mal langsamer ist als die schnellste Lösung. Es ist möglich, dass sich diese Ungleichheit in den letzten 7 Jahren verbessert hat, aber ich persönlich würde diese Funktion nicht für einen großen Tisch verwenden, bis ich ihre Leistung im Vergleich zu anderen Lösungen überprüft habe.

ORIGINAL 2009 POST IST UNTEN:

Es gibt viele Möglichkeiten, dies zu tun, mit dramatisch variierender Leistung. Hier ist eine besonders gut optimierte Lösung aus Median, ROW_NUMBERs und Leistung . Dies ist eine besonders optimale Lösung, wenn es um tatsächliche E / A geht, die während der Ausführung generiert werden. Sie sieht teurer aus als andere Lösungen, ist aber tatsächlich viel schneller.

Diese Seite enthält auch eine Diskussion anderer Lösungen und Details zu Leistungstests. Beachten Sie die Verwendung einer eindeutigen Spalte als Disambiguator, falls mehrere Zeilen mit demselben Wert der Medianspalte vorhanden sind.

Versuchen Sie wie bei allen Datenbankleistungsszenarien immer, eine Lösung mit realen Daten auf realer Hardware zu testen. Sie wissen nie, wann eine Änderung des SQL Server-Optimierers oder eine Besonderheit in Ihrer Umgebung eine normalerweise schnelle Lösung verlangsamt.

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;

12
Ich denke nicht, dass dies funktioniert, wenn Ihre Daten Dupes enthalten, insbesondere viele Dupes. Sie können nicht garantieren, dass die row_numbers ausgerichtet werden. Sie können einige wirklich verrückte Antworten für Ihren Median erhalten, oder noch schlimmer, überhaupt keinen Median.
Jonathan Beerhalter

26
Aus diesem Grund ist es wichtig, einen Disambiguator (SalesOrderId im obigen Codebeispiel) zu haben, damit Sie sicherstellen können, dass die Reihenfolge der Ergebnismengenzeilen sowohl vorwärts als auch rückwärts konsistent ist. Oft ist ein eindeutiger Primärschlüssel ein idealer Disambiguator, da er ohne separate Indexsuche verfügbar ist. Wenn keine Disambiguierungsspalte verfügbar ist (z. B. wenn die Tabelle keinen eindeutigen Schlüssel enthält), muss ein anderer Ansatz zur Berechnung des Medians verwendet werden, da Sie, wie Sie richtig hervorheben, nicht garantieren können, dass DESC-Zeilennummern Spiegelbilder von sind ASC-Zeilennummern, dann sind die Ergebnisse unvorhersehbar.
Justin Grant

4
Vielen Dank, als ich die Spalten auf meine Datenbank umstellte, ließ ich den Disambiguator fallen, weil ich dachte, dass er nicht relevant ist. In diesem Fall funktioniert diese Lösung wirklich sehr gut.
Jonathan Beerhalter

8
Ich schlage vor, dem Code selbst einen Kommentar hinzuzufügen, der die Notwendigkeit des Disambiguators beschreibt.
Hoffmanc

4
Genial! Ich habe lange gewusst, wie wichtig es ist, aber jetzt kann ich ihm einen Namen geben ... den Disambiguator! Danke, Justin!
CodeMonkey

204

Wenn Sie SQL 2005 oder besser verwenden, ist dies eine nette, einfache Medianberechnung für eine einzelne Spalte in einer Tabelle:

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

62
Das ist klug und relativ einfach, da es keine Median () - Aggregatfunktion gibt. Aber wie kommt es, dass keine Median () -Funktion existiert? Ich bin ein bisschen FLOOR () ed, ehrlich gesagt.
Charlie Kilian

Gut, nett und einfach, aber normalerweise benötigen Sie einen Median pro bestimmter Gruppenkategorie, z select gid, median(score) from T group by gid. Benötigen Sie dafür eine korrelierte Unterabfrage?
TMS

1
... ich meine wie in diesem Fall (die 2. Abfrage mit dem Namen "Benutzer mit der höchsten mittleren Antwortpunktzahl").
TMS

Tomas - haben Sie es geschafft, Ihr Problem "pro bestimmte Gruppenkategorie" zu lösen? Da habe ich das gleiche Problem. Vielen Dank.
Stu Harper

3
Wie verwende ich diese Lösung mit einem GROUP BY?
Przemyslaw Remin

82

In SQL Server 2012 sollten Sie PERCENTILE_CONT verwenden :

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

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


12
Diese Expertenanalyse ist aufgrund der schlechten Leistung ein überzeugendes Argument gegen die PERCENTILE-Funktionen. sqlperformance.com/2012/08/t-sql-queries/median
carl.anderson

4
Müssen Sie kein DISTINCToder hinzufügen GROUPY BY SalesOrderID? Andernfalls haben Sie viele doppelte Zeilen.
Konstantin

1
das ist die Antwort. Ich weiß nicht, warum ich so weit scrollen musste
FistOfFury

Es gibt auch eine diskrete Version mitPERCENTILE_DISC
JohnDanger

Hervorheben des obigen Punktes von @ carl.anderson: Eine PERCENTILE_CONT-Lösung wurde als 373-mal langsamer (!!!!) gemessen als die schnellste Lösung, die sie unter SQL Server 2012 in ihrem jeweiligen Testschema getestet haben. Lesen Sie den Artikel, den Carl verlinkt hat, um weitere Informationen zu erhalten.
Justin Grant

21

Meine ursprüngliche schnelle Antwort war:

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

Dies gibt Ihnen den Median und den Interquartilbereich auf einen Schlag. Wenn Sie wirklich nur eine Zeile als Median möchten, kommentieren Sie die where-Klausel aus.

Wenn Sie dies in einen Erklärungsplan einfügen, sortieren 60% der Arbeit die Daten, was bei der Berechnung derartiger positionsabhängiger Statistiken unvermeidbar ist.

Ich habe die Antwort geändert, um dem ausgezeichneten Vorschlag von Robert Ševčík-Robajz in den Kommentaren unten zu folgen:

;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

Dies sollte die korrekten Median- und Perzentilwerte berechnen, wenn Sie eine gerade Anzahl von Datenelementen haben. Kommentieren Sie die letzte where-Klausel erneut aus, wenn Sie nur den Median und nicht die gesamte Perzentilverteilung möchten.


1
Dies funktioniert tatsächlich ziemlich gut und ermöglicht die Partitionierung der Daten.
Jonathan Beerhalter

3
Wenn es in Ordnung ist, um eins aus zu sein, ist die obige Abfrage in Ordnung. Wenn Sie jedoch den genauen Median benötigen, haben Sie Probleme. Zum Beispiel ist für die Sequenz (1,3,5,7) der Median 4, aber die obige Abfrage gibt 3 zurück. Für (1,2,3,503,603,703) ist der Median 258, aber die obige Abfrage gibt 503 zurück.
Justin Grant

1
Sie könnten den Fehler der Ungenauigkeit beheben, indem Sie max und min jedes Quartils in einer Unterabfrage nehmen und dann den MAX des vorherigen und den MIN des nächsten AVGing?
Rbjz

18

Noch besser:

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);

Vom Meister selbst, Itzik Ben-Gan !



4

Einfach, schnell, genau

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)  

4

Wenn Sie die Funktion "Aggregat erstellen" in SQL Server verwenden möchten, gehen Sie wie folgt vor. Auf diese Weise haben Sie den Vorteil, dass Sie saubere Abfragen schreiben können. Beachten Sie, dass dieser Prozess angepasst werden kann, um einen Perzentilwert ziemlich einfach zu berechnen.

Erstellen Sie ein neues Visual Studio-Projekt und setzen Sie das Zielframework auf .NET 3.5 (dies ist für SQL 2008, in SQL 2012 kann es anders sein). Erstellen Sie dann eine Klassendatei und geben Sie den folgenden Code oder ein c # -Äquivalent ein:

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

Kompilieren Sie es dann, kopieren Sie die DLL- und PDB-Datei auf Ihren SQL Server-Computer und führen Sie den folgenden Befehl in SQL Server aus:

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

Sie können dann eine Abfrage schreiben, um den Median wie folgt zu berechnen: SELECT dbo.Median (Field) FROM Table


3

Ich bin gerade auf diese Seite gestoßen, als ich nach einer satzbasierten Lösung für den Median gesucht habe. Nachdem ich mir einige der Lösungen hier angesehen hatte, kam ich auf Folgendes. Hoffnung ist Hilfe / funktioniert.

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

3

Die folgende Abfrage gibt den Median aus einer Liste von Werten in einer Spalte zurück. Es kann nicht als oder zusammen mit einer Aggregatfunktion verwendet werden, Sie können es jedoch weiterhin als Unterabfrage mit einer WHERE-Klausel in der inneren Auswahl verwenden.

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

3

Obwohl die Lösung von Justin Grant solide erscheint, stellte ich fest, dass bei einer Anzahl von doppelten Werten innerhalb eines bestimmten Partitionsschlüssels die Zeilennummern für die doppelten ASC-Werte nicht in der richtigen Reihenfolge sind, sodass sie nicht richtig ausgerichtet sind.

Hier ist ein Fragment aus meinem Ergebnis:

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

Ich habe Justins Code als Grundlage für diese Lösung verwendet. Obwohl es angesichts der Verwendung mehrerer abgeleiteter Tabellen nicht so effizient ist, löst es das aufgetretene Problem der Zeilenreihenfolge. Verbesserungen wären willkommen, da ich nicht so viel Erfahrung mit T-SQL habe.

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

2

Justins Beispiel oben ist sehr gut. Dieser Primärschlüsselbedarf sollte jedoch sehr deutlich angegeben werden. Ich habe diesen Code in freier Wildbahn ohne den Schlüssel gesehen und die Ergebnisse sind schlecht.

Die Beschwerde, die ich über Percentile_Cont bekomme, ist, dass es Ihnen keinen tatsächlichen Wert aus dem Datensatz gibt. Verwenden Sie Percentile_Disc, um zu einem "Median" zu gelangen, der ein tatsächlicher Wert aus dem Datensatz ist.

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

2

Schreiben Sie in einer UDF:

 Select Top 1 medianSortColumn from Table T
  Where (Select Count(*) from Table
         Where MedianSortColumn <
           (Select Count(*) From Table) / 2)
  Order By medianSortColumn

7
Bei einer geraden Anzahl von Elementen ist der Median der Durchschnitt der beiden mittleren Elemente, der von dieser UDF nicht abgedeckt wird.
Yaakov Ellis

1
Können Sie es in der gesamten UDF umschreiben?
Przemyslaw Remin

2

Median Finding

Dies ist die einfachste Methode, um den Median eines Attributs zu ermitteln.

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)

Wie wird der Fall behandelt, wenn die Anzahl der Zeilen gerade ist?
Priojeet Priyom


1

Für eine stetige Variable / Maßnahme 'col1' aus '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

1

Mit dem COUNT-Aggregat können Sie zunächst zählen, wie viele Zeilen vorhanden sind, und in einer Variablen namens @cnt speichern. Anschließend können Sie Parameter für den OFFSET-FETCH-Filter berechnen, um basierend auf der Mengenreihenfolge anzugeben, wie viele Zeilen übersprungen werden sollen (Versatzwert) und wie viele gefiltert werden sollen (Abrufwert).

Die Anzahl der zu überspringenden Zeilen beträgt (@cnt - 1) / 2. Es ist klar, dass diese Berechnung für eine ungerade Anzahl korrekt ist, da Sie zuerst 1 für den einzelnen Mittelwert subtrahieren, bevor Sie durch 2 dividieren.

Dies funktioniert auch bei einer geraden Zählung korrekt, da die im Ausdruck verwendete Division eine ganzzahlige Division ist. Wenn Sie also 1 von einer geraden Zählung abziehen, bleibt ein ungerader Wert übrig.

Wenn Sie diesen ungeraden Wert durch 2 teilen, wird der Bruchteil des Ergebnisses (.5) abgeschnitten. Die Anzahl der abzurufenden Zeilen beträgt 2 - (@cnt% 2). Die Idee ist, dass wenn die Anzahl ungerade ist, das Ergebnis der Modulo-Operation 1 ist und Sie 1 Zeile abrufen müssen. Wenn die Anzahl gerade ist, ist das Ergebnis der Modulo-Operation 0, und Sie müssen 2 Zeilen abrufen. Durch Subtrahieren des 1- oder 0-Ergebnisses der Modulo-Operation von 2 erhalten Sie die gewünschte 1 bzw. 2. Um schließlich die Mediangröße zu berechnen, nehmen Sie die eine oder zwei Ergebnisgrößen und wenden Sie einen Durchschnitt an, nachdem Sie den eingegebenen ganzzahligen Wert wie folgt in einen numerischen Wert konvertiert haben:

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;

0

Ich wollte selbst eine Lösung finden, aber mein Gehirn stolperte und fiel auf den Weg. Ich denke, es funktioniert, aber bitte mich nicht, es am Morgen zu erklären. : 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)

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]

0

Dies funktioniert mit 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

)

0

Für Neulinge wie mich, die die Grundlagen lernen, finde ich es persönlich einfacher, diesem Beispiel zu folgen, da es einfacher ist, genau zu verstehen, was passiert und woher die Medianwerte kommen ...

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

In absoluter Ehrfurcht vor einigen der oben genannten Codes !!!


0

Dies ist eine so einfache Antwort, wie ich sie mir vorstellen kann. Hat gut mit meinen Daten funktioniert. Wenn Sie bestimmte Werte ausschließen möchten, fügen Sie der inneren Auswahl einfach eine where-Klausel hinzu.

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

0

Die folgende Lösung funktioniert unter diesen Annahmen:

  • Keine doppelten Werte
  • Keine 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) ; 

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

0

Ich versuche es mit mehreren Alternativen, aber da meine Datensätze wiederholte Werte haben, scheinen die ROW_NUMBER-Versionen für mich keine Wahl zu sein. Also hier die Abfrage, die ich verwendet habe (eine Version mit 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;

0

Aufbauend auf Jeff Atwoods Antwort oben wird mit GROUP BY und einer korrelierten Unterabfrage der Median für jede Gruppe ermittelt.

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

0

Häufig müssen wir den Median möglicherweise nicht nur für die gesamte Tabelle berechnen, sondern auch für Aggregate in Bezug auf eine ID. Mit anderen Worten, berechnen Sie den Median für jede ID in unserer Tabelle, wobei jede ID viele Datensätze enthält. (basierend auf der von @gdoron bearbeiteten Lösung: gute Leistung und funktioniert in vielen 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;

Ich hoffe es hilft.


0

Für Ihre Frage hatte Jeff Atwood bereits die einfache und effektive Lösung angegeben. Wenn Sie jedoch nach einem alternativen Ansatz zur Berechnung des Medians suchen, hilft Ihnen der folgende SQL-Code.

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;

Wenn Sie den Median in MySQL berechnen möchten , ist dieser Github-Link hilfreich.


0

Dies ist die optimalste Lösung, um Mediane zu finden, die mir einfallen. Die Namen im Beispiel basieren auf dem Beispiel von Justin. Stellen Sie sicher, dass ein Index für die Tabelle Sales.SalesOrderHeader mit den Indexspalten CustomerId und TotalDue in dieser Reihenfolge vorhanden ist.

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

AKTUALISIEREN

Ich war mir nicht sicher, welche Methode die beste Leistung aufweist. Daher habe ich einen Vergleich zwischen meiner Methode Justin Grants und Jeff Atwoods durchgeführt, indem ich eine Abfrage basierend auf allen drei Methoden in einem Stapel ausgeführt habe. Die Stapelkosten für jede Abfrage waren:

Ohne Index:

  • Meins 30%
  • Justin gewährt 13%
  • Jeff Atwoods 58%

Und mit Index

  • Meins 3%.
  • Justin gewährt 10%
  • Jeff Atwoods 87%

Ich habe versucht zu sehen, wie gut die Abfragen skalieren, wenn Sie einen Index haben, indem Sie mehr Daten aus ungefähr 14 000 Zeilen um den Faktor 2 bis 512 erstellen, was am Ende ungefähr 7,2 Millionen Zeilen bedeutet. Hinweis Ich habe sichergestellt, dass das CustomeId-Feld bei jeder einzelnen Kopie eindeutig ist, sodass der Anteil der Zeilen im Vergleich zur eindeutigen Instanz von CustomerId konstant gehalten wurde. Während ich dies tat, führte ich Ausführungen aus, bei denen ich den Index anschließend neu erstellte, und bemerkte, dass sich die Ergebnisse mit den Daten, die ich zu diesen Werten hatte, auf einen Faktor von 128 stabilisierten:

  • Meins 3%.
  • Justin gewährt 5%
  • Jeff Atwoods 92%

Ich habe mich gefragt, wie sich die Skalierung der Anzahl der Zeilen auf die Leistung ausgewirkt haben könnte, aber die eindeutige Kunden-ID konstant gehalten hat. Deshalb habe ich einen neuen Test eingerichtet, bei dem ich genau dies getan habe. Anstatt sich zu stabilisieren, ging das Batch-Kostenverhältnis weiter auseinander, auch anstatt etwa 20 Zeilen pro Kunden-ID pro Durchschnitt, den ich am Ende hatte, ungefähr 10000 Zeilen pro solch eindeutiger ID. Die Zahlen wo:

  • Meins 4%
  • Justins 60%
  • Jeffs 35%

Ich habe sichergestellt, dass ich jede Methode korrekt implementiert habe, indem ich die Ergebnisse verglichen habe. Mein Fazit ist, dass die von mir verwendete Methode im Allgemeinen schneller ist, solange ein Index vorhanden ist. Beachten Sie auch, dass diese Methode in diesem Artikel https://www.microsoftpressstore.com/articles/article.aspx?p=2314819&seqNum=5 für dieses spezielle Problem empfohlen wird

Eine Möglichkeit, die Leistung nachfolgender Aufrufe dieser Abfrage noch weiter zu verbessern, besteht darin, die Zählinformationen in einer Hilfstabelle beizubehalten. Sie können es sogar beibehalten, indem Sie einen Trigger haben, der Informationen zur Anzahl der SalesOrderHeader-Zeilen abhängig von der Kunden-ID aktualisiert und enthält. Natürlich können Sie dann auch einfach den Median speichern.


Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.