Funzione per calcolare la mediana in SQL Server


227

Secondo MSDN , Median non è disponibile come funzione aggregata in Transact-SQL. Tuttavia, vorrei scoprire se è possibile creare questa funzionalità (utilizzando la funzione Crea aggregato , la funzione definita dall'utente o un altro metodo).

Quale sarebbe il modo migliore (se possibile) per farlo - consentire il calcolo di un valore mediano (assumendo un tipo di dati numerico) in una query aggregata?


Risposte:


145

AGGIORNAMENTO 2019: nei 10 anni da quando ho scritto questa risposta, sono state scoperte più soluzioni che potrebbero produrre risultati migliori. Inoltre, le versioni di SQL Server da allora (in particolare SQL 2012) hanno introdotto nuove funzionalità T-SQL che possono essere utilizzate per calcolare le mediane. Le versioni di SQL Server hanno anche migliorato il suo Query Optimizer, che potrebbe influire sulla perf di varie soluzioni mediane. Net-net, il mio post originale del 2009 è ancora OK, ma potrebbero esserci soluzioni migliori per le moderne app di SQL Server. Dai un'occhiata a questo articolo del 2012 che è un'ottima risorsa: https://sqlperformance.com/2012/08/t-sql-queries/median

Questo articolo ha riscontrato che il seguente schema è molto, molto più veloce di tutte le altre alternative, almeno sul semplice schema che hanno testato. Questa soluzione è stata 373x più veloce (!!!) della PERCENTILE_CONTsoluzione più lenta ( ) testata. Nota che questo trucco richiede due query separate che potrebbero non essere pratiche in tutti i casi. Richiede anche SQL 2012 o versioni successive.

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;

Naturalmente, solo perché un test su uno schema nel 2012 ha prodotto ottimi risultati, il chilometraggio può variare, soprattutto se si è su SQL Server 2014 o versioni successive. Se perf è importante per il tuo calcolo mediano, ti consiglio vivamente di provare e sottoporre a test perf alcune delle opzioni raccomandate in quell'articolo per assicurarti di aver trovato la migliore per il tuo schema.

Starei anche particolarmente attento usando la funzione (nuova in SQL Server 2012) PERCENTILE_CONTche è consigliata in una delle altre risposte a questa domanda, perché l'articolo collegato sopra ha trovato questa funzione incorporata 373 volte più lenta della soluzione più veloce. È possibile che questa disparità sia migliorata nei 7 anni successivi, ma personalmente non userei questa funzione su un tavolo di grandi dimensioni fino a quando non avrò verificato le sue prestazioni rispetto ad altre soluzioni.

IL POSTO ORIGINALE 2009 È QUI SOTTO:

Ci sono molti modi per farlo, con prestazioni notevolmente diverse. Ecco una soluzione particolarmente ottimizzata, da mediani, ROW_NUMBER e prestazioni . Questa è una soluzione particolarmente ottimale quando si tratta di effettivi I / O generati durante l'esecuzione: sembra più costosa di altre soluzioni, ma in realtà è molto più veloce.

Tale pagina contiene anche una discussione di altre soluzioni e dettagli sui test delle prestazioni. Nota l'uso di una colonna univoca come disambiguatore nel caso in cui vi siano più righe con lo stesso valore della colonna mediana.

Come per tutti gli scenari di prestazioni del database, prova sempre a testare una soluzione con dati reali su hardware reale: non sai mai quando una modifica all'ottimizzatore di SQL Server o una peculiarità nel tuo ambiente rallenterà una soluzione normalmente veloce.

SELECT
   CustomerId,
   AVG(TotalDue)
FROM
(
   SELECT
      CustomerId,
      TotalDue,
      -- SalesOrderId in the ORDER BY is a disambiguator to break ties
      ROW_NUMBER() OVER (
         PARTITION BY CustomerId
         ORDER BY TotalDue ASC, SalesOrderId ASC) AS RowAsc,
      ROW_NUMBER() OVER (
         PARTITION BY CustomerId
         ORDER BY TotalDue DESC, SalesOrderId DESC) AS RowDesc
   FROM Sales.SalesOrderHeader SOH
) x
WHERE
   RowAsc IN (RowDesc, RowDesc - 1, RowDesc + 1)
GROUP BY CustomerId
ORDER BY CustomerId;

12
Non penso che funzioni se hai dati duplicati, in particolare molti dati duplicati, nei tuoi dati. Non è possibile garantire che i numeri di riga siano allineati. Puoi ottenere risposte davvero pazze per la tua mediana, o peggio ancora, nessuna mediana.
Jonathan Beerhalter,

26
Ecco perché è importante disporre di un disambiguatore (SalesOrderId nell'esempio di codice sopra), in modo da poter garantire che l'ordine delle righe del set di risultati sia coerente sia avanti che indietro. Spesso una chiave primaria univoca rende un disambiguatore ideale perché è disponibile senza una ricerca dell'indice separata. Se non è disponibile alcuna colonna di chiarimento delle ambiguità (ad esempio, se la tabella non ha una chiave univoca), è necessario utilizzare un altro approccio per calcolare la mediana, poiché, come sottolineato correttamente, se non è possibile garantire che i numeri di riga DESC siano immagini speculari di Numeri di riga ASC, quindi i risultati sono imprevedibili.
Justin Grant,

4
Grazie, quando ho cambiato le colonne nel mio DB, ho lasciato cadere il disambiguatore, pensando che non fosse rilevante. In tal caso, questa soluzione funziona davvero molto bene.
Jonathan Beerhalter,

8
Suggerisco di aggiungere un commento al codice stesso, descrivendo la necessità del disambiguatore.
hoffmanc,

4
Eccezionale! da molto tempo ne conosco l'importanza, ma ora posso dargli un nome ... il chiaritore! Grazie Justin!
CodeMonkey,

204

Se stai usando SQL 2005 o meglio, questo è un bel calcolo mediano semplice per una singola colonna in una tabella:

SELECT
(
 (SELECT MAX(Score) FROM
   (SELECT TOP 50 PERCENT Score FROM Posts ORDER BY Score) AS BottomHalf)
 +
 (SELECT MIN(Score) FROM
   (SELECT TOP 50 PERCENT Score FROM Posts ORDER BY Score DESC) AS TopHalf)
) / 2 AS Median

62
È intelligente e relativamente semplice dato che non esiste alcuna funzione aggregata Median (). Ma come mai non esiste alcuna funzione Median () !? Sono un po 'FLOOR () ed, francamente.
Charlie Kilian,

Bene, bello e semplice, ma di solito hai bisogno di una mediana per una determinata categoria di gruppo, cioè come select gid, median(score) from T group by gid. Hai bisogno di una subquery correlata per questo?
TMS

1
... intendo come in questo caso (la seconda query denominata "Utenti con il punteggio di risposta mediano più alto").
TMS

Tomas - sei riuscito a risolvere il tuo problema "per una determinata categoria di gruppo"? Come ho lo stesso problema. Grazie.
Stu Harper,

3
Come utilizzare questa soluzione con un GROUP BY?
Przemyslaw Remin,

82

In SQL Server 2012 è necessario utilizzare 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

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


12
Questa analisi di esperti fa un argomento convincente contro le funzioni PERCENTILE a causa delle scarse prestazioni. sqlperformance.com/2012/08/t-sql-queries/median
carl.anderson

4
Non è necessario aggiungere un DISTINCTo GROUPY BY SalesOrderID? Altrimenti avrai molte righe duplicate.
Konstantin,

1
questa è la risposta non so perché ho dovuto scorrere così lontano
FistOfFury

Esiste anche una versione discreta che utilizzaPERCENTILE_DISC
johnDanger il

enfatizzando il punto sopra di @ carl.anderson: una soluzione PERCENTILE_CONT è stata misurata per essere 373x più lenta (!!!!) rispetto alla soluzione più veloce che hanno testato su SQL Server 2012 sul loro particolare schema di test. Leggi l'articolo che Carl ha collegato per maggiori dettagli.
Justin Grant,

21

La mia risposta rapida originale era:

select  max(my_column) as [my_column], quartile
from    (select my_column, ntile(4) over (order by my_column) as [quartile]
         from   my_table) i
--where quartile = 2
group by quartile

Questo ti darà la gamma mediana e interquartile in un colpo solo. Se vuoi davvero solo una riga che è la mediana, decommenta la clausola where.

Quando lo inserisci in un piano esplicativo, il 60% del lavoro sta ordinando i dati che sono inevitabili quando si calcolano statistiche dipendenti dalla posizione come questa.

Ho modificato la risposta per seguire l'eccellente suggerimento di Robert Ševčík-Robajz nei commenti seguenti:

;with PartitionedData as
  (select my_column, ntile(10) over (order by my_column) as [percentile]
   from   my_table),
MinimaAndMaxima as
  (select  min(my_column) as [low], max(my_column) as [high], percentile
   from    PartitionedData
   group by percentile)
select
  case
    when b.percentile = 10 then cast(b.high as decimal(18,2))
    else cast((a.low + b.high)  as decimal(18,2)) / 2
  end as [value], --b.high, a.low,
  b.percentile
from    MinimaAndMaxima a
  join  MinimaAndMaxima b on (a.percentile -1 = b.percentile) or (a.percentile = 10 and b.percentile = 10)
--where b.percentile = 5

Questo dovrebbe calcolare i valori mediani e percentuali corretti quando si dispone di un numero pari di elementi di dati. Ancora una volta, decommenta la clausola where finale se vuoi solo la distribuzione mediana e non l'intera percentuale.


1
In realtà funziona abbastanza bene e consente il partizionamento dei dati.
Jonathan Beerhalter,

3
Se è OK essere disattivato da uno, quindi la query sopra va bene. Ma se hai bisogno della mediana esatta, avrai problemi. Ad esempio, per la sequenza (1,3,5,7) la mediana è 4 ma la query sopra restituisce 3. Per (1,2,3,503.603.703) la mediana è 258 ma la query sopra restituisce 503.
Justin Grant

1
Potresti correggere il difetto dell'imprecisione prendendo il massimo e il minimo di ciascun quartile in una sottoquery, quindi AVGing il MAX del precedente e il MIN del successivo?
Rbjz,

18

Anche meglio:

SELECT @Median = AVG(1.0 * val)
FROM
(
    SELECT o.val, rn = ROW_NUMBER() OVER (ORDER BY o.val), c.c
    FROM dbo.EvenRows AS o
    CROSS JOIN (SELECT c = COUNT(*) FROM dbo.EvenRows) AS c
) AS x
WHERE rn IN ((c + 1)/2, (c + 2)/2);

Dal maestro stesso, Itzik Ben-Gan !



4

Semplice, veloce, preciso

SELECT x.Amount 
FROM   (SELECT amount, 
               Count(1) OVER (partition BY 'A')        AS TotalRows, 
               Row_number() OVER (ORDER BY Amount ASC) AS AmountOrder 
        FROM   facttransaction ft) x 
WHERE  x.AmountOrder = Round(x.TotalRows / 2.0, 0)  

4

Se si desidera utilizzare la funzione Crea aggregato in SQL Server, ecco come farlo. Farlo in questo modo ha il vantaggio di poter scrivere query pulite. Si noti che questo processo potrebbe essere adattato per calcolare un valore percentuale abbastanza facilmente.

Creare un nuovo progetto di Visual Studio e impostare il framework di destinazione su .NET 3.5 (questo è per SQL 2008, potrebbe essere diverso in SQL 2012). Quindi crea un file di classe e inserisci il seguente codice, o equivalente in c #:

Imports Microsoft.SqlServer.Server
Imports System.Data.SqlTypes
Imports System.IO

<Serializable>
<SqlUserDefinedAggregate(Format.UserDefined, IsInvariantToNulls:=True, IsInvariantToDuplicates:=False, _
  IsInvariantToOrder:=True, MaxByteSize:=-1, IsNullIfEmpty:=True)>
Public Class Median
  Implements IBinarySerialize
  Private _items As List(Of Decimal)

  Public Sub Init()
    _items = New List(Of Decimal)()
  End Sub

  Public Sub Accumulate(value As SqlDecimal)
    If Not value.IsNull Then
      _items.Add(value.Value)
    End If
  End Sub

  Public Sub Merge(other As Median)
    If other._items IsNot Nothing Then
      _items.AddRange(other._items)
    End If
  End Sub

  Public Function Terminate() As SqlDecimal
    If _items.Count <> 0 Then
      Dim result As Decimal
      _items = _items.OrderBy(Function(i) i).ToList()
      If _items.Count Mod 2 = 0 Then
        result = ((_items((_items.Count / 2) - 1)) + (_items(_items.Count / 2))) / 2@
      Else
        result = _items((_items.Count - 1) / 2)
      End If

      Return New SqlDecimal(result)
    Else
      Return New SqlDecimal()
    End If
  End Function

  Public Sub Read(r As BinaryReader) Implements IBinarySerialize.Read
    'deserialize it from a string
    Dim list = r.ReadString()
    _items = New List(Of Decimal)

    For Each value In list.Split(","c)
      Dim number As Decimal
      If Decimal.TryParse(value, number) Then
        _items.Add(number)
      End If
    Next

  End Sub

  Public Sub Write(w As BinaryWriter) Implements IBinarySerialize.Write
    'serialize the list to a string
    Dim list = ""

    For Each item In _items
      If list <> "" Then
        list += ","
      End If      
      list += item.ToString()
    Next
    w.Write(list)
  End Sub
End Class

Quindi compilarlo e copiare il file DLL e PDB sul computer SQL Server ed eseguire il comando seguente in SQL Server:

CREATE ASSEMBLY CustomAggregate FROM '{path to your DLL}'
WITH PERMISSION_SET=SAFE;
GO

CREATE AGGREGATE Median(@value decimal(9, 3))
RETURNS decimal(9, 3) 
EXTERNAL NAME [CustomAggregate].[{namespace of your DLL}.Median];
GO

È quindi possibile scrivere una query per calcolare la mediana in questo modo: SELEZIONARE dbo.Median (Field) FROM Table


3

Mi sono appena imbattuto in questa pagina mentre cercavo una soluzione basata su set per la mediana. Dopo aver esaminato alcune delle soluzioni qui, ho pensato a quanto segue. La speranza è di aiuto / funziona.

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

La query seguente restituisce la mediana da un elenco di valori in una colonna. Non può essere utilizzato come o insieme a una funzione aggregata, ma è comunque possibile utilizzarlo come una sottoquery con una clausola WHERE nella selezione interna.

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

Sebbene la soluzione di concessione di Justin appaia solida, ho scoperto che quando hai un numero di valori duplicati all'interno di una determinata chiave di partizione, i numeri di riga per i valori duplicati ASC finiscono fuori sequenza quindi non si allineano correttamente.

Ecco un frammento del mio risultato:

KEY VALUE ROWA ROWD  

13  2     22   182
13  1     6    183
13  1     7    184
13  1     8    185
13  1     9    186
13  1     10   187
13  1     11   188
13  1     12   189
13  0     1    190
13  0     2    191
13  0     3    192
13  0     4    193
13  0     5    194

Ho usato il codice di Justin come base per questa soluzione. Sebbene non sia efficiente dato l'uso di più tabelle derivate, risolve il problema di ordinamento delle righe che ho riscontrato. Eventuali miglioramenti sarebbero i benvenuti in quanto non ho esperienza in T-SQL.

SELECT PKEY, cast(AVG(VALUE)as decimal(5,2)) as MEDIANVALUE
FROM
(
  SELECT PKEY,VALUE,ROWA,ROWD,
  'FLAG' = (CASE WHEN ROWA IN (ROWD,ROWD-1,ROWD+1) THEN 1 ELSE 0 END)
  FROM
  (
    SELECT
    PKEY,
    cast(VALUE as decimal(5,2)) as VALUE,
    ROWA,
    ROW_NUMBER() OVER (PARTITION BY PKEY ORDER BY ROWA DESC) as ROWD 

    FROM
    (
      SELECT
      PKEY, 
      VALUE,
      ROW_NUMBER() OVER (PARTITION BY PKEY ORDER BY VALUE ASC,PKEY ASC ) as ROWA 
      FROM [MTEST]
    )T1
  )T2
)T3
WHERE FLAG = '1'
GROUP BY PKEY
ORDER BY PKEY

2

L'esempio di Justin sopra è molto buono. Ma la necessità della chiave primaria dovrebbe essere dichiarata in modo molto chiaro. Ho visto quel codice allo stato brado senza la chiave e i risultati sono cattivi.

Il reclamo che ricevo riguardo a Percentile_Cont è che non ti darà un valore effettivo dal set di dati. Per arrivare a una "mediana" che è un valore effettivo dal set di dati, utilizzare Percentile_Disc.

SELECT SalesOrderID, OrderQty,
    PERCENTILE_DISC(0.5) 
        WITHIN GROUP (ORDER BY OrderQty)
        OVER (PARTITION BY SalesOrderID) AS MedianCont
FROM Sales.SalesOrderDetail
WHERE SalesOrderID IN (43670, 43669, 43667, 43663)
ORDER BY SalesOrderID DESC

2

In un UDF, scrivi:

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

7
Nel caso di un numero pari di elementi, la mediana è la media dei due elementi intermedi, che non è coperta da questo UDF.
Yaakov Ellis

1
Puoi riscriverlo nell'intero UDF?
Przemyslaw Remin,

2

Alla ricerca mediana

Questo è il metodo più semplice per trovare la mediana di un attributo.

Select round(S.salary,4) median from employee S where (select count(salary) from station where salary < S.salary ) = (select count(salary) from station where salary > S.salary)

come gestirà il caso quando il numero di righe è pari?
priojeet priyom


1

Per una variabile / misura continua 'col1' da 'table1'

select col1  
from
    (select top 50 percent col1, 
    ROW_NUMBER() OVER(ORDER BY col1 ASC) AS Rowa,
    ROW_NUMBER() OVER(ORDER BY col1 DESC) AS Rowd
    from table1 ) tmp
where tmp.Rowa = tmp.Rowd

1

Usando il COUNT aggregato, puoi prima contare quante righe ci sono e archiviare in una variabile chiamata @cnt. Quindi è possibile calcolare i parametri per il filtro OFFSET-FETCH per specificare, in base all'ordine qty, quante righe saltare (valore offset) e quante filtrare (valore recupero).

Il numero di righe da saltare è (@cnt - 1) / 2. È chiaro che per un conteggio dispari questo calcolo è corretto perché sottrai 1 per il singolo valore medio, prima di dividere per 2.

Questo funziona anche correttamente per un conteggio pari perché la divisione utilizzata nell'espressione è divisione intera; quindi, sottraendo 1 da un conteggio pari, ti rimane un valore dispari.

Quando si divide quel valore dispari per 2, la parte della frazione del risultato (.5) viene troncata. Il numero di righe da recuperare è 2 - (@cnt% 2). L'idea è che quando il conteggio è dispari il risultato dell'operazione modulo è 1 e devi recuperare 1 riga. Quando il conteggio è pari, il risultato dell'operazione modulo è 0 e devi recuperare 2 righe. Sottraendo il risultato 1 o 0 dell'operazione modulo da 2, si ottiene rispettivamente l'1 o 2 desiderato. Infine, per calcolare la quantità mediana, prendere una o due quantità di risultati e applicare una media dopo aver convertito il valore intero di input in uno numerico come segue:

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

Volevo trovare una soluzione da solo, ma il mio cervello è inciampato e si è messo in cammino. Penso che funzioni, ma non chiedermi di spiegarlo al mattino. : 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

Funziona con SQL 2000:

DECLARE @testTable TABLE 
( 
    VALUE   INT
)
--INSERT INTO @testTable -- Even Test
--SELECT 3 UNION ALL
--SELECT 5 UNION ALL
--SELECT 7 UNION ALL
--SELECT 12 UNION ALL
--SELECT 13 UNION ALL
--SELECT 14 UNION ALL
--SELECT 21 UNION ALL
--SELECT 23 UNION ALL
--SELECT 23 UNION ALL
--SELECT 23 UNION ALL
--SELECT 23 UNION ALL
--SELECT 29 UNION ALL
--SELECT 40 UNION ALL
--SELECT 56

--
--INSERT INTO @testTable -- Odd Test
--SELECT 3 UNION ALL
--SELECT 5 UNION ALL
--SELECT 7 UNION ALL
--SELECT 12 UNION ALL
--SELECT 13 UNION ALL
--SELECT 14 UNION ALL
--SELECT 21 UNION ALL
--SELECT 23 UNION ALL
--SELECT 23 UNION ALL
--SELECT 23 UNION ALL
--SELECT 23 UNION ALL
--SELECT 29 UNION ALL
--SELECT 39 UNION ALL
--SELECT 40 UNION ALL
--SELECT 56


DECLARE @RowAsc TABLE
(
    ID      INT IDENTITY,
    Amount  INT
)

INSERT INTO @RowAsc
SELECT  VALUE 
FROM    @testTable 
ORDER BY VALUE ASC

SELECT  AVG(amount)
FROM @RowAsc ra
WHERE ra.id IN
(
    SELECT  ID 
    FROM    @RowAsc
    WHERE   ra.id -
    (
        SELECT  MAX(id) / 2.0 
        FROM    @RowAsc
    ) BETWEEN 0 AND 1

)

0

Per i neofiti come me che stanno imparando le basi, trovo personalmente questo esempio più facile da seguire, in quanto è più facile capire esattamente cosa sta succedendo e da dove provengono i valori mediani ...

select
 ( max(a.[Value1]) + min(a.[Value1]) ) / 2 as [Median Value1]
,( max(a.[Value2]) + min(a.[Value2]) ) / 2 as [Median Value2]

from (select
    datediff(dd,startdate,enddate) as [Value1]
    ,xxxxxxxxxxxxxx as [Value2]
     from dbo.table1
     )a

In assoluto timore di alcuni dei codici sopra però !!!


0

Questa è una risposta tanto semplice quanto potrei trovare. Ha funzionato bene con i miei dati. Se si desidera escludere determinati valori, è sufficiente aggiungere una clausola where alla selezione interna.

SELECT TOP 1 
    ValueField AS MedianValue
FROM
    (SELECT TOP(SELECT COUNT(1)/2 FROM tTABLE)
        ValueField
    FROM 
        tTABLE
    ORDER BY 
        ValueField) A
ORDER BY
    ValueField DESC

0

La seguente soluzione funziona con questi presupposti:

  • Nessun valore duplicato
  • Nessun null

Codice:

IF OBJECT_ID('dbo.R', 'U') IS NOT NULL
  DROP TABLE dbo.R

CREATE TABLE R (
    A FLOAT NOT NULL);

INSERT INTO R VALUES (1);
INSERT INTO R VALUES (2);
INSERT INTO R VALUES (3);
INSERT INTO R VALUES (4);
INSERT INTO R VALUES (5);
INSERT INTO R VALUES (6);

-- Returns Median(R)
select SUM(A) / CAST(COUNT(A) AS FLOAT)
from R R1 
where ((select count(A) from R R2 where R1.A > R2.A) = 
      (select count(A) from R R2 where R1.A < R2.A)) OR
      ((select count(A) from R R2 where R1.A > R2.A) + 1 = 
      (select count(A) from R R2 where R1.A < R2.A)) OR
      ((select count(A) from R R2 where R1.A > R2.A) = 
      (select count(A) from R R2 where R1.A < R2.A) + 1) ; 

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

Provo con diverse alternative, ma a causa dei miei record di dati ha valori ripetuti, le versioni di ROW_NUMBER sembrano non essere una scelta per me. Quindi qui la query che ho usato (una versione con NTILE):

SELECT distinct
   CustomerId,
   (
       MAX(CASE WHEN Percent50_Asc=1 THEN TotalDue END) OVER (PARTITION BY CustomerId)  +
       MIN(CASE WHEN Percent50_desc=1 THEN TotalDue END) OVER (PARTITION BY CustomerId) 
   )/2 MEDIAN
FROM
(
   SELECT
      CustomerId,
      TotalDue,
     NTILE(2) OVER (
         PARTITION BY CustomerId
         ORDER BY TotalDue ASC) AS Percent50_Asc,
     NTILE(2) OVER (
         PARTITION BY CustomerId
         ORDER BY TotalDue DESC) AS Percent50_desc
   FROM Sales.SalesOrderHeader SOH
) x
ORDER BY CustomerId;

0

Basandosi sulla risposta di Jeff Atwood qui sopra, è con GROUP BY e una sottoquery correlata per ottenere la mediana per ciascun gruppo.

SELECT TestID, 
(
 (SELECT MAX(Score) FROM
   (SELECT TOP 50 PERCENT Score FROM Posts WHERE TestID = Posts_parent.TestID ORDER BY Score) AS BottomHalf)
 +
 (SELECT MIN(Score) FROM
   (SELECT TOP 50 PERCENT Score FROM Posts WHERE TestID = Posts_parent.TestID ORDER BY Score DESC) AS TopHalf)
) / 2 AS MedianScore,
AVG(Score) AS AvgScore, MIN(Score) AS MinScore, MAX(Score) AS MaxScore
FROM Posts_parent
GROUP BY Posts_parent.TestID

0

Spesso, potrebbe essere necessario calcolare la mediana non solo per l'intera tabella, ma per gli aggregati rispetto ad alcuni ID. In altre parole, calcola la mediana per ciascun ID nella nostra tabella, dove ogni ID ha molti record. (basato sulla soluzione modificata da @gdoron: buone prestazioni e funziona in molti SQL)

SELECT our_id, AVG(1.0 * our_val) as Median
FROM
( SELECT our_id, our_val, 
  COUNT(*) OVER (PARTITION BY our_id) AS cnt,
  ROW_NUMBER() OVER (PARTITION BY our_id ORDER BY our_val) AS rnk
  FROM our_table
) AS x
WHERE rnk IN ((cnt + 1)/2, (cnt + 2)/2) GROUP BY our_id;

Spero che sia d'aiuto.


0

Per la tua domanda, Jeff Atwood aveva già fornito la soluzione semplice ed efficace. Ma, se stai cercando un approccio alternativo per calcolare la mediana, sotto il codice SQL ti aiuterà.

create table employees(salary int);

insert into employees values(8); insert into employees values(23); insert into employees values(45); insert into employees values(123); insert into employees values(93); insert into employees values(2342); insert into employees values(2238);

select * from employees;

declare @odd_even int; declare @cnt int; declare @middle_no int;


set @cnt=(select count(*) from employees); set @middle_no=(@cnt/2)+1; select @odd_even=case when (@cnt%2=0) THEN -1 ELse 0 END ;


 select AVG(tbl.salary) from  (select  salary,ROW_NUMBER() over (order by salary) as rno from employees group by salary) tbl  where tbl.rno=@middle_no or tbl.rno=@middle_no+@odd_even;

Se stai cercando di calcolare la mediana in MySQL, questo link github sarà utile.


0

Questa è la soluzione ottimale per trovare mediane a cui riesco a pensare. I nomi nell'esempio si basano sull'esempio di Giustino. Assicurarsi che esista un indice per la tabella Sales.SalesOrderHeader con le colonne dell'indice CustomerId e TotalDue in quell'ordine.

SELECT
 sohCount.CustomerId,
 AVG(sohMid.TotalDue) as TotalDueMedian
FROM 
(SELECT 
  soh.CustomerId,
  COUNT(*) as NumberOfRows
FROM 
  Sales.SalesOrderHeader soh 
GROUP BY soh.CustomerId) As sohCount
CROSS APPLY 
    (Select 
       soh.TotalDue
    FROM 
    Sales.SalesOrderHeader soh 
    WHERE soh.CustomerId = sohCount.CustomerId 
    ORDER BY soh.TotalDue
    OFFSET sohCount.NumberOfRows / 2 - ((sohCount.NumberOfRows + 1) % 2) ROWS 
    FETCH NEXT 1 + ((sohCount.NumberOfRows + 1) % 2) ROWS ONLY
    ) As sohMid
GROUP BY sohCount.CustomerId

AGGIORNARE

Non ero sicuro di quale metodo avesse le migliori prestazioni, quindi ho fatto un confronto tra il mio metodo Justin Grants e Jeff Atwoods eseguendo una query basata su tutti e tre i metodi in un batch e il costo in batch di ogni query era:

Senza indice:

  • Miniera 30%
  • Justin concede il 13%
  • Jeff Atwoods 58%

E con indice

  • Il mio 3%.
  • Justin concede il 10%
  • Jeff Atwoods 87%

Ho provato a vedere quanto le query si ridimensionano se si dispone di un indice creando più dati da circa 14000 righe di un fattore compreso tra 2 e 512, il che significa alla fine circa 7,2 milioni di righe. Nota: mi sono assicurato che il campo CustomeId fosse unico per ogni volta che facevo una singola copia, quindi la proporzione di righe rispetto all'istanza univoca di CustomerId era mantenuta costante. Mentre stavo facendo questo, ho eseguito esecuzioni in cui ho ricostruito l'indice in seguito, e ho notato che i risultati si sono stabilizzati intorno a un fattore 128 con i dati che ho avuto a questi valori:

  • Il mio 3%.
  • Justin concede il 5%
  • Jeff Atwoods 92%

Mi chiedevo come le prestazioni avrebbero potuto essere influenzate dal ridimensionamento del numero di righe ma mantenendo costante l'ID cliente unico, quindi ho impostato un nuovo test in cui ho fatto proprio questo. Ora invece di stabilizzarsi, il rapporto costi lotto ha continuato a divergere, anche invece di circa 20 righe per ID cliente per media che alla fine avevo circa 10000 righe per tale ID univoco. I numeri dove:

  • Miniera 4%
  • Giustino 60%
  • Jeffs 35%

Mi sono assicurato di aver implementato ogni metodo corretto confrontando i risultati. La mia conclusione è che il metodo che ho usato è generalmente più veloce finché esiste l'indice. Ho anche notato che questo metodo è ciò che è raccomandato per questo particolare problema in questo articolo https://www.microsoftpressstore.com/articles/article.aspx?p=2314819&seqNum=5

Un modo per migliorare ulteriormente ulteriormente le prestazioni delle chiamate successive a questa query consiste nel persistere le informazioni sul conteggio in una tabella ausiliaria. Potresti anche mantenerlo avendo un trigger che aggiorna e contiene informazioni relative al conteggio delle righe SalesOrderHeader dipendenti da CustomerId, ovviamente puoi anche semplicemente memorizzare la mediana.


Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.