Funkcja do obliczania mediany w programie SQL Server


227

Według MSDN Mediana nie jest dostępna jako funkcja agregująca w Transact-SQL. Chciałbym jednak dowiedzieć się, czy można utworzyć tę funkcję (za pomocą funkcji Utwórz agregację, funkcji zdefiniowanej przez użytkownika lub innej metody).

Jaki byłby najlepszy sposób (jeśli to możliwe), aby to zrobić - pozwolić na obliczenie wartości mediany (przy założeniu liczbowego typu danych) w zapytaniu zagregowanym?


Odpowiedzi:


145

AKTUALIZACJA 2019: W ciągu 10 lat od napisania tej odpowiedzi odkryto więcej rozwiązań, które mogą przynieść lepsze wyniki. Ponadto wersje SQL Server od tego czasu (zwłaszcza SQL 2012) wprowadziły nowe funkcje T-SQL, których można używać do obliczania median. Wersje programu SQL Server poprawiły również optymalizator zapytań, który może wpływać na różne rozwiązania mediany. Net-net, mój oryginalny post z 2009 roku jest nadal OK, ale mogą istnieć lepsze rozwiązania dla nowoczesnych aplikacji SQL Server. Spójrz na ten artykuł z 2012 roku, który jest świetnym źródłem: https://sqlperformance.com/2012/08/t-sql-queries/median

W tym artykule stwierdzono, że następujący wzorzec jest znacznie, znacznie szybszy niż wszystkie inne alternatywy, przynajmniej na testowanym prostym schemacie. To rozwiązanie było 373 razy szybsze (!!!) niż PERCENTILE_CONTtestowane najwolniejsze ( ) rozwiązanie. Pamiętaj, że ta sztuczka wymaga dwóch osobnych zapytań, które mogą nie być praktyczne we wszystkich przypadkach. Wymaga również SQL 2012 lub nowszego.

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;

Oczywiście tylko dlatego, że jeden test na jednym schemacie w 2012 roku przyniósł świetne wyniki, twój przebieg może się różnić, szczególnie jeśli korzystasz z SQL Server 2014 lub nowszej wersji. Jeśli perf jest ważne dla obliczenia mediany, zdecydowanie sugeruję wypróbowanie i przetestowanie kilku opcji zalecanych w tym artykule, aby upewnić się, że znalazłeś najlepszą dla swojego schematu.

Byłbym również szczególnie ostrożny przy użyciu funkcji (nowość w SQL Server 2012), PERCENTILE_CONTktóra jest zalecana w jednej z pozostałych odpowiedzi na to pytanie, ponieważ w powyższym artykule stwierdzono, że ta wbudowana funkcja jest 373 razy wolniejsza niż najszybsze rozwiązanie. Możliwe, że różnica ta uległa poprawie w ciągu 7 lat, ale osobiście nie użyłbym tej funkcji na dużym stole, dopóki nie zweryfikuję jej wydajności w porównaniu z innymi rozwiązaniami.

POCZĄTEK ORYGINALNY 2009 JEST PONIŻEJ:

Można to zrobić na wiele sposobów, z radykalnie różną wydajnością. Oto jedno szczególnie dobrze zoptymalizowane rozwiązanie od median, ROW_NUMBER i wydajności . Jest to szczególnie optymalne rozwiązanie, jeśli chodzi o rzeczywiste operacje we / wy generowane podczas wykonywania - wygląda na droższe niż inne rozwiązania, ale w rzeczywistości jest znacznie szybsze.

Ta strona zawiera także omówienie innych rozwiązań i szczegóły testowania wydajności. Zwróć uwagę na użycie unikatowej kolumny jako elementu ujednoznaczniającego w przypadku, gdy istnieje wiele wierszy o tej samej wartości środkowej kolumny.

Podobnie jak w przypadku wszystkich scenariuszy wydajności bazy danych, zawsze staraj się przetestować rozwiązanie z prawdziwymi danymi na prawdziwym sprzęcie - nigdy nie wiesz, kiedy zmiana optymalizatora SQL Server lub osobliwość w twoim środowisku spowolnią normalnie szybkie rozwiązanie.

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
Nie sądzę, że to działa, jeśli masz kopie, szczególnie dużo kopii w swoich danych. Nie możesz zagwarantować, że liczba_wierszy zostanie wyrównana. Możesz uzyskać naprawdę szalone odpowiedzi na swoją medianę lub, co gorsza, brak mediany w ogóle.
Jonathan Beerhalter

26
Dlatego tak ważne jest posiadanie disambiguatora (SalesOrderId w powyższym przykładzie kodu), aby można było zapewnić, że kolejność wierszy zestawu wyników jest spójna zarówno do tyłu, jak i do przodu. Często unikalny klucz główny stanowi idealny dezambiguator, ponieważ jest dostępny bez osobnego wyszukiwania indeksu. Jeśli nie ma dostępnej kolumny ujednoznaczniającej (na przykład, jeśli tabela nie ma klucza jednoznacznego), należy zastosować inne podejście do obliczenia mediany, ponieważ jak prawidłowo wskazano, jeśli nie można zagwarantować, że numery wierszy DESC są lustrzanymi obrazami Numery wierszy ASC, a następnie wyniki są nieprzewidywalne.
Justin Grant

4
Dzięki, przełączając kolumny na moją bazę danych, upuściłem disambiguator, myśląc, że to nie ma znaczenia. W takim przypadku to rozwiązanie działa naprawdę bardzo dobrze.
Jonathan Beerhalter,

8
Sugeruję dodanie komentarza do samego kodu, opisującego potrzebę wprowadzenia disambiguatora.
hoffmanc

4
Niesamowite! już dawno wiedziałem o jego znaczeniu, ale teraz mogę nadać jej nazwę ... disambiguator! Dziękuję Ci Justin!
CodeMonkey,

204

Jeśli używasz SQL 2005 lub nowszego, jest to ładne, proste obliczenie mediany dla pojedynczej kolumny w tabeli:

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
Jest to sprytne i stosunkowo proste, biorąc pod uwagę, że nie istnieje funkcja agregująca Median (). Ale jak to możliwe, że nie istnieje funkcja Median () !? Szczerze mówiąc, jestem trochę FLOOR ().
Charlie Kilian,

Cóż, ładne i proste, ale zwykle potrzebujesz mediany dla określonej kategorii grupy, tj select gid, median(score) from T group by gid. Jak . Czy potrzebujesz do tego skorelowanego podzapytania?
TMS

1
... Mam na myśli jak w tym przypadku (drugie zapytanie o nazwie „Użytkownicy z najwyższym medianowym wynikiem odpowiedzi”).
TMS

Tomas - czy udało Ci się rozwiązać problem związany z „dla określonej kategorii grupy”? Ponieważ mam ten sam problem. Dzięki.
Stu Harper,

3
Jak korzystać z tego rozwiązania w przypadku GROUP BY?
Przemysław Remin

82

W SQL Server 2012 należy użyć 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

Zobacz także: http://blog.sqlauthority.com/2011/11/20/sql-server-introduction-to-percentile_cont-analytic-functions-introduced-in-sql-server-2012/


12
Ta ekspercka analiza stanowi ważny argument przeciwko funkcjom PERCENTILE z powodu niskiej wydajności. sqlperformance.com/2012/08/t-sql-queries/median
carl.anderson

4
Nie musisz dodawać DISTINCTlub GROUPY BY SalesOrderID? W przeciwnym razie będziesz mieć wiele zduplikowanych wierszy.
Konstantin

1
to jest odpowiedź. nie wiem, dlaczego musiałem przewijać tak daleko
FistOfFury,

Dostępna jest także dyskretna wersjaPERCENTILE_DISC
johnDanger

podkreślając powyższy punkt @ carl.anderson: zmierzono, że rozwiązanie PERCENTILE_CONT jest 373x wolniejsze (!!!!) w porównaniu z najszybszym rozwiązaniem, które przetestowali na SQL Server 2012 na ich konkretnym schemacie testowym. Przeczytaj artykuł, który łączy Carl, aby uzyskać więcej informacji.
Justin Grant,

21

Moja oryginalna szybka odpowiedź brzmiała:

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

To da ci medianę i zakres międzykwartylowy za jednym zamachem. Jeśli naprawdę chcesz tylko jednego wiersza, który jest medianą, usuń komentarz z klauzuli where.

Gdy włożysz to w plan wyjaśniania, 60% pracy polega na sortowaniu danych, co jest nieuniknione przy obliczaniu takich statystyk zależnych od pozycji.

Zmieniłem odpowiedź, aby zastosować się do doskonałej sugestii Roberta Ševčíka-Robajza w poniższych komentarzach:

;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

To powinno obliczyć prawidłowe wartości mediany i percentyla, gdy masz parzystą liczbę elementów danych. Ponownie odkomentuj klauzulę końcową gdzie, jeśli chcesz tylko mediany, a nie całego rozkładu percentyla.


1
To faktycznie działa całkiem dobrze i pozwala na partycjonowanie danych.
Jonathan Beerhalter

3
Jeśli jedno jest wyłączone, powyższe zapytanie jest w porządku. Ale jeśli potrzebujesz dokładnej mediany, będziesz miał kłopoty. Na przykład dla sekwencji (1,3,5,7) mediana wynosi 4, ale powyższe zapytanie zwraca 3. Dla (1,2,3,503,603,703) mediana wynosi 258, ale powyższe zapytanie zwraca 503.
Justin Grant

1
Możesz naprawić wadę niedokładności, biorąc maks i min każdego kwartylu w podzapytaniu, a następnie AVGing MAX z poprzedniego i MIN z następnego?
Rbjz

18

Nawet lepiej:

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

Od samego mistrza, Itzika Ben-Gana !



4

Prosty, szybki, dokładny

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

Jeśli chcesz użyć funkcji Utwórz agregację w programie SQL Server, oto jak to zrobić. Robienie tego w ten sposób ma tę zaletę, że można pisać czyste zapytania. Należy zauważyć, że ten proces można dostosować do dość łatwego obliczania wartości procentowej.

Utwórz nowy projekt Visual Studio i ustaw platformę docelową na .NET 3.5 (dotyczy SQL 2008, może być inaczej w SQL 2012). Następnie utwórz plik klasy i wstaw następujący kod lub równoważnik 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

Następnie skompiluj go i skopiuj plik DLL i PDB na maszynę SQL Server i uruchom następującą komendę w 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

Następnie możesz napisać zapytanie, aby obliczyć medianę w następujący sposób: WYBIERZ dbo.Median (pole) Z tabeli


3

Właśnie natknąłem się na tę stronę, szukając rozwiązania mediany opartego na zestawie. Po zapoznaniu się z niektórymi rozwiązaniami tutaj wymyśliłem następujące. Nadzieja pomaga / działa.

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

Poniższe zapytanie zwraca medianę z listy wartości w jednej kolumnie. Nie można go używać jako funkcji agregującej lub razem z nią, ale nadal można jej używać jako zapytania podrzędnego z klauzulą ​​WHERE w wewnętrznej selekcji.

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

Chociaż rozwiązanie przyznane przez Justina wydaje się solidne, zauważyłem, że gdy masz wiele zduplikowanych wartości w danym kluczu partycji, numery wierszy dla duplikatów wartości ASC kończą się poza kolejnością, więc nie są odpowiednio wyrównane.

Oto fragment z mojego wyniku:

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

Użyłem kodu Justina jako podstawy tego rozwiązania. Chociaż nie jest tak wydajny, biorąc pod uwagę użycie wielu tabel pochodnych, rozwiązuje napotkany problem porządkowania wierszy. Wszelkie ulepszenia byłyby mile widziane, ponieważ nie mam doświadczenia w 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

2

Powyższy przykład Justina jest bardzo dobry. Ale ta potrzeba klucza podstawowego powinna być jasno określona. Widziałem ten kod na wolności bez klucza, a wyniki są złe.

Skarga, którą otrzymuję na temat Percentile_Cont, polega na tym, że nie da ona rzeczywistej wartości z zestawu danych. Aby dostać się do „mediany”, która jest rzeczywistą wartością z zestawu danych, użyj 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

2

W UDF napisz:

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

7
W przypadku parzystej liczby pozycji mediana jest średnią z dwóch środkowych pozycji, która nie jest objęta tym UDF.
Yaakov Ellis

1
Czy możesz przepisać to w całym UDF?
Przemysław Remin

2

Mediana Finding

Jest to najprostsza metoda znalezienia mediany atrybutu.

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)

jak do obsługi przypadku, gdy liczba rzędów jest parzysta?
priojeet priyom


1

Dla zmiennej ciągłej / miary „col1” z „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

Za pomocą agregatu COUNT możesz najpierw policzyć, ile jest wierszy, i zapisać w zmiennej o nazwie @cnt. Następnie możesz obliczyć parametry dla filtra OFFSET-FETCH, aby określić, w oparciu o kolejność według ilości, liczbę wierszy do pominięcia (wartość przesunięcia) i liczbę do filtrowania (wartość pobierania).

Liczba wierszy do pominięcia wynosi (@cnt - 1) / 2. Jest oczywiste, że dla liczby nieparzystej obliczenia są poprawne, ponieważ najpierw odejmuje się 1 dla pojedynczej wartości środkowej, a następnie dzieli się przez 2.

Działa to również poprawnie dla liczenia parzystego, ponieważ użytym w wyrażeniu podziałem jest dzielenie całkowite; więc odejmując 1 od liczby parzystej, otrzymujesz nieparzystą wartość.

Dzieląc tę ​​nieparzystą wartość przez 2, ułamkowa część wyniku (.5) jest obcinana. Liczba wierszy do pobrania wynosi 2 - (@cnt% 2). Chodzi o to, że gdy liczba jest nieparzysta, wynikiem operacji modulo jest 1, a ty musisz pobrać 1 wiersz. Gdy liczba jest nawet wynikiem operacji modulo, wynosi 0 i musisz pobrać 2 wiersze. Odejmując wynik 1 lub 0 operacji modulo od 2, otrzymujesz odpowiednio 1 lub 2 odpowiednio. Na koniec, aby obliczyć medianę, weź jedną lub dwie wielkości wynikowe i zastosuj średnią po konwersji wejściowej wartości całkowitej na liczbową w następujący sposób:

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

Chciałem sam wypracować rozwiązanie, ale mój mózg potknął się i upadł. Myślę, że to działa, ale nie proś mnie o wyjaśnienie rano. : 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

Działa to z 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

Dla początkujących, takich jak ja, którzy uczą się podstaw, osobiście uważam, że ten przykład jest łatwiejszy do naśladowania, ponieważ łatwiej jest dokładnie zrozumieć, co się dzieje i skąd pochodzą wartości mediany ...

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

Jednak pod absolutnym podziwem niektóre z powyższych kodów !!!


0

To najprostsza odpowiedź, jaką mogłem wymyślić. Działa dobrze z moimi danymi. Jeśli chcesz wykluczyć pewne wartości, po prostu dodaj klauzulę where do wewnętrznego wyboru.

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

Poniższe rozwiązanie działa przy tych założeniach:

  • Brak zduplikowanych wartości
  • Brak wartości NULL

Kod:

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

Próbuję z kilkoma alternatywami, ale ponieważ moje rekordy danych mają powtarzające się wartości, wydaje się, że wersje ROW_NUMBER nie są dla mnie wyborem. Więc tutaj użyłem zapytania (wersja z 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

Opierając się na powyższej odpowiedzi Jeffa Atwooda, jest to z GROUP BY i skorelowanym podzapytaniem, aby uzyskać medianę dla każdej grupy.

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

Często możemy potrzebować obliczyć Medianę nie tylko dla całej tabeli, ale dla agregatów w odniesieniu do niektórych ID. Innymi słowy, oblicz medianę dla każdego identyfikatora w naszej tabeli, gdzie każdy identyfikator ma wiele rekordów. (w oparciu o rozwiązanie edytowane przez @gdoron: dobra wydajność i działa w wielu 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;

Mam nadzieję, że to pomoże.


0

Na twoje pytanie Jeff Atwood podał już proste i skuteczne rozwiązanie. Ale jeśli szukasz alternatywnego podejścia do obliczenia mediany, pomoże ci poniższy kod SQL.

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;

Jeśli chcesz obliczyć medianę w MySQL, ten link github będzie przydatny.


0

Jest to najbardziej optymalne rozwiązanie do znajdowania median, jakie mogę wymyślić. Nazwy w przykładzie oparte są na przykładzie Justina. Upewnij się, że istnieje indeks dla tabeli Sales.SalesOrderHeader z kolumnami indeksów CustomerId i TotalDue w tym zamówieniu.

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

AKTUALIZACJA

Nie byłem pewien, która metoda ma najlepszą wydajność, więc porównałem moją metodę Justin Grants i Jeffa Atwooda, uruchamiając zapytanie oparte na wszystkich trzech metodach w jednej partii, a koszt partii dla każdego zapytania wynosił:

Bez indeksu:

  • Mój 30%
  • Justin przyznaje 13%
  • Jeff Atwoods 58%

I z indeksem

  • Moje 3%.
  • Justin przyznaje 10%
  • Jeff Atwoods 87%

Próbowałem zobaczyć, jak dobrze skalują się zapytania, jeśli masz indeks, tworząc więcej danych z około 14 000 wierszy od 2 do 512 razy, co oznacza w końcu około 7,2 miliona wierszy. Uwaga: Upewniłem się, że pole CustomeId jest unikalne dla każdego wykonania pojedynczej kopii, więc proporcja wierszy w porównaniu do unikalnego wystąpienia CustomerId była stała. Podczas gdy to robiłem, uruchomiłem wykonanie, w którym później odbudowałem indeks, i zauważyłem, że wyniki ustabilizowały się na poziomie około 128 przy danych, które miałem do tych wartości:

  • Moje 3%.
  • Justin przyznaje 5%
  • Jeff Atwoods 92%

Zastanawiałem się, w jaki sposób mogło wpłynąć na wydajność poprzez skalowanie liczby wierszy, ale utrzymanie stałej unikalnej wartości CustomerId, więc skonfigurowałem nowy test, w którym właśnie to zrobiłem. Teraz zamiast się ustabilizować, stosunek kosztów partii wciąż się rozchodził, również zamiast około 20 wierszy na CustomerId średnio miałem na koniec około 10000 wierszy na taki unikalny identyfikator. Liczby, w których:

  • Moje 4%
  • Justins 60%
  • Jeffs 35%

Upewniłem się, że zaimplementowałem każdą metodę poprawnie, porównując wyniki. Mój wniosek jest taki, że zastosowana metoda jest generalnie szybsza, dopóki istnieje indeks. Zauważyłem również, że ta metoda jest zalecana dla tego konkretnego problemu w tym artykule https://www.microsoftpressstore.com/articles/article.aspx?p=2314819&seqNum=5

Sposobem, aby jeszcze bardziej poprawić wydajność kolejnych wywołań tego zapytania, jest utrwalenie informacji o liczbie w tabeli pomocniczej. Można go nawet utrzymać, uruchamiając aktualizację wyzwalacza i przechowującą informacje dotyczące liczby wierszy SalesOrderHeader zależnych od CustomerId, oczywiście wtedy można również po prostu zapisać medianę.


0

W przypadku zestawów danych na dużą skalę możesz wypróbować ten GIST:

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

Działa poprzez agregację odrębnych wartości, które można znaleźć w zestawie (takich jak wiek, rok urodzenia itp.), I wykorzystuje funkcje okna SQL do zlokalizowania dowolnej pozycji percentyla określonej w zapytaniu.

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.