Função para calcular a mediana no SQL Server


227

De acordo com o MSDN , a Mediana não está disponível como uma função agregada no Transact-SQL. No entanto, gostaria de descobrir se é possível criar essa funcionalidade (usando a função Criar Agregado , função definida pelo usuário ou algum outro método).

Qual seria a melhor maneira (se possível) de fazer isso - permitir o cálculo de um valor mediano (assumindo um tipo de dados numérico) em uma consulta agregada?


Respostas:


145

ATUALIZAÇÃO 2019: Nos 10 anos desde que escrevi esta resposta, foram descobertas mais soluções que podem produzir melhores resultados. Além disso, as versões do SQL Server desde então (especialmente o SQL 2012) introduziram novos recursos do T-SQL que podem ser usados ​​para calcular medianas. As versões do SQL Server também aprimoraram seu otimizador de consultas, que pode afetar o desempenho de várias soluções medianas. Net-net, minha postagem original de 2009 ainda está OK, mas pode haver soluções melhores para os aplicativos modernos do SQL Server. Dê uma olhada neste artigo de 2012, que é um ótimo recurso: https://sqlperformance.com/2012/08/t-sql-queries/median

Este artigo considerou o seguinte padrão muito, muito mais rápido que todas as outras alternativas, pelo menos no esquema simples que eles testaram. Esta solução foi 373x mais rápida (!!!) que a PERCENTILE_CONTsolução mais lenta ( ) testada. Observe que esse truque requer duas consultas separadas, que podem não ser práticas em todos os casos. Também requer o SQL 2012 ou posterior.

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;

Obviamente, apenas porque um teste em um esquema em 2012 produziu ótimos resultados, sua milhagem pode variar, especialmente se você estiver no SQL Server 2014 ou posterior. Se o perf for importante para o cálculo da mediana, sugiro que você teste e faça o teste de várias das opções recomendadas nesse artigo para garantir que você encontrou o melhor para o seu esquema.

Eu também teria um cuidado especial ao usar a função (nova no SQL Server 2012) PERCENTILE_CONTrecomendada em uma das outras respostas a esta pergunta, porque o artigo vinculado acima considerou essa função interna 373x mais lenta que a solução mais rápida. É possível que essa disparidade tenha melhorado nos 7 anos desde então, mas pessoalmente eu não usaria essa função em uma tabela grande até verificar seu desempenho em relação a outras soluções.

O POST ORIGINAL DE 2009 ESTÁ ABAIXO:

Existem várias maneiras de fazer isso, com desempenho dramaticamente variável. Aqui está uma solução particularmente otimizada, de medianas, ROW_NUMBERs e desempenho . Essa é uma solução particularmente ideal quando se trata de E / Ss reais geradas durante a execução - ela parece mais cara que outras soluções, mas na verdade é muito mais rápida.

Essa página também contém uma discussão de outras soluções e detalhes de testes de desempenho. Observe o uso de uma coluna exclusiva como um desambiguador, caso haja várias linhas com o mesmo valor da coluna mediana.

Como em todos os cenários de desempenho do banco de dados, sempre tente testar uma solução com dados reais em hardware real - você nunca sabe quando uma alteração no otimizador do SQL Server ou uma peculiaridade em seu ambiente tornará uma solução normalmente mais rápida.

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
Eu não acho que isso funcione se você tiver enganos, principalmente muitos enganadores, em seus dados. Você não pode garantir que os números de linha se alinham. Você pode obter respostas realmente loucas por sua mediana, ou pior ainda, por nenhuma mediana.
Jonathan Beerhalter 5/10/10

26
É por isso que ter um desambiguador (SalesOrderId no exemplo de código acima) é importante, para garantir que a ordem das linhas do conjunto de resultados seja consistente tanto para trás quanto para frente. Freqüentemente, uma chave primária exclusiva cria um desambiguador ideal porque está disponível sem uma pesquisa de índice separada. Se não houver uma coluna de desambiguação disponível (por exemplo, se a tabela não tiver uma chave uniquificadora), outra abordagem deverá ser usada para calcular a mediana, porque, como você indica corretamente, se não puder garantir que os números das linhas DESC sejam imagens espelhadas de Números de linha ASC, os resultados são imprevisíveis.
Justin Grant

4
Obrigado, ao alternar as colunas para o meu banco de dados, abandonei o desambiguador, pensando que não era relevante. Nesse caso, esta solução funciona realmente muito bem.
Jonathan Beerhalter 10/10

8
Sugiro adicionar um comentário ao próprio código, descrevendo a necessidade do desambiguador.
Hoffmanc

4
Impressionante! há muito que sei sua importância, mas agora posso dar um nome ... o desambiguador! Obrigado justin!
codemonkey

204

Se você estiver usando o SQL 2005 ou melhor, esse é um cálculo mediano simples e agradável para uma única coluna em uma tabela:

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
Isso é inteligente e relativamente simples, uma vez que não existe função agregada Median (). Mas como é que nenhuma função Median () existe !? Estou um pouco chateado, francamente.
Charlie Kilian

Bem, agradável e simples, mas geralmente você precisa de mediana por determinada categoria de grupo, como por exemplo select gid, median(score) from T group by gid. Você precisa de uma subconsulta correlacionada para isso?
TMS

1
... Quero dizer, neste caso (a segunda consulta denominada "Usuários com maior pontuação média de resposta").
TMS

Tomas - você conseguiu resolver o seu problema "por categoria de grupo"? Como eu tenho o mesmo problema. Obrigado.
precisa

3
Como usar esta solução com um GROUP BY?
Przemyslaw Remin

82

No SQL Server 2012, você deve usar 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

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


12
Essa análise especializada apresenta um argumento convincente contra as funções PERCENTILE devido ao baixo desempenho. sqlperformance.com/2012/08/t-sql-queries/median
carl.anderson

4
Você não precisa adicionar um DISTINCTou GROUPY BY SalesOrderID? Caso contrário, você terá muitas linhas duplicadas.
Konstantin

1
essa é a resposta Não sei por que eu tinha para se deslocar até aqui
FistOfFury

Há também uma versão discreta usandoPERCENTILE_DISC
johnDanger

enfatizando o ponto de @ carl.anderson acima: uma solução PERCENTILE_CONT foi avaliada como 373x mais lenta (!!!!) em comparação com a solução mais rápida que eles testaram no SQL Server 2012 em seu esquema de teste específico. Leia o artigo que Carl ligou para mais detalhes.
Justin Grant

21

Minha resposta rápida original foi:

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

Isso lhe dará o alcance mediano e interquartil de uma só vez. Se você realmente deseja apenas uma linha com a mediana, remova o comentário da cláusula where.

Quando você coloca isso em um plano de explicação, 60% do trabalho está classificando os dados que são inevitáveis ​​ao calcular estatísticas dependentes da posição como esta.

Alterei a resposta para seguir a excelente sugestão de Robert Ševčík-Robajz nos comentários abaixo:

;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

Isso deve calcular os valores corretos da mediana e do percentil quando você possui um número par de itens de dados. Novamente, remova o comentário da cláusula where where se você deseja apenas a distribuição mediana e não toda a porcentagem.


1
Isso realmente funciona muito bem e permite o particionamento dos dados.
Jonathan Beerhalter 6/10/10

3
Se não há problema em ser desativado por um, a consulta acima está correta. Mas se você precisar da mediana exata, terá problemas. Por exemplo, para a sequência (1,3,5,7), a mediana é 4, mas a consulta acima retorna 3. Para (1,2,3,503,603,703), a mediana é 258, mas a consulta acima retorna 503.
Justin Grant,

1
Você poderia consertar a falha da imprecisão usando max e min de cada quartil em uma subconsulta, depois AVGing o MAX do anterior e o MIN do próximo?
Rbjz

18

Melhor ainda:

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

Do próprio mestre, Itzik Ben-Gan !



4

Simples, rápido, 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 você deseja usar a função Criar Agregado no SQL Server, é assim que se faz. Fazer dessa maneira tem o benefício de poder escrever consultas limpas. Observe que esse processo pode ser adaptado para calcular um valor percentual com bastante facilidade.

Crie um novo projeto do Visual Studio e defina a estrutura de destino como .NET 3.5 (isso é para o SQL 2008, pode ser diferente no SQL 2012). Em seguida, crie um arquivo de classe e insira o seguinte código ou equivalente em 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

Em seguida, compile-o e copie o arquivo DLL e PDB para sua máquina SQL Server e execute o seguinte comando no 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

Em seguida, você pode escrever uma consulta para calcular a mediana assim: SELECT dbo.Median (Field) FROM Table


3

Acabei de encontrar esta página enquanto procurava uma solução baseada em conjunto para mediana. Depois de analisar algumas das soluções aqui, criei o seguinte. A esperança é ajuda / funciona.

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

A consulta a seguir retorna a mediana de uma lista de valores em uma coluna. Ele não pode ser usado como ou em conjunto com uma função agregada, mas você ainda pode usá-lo como uma subconsulta com uma cláusula WHERE na seleção interna.

SQL Server 2005 ou superior:

SELECT TOP 1 value from
(
    SELECT TOP 50 PERCENT value 
    FROM table_name 
    ORDER BY  value
)for_median
ORDER BY value DESC

3

Embora a solução de Justin Grant pareça sólida, descobri que, quando você tem um número de valores duplicados em uma determinada chave de partição, os números de linha dos valores duplicados ASC terminam fora de sequência, para que não se alinhem adequadamente.

Aqui está um fragmento do meu resultado:

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

Eu usei o código de Justin como base para esta solução. Embora não seja tão eficiente, devido ao uso de várias tabelas derivadas, ele resolve o problema de ordenação de linhas que encontrei. Quaisquer melhorias seriam bem-vindas, pois não sou tão experiente em 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

O exemplo de Justin acima é muito bom. Mas essa necessidade da chave primária deve ser declarada com muita clareza. Eu vi esse código na natureza sem a chave e os resultados são ruins.

A reclamação que recebo sobre o Percentile_Cont é que ele não fornece um valor real do conjunto de dados. Para chegar a uma "mediana" que é um valor real do conjunto de dados, use 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

Em um UDF, escreva:

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

7
No caso de um número par de itens, a mediana é a média dos dois itens do meio, que não é coberta por esta UDF.
Yaakov Ellis

1
Você pode reescrevê-lo em toda a UDF?
Przemyslaw Remin

2

Constatação Mediana

Este é o método mais simples para encontrar a mediana de um atributo.

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)

como vai lidar com o caso quando o número de linhas é par?
priojeet priyom 7/02/19


1

Para uma variável contínua / medida 'col1' de 'tabela1'

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 o COUNT agregado, você pode primeiro contar quantas linhas existem e armazenar em uma variável chamada @cnt. Em seguida, você pode calcular parâmetros para o filtro OFFSET-FETCH para especificar, com base na ordem de quantidade, quantas linhas ignorar (valor de deslocamento) e quantas filtrar (valor de busca).

O número de linhas a serem ignoradas é (@cnt - 1) / 2. É claro que, para uma contagem ímpar, esse cálculo está correto porque você subtrai 1 pelo valor médio único antes de dividir por 2.

Isso também funciona corretamente para uma contagem par porque a divisão usada na expressão é divisão inteira; portanto, ao subtrair 1 de uma contagem par, você fica com um valor ímpar.

Ao dividir esse valor ímpar por 2, a parte da fração do resultado (0,5) é truncada. O número de linhas a serem buscadas é 2 - (@cnt% 2). A idéia é que, quando a contagem for ímpar, o resultado da operação do módulo for 1 e você precisará buscar 1 linha. Quando a contagem é uniforme, o resultado da operação do módulo é 0 e você precisa buscar 2 linhas. Subtraindo o resultado 1 ou 0 da operação do módulo de 2, você obtém o 1 ou 2 desejado, respectivamente. Por fim, para calcular a quantidade mediana, pegue uma ou duas quantidades de resultado e aplique uma média após converter o valor inteiro de entrada em um numérico da seguinte maneira:

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

Eu queria encontrar uma solução sozinho, mas meu cérebro tropeçou e caiu no caminho. Eu acho que funciona, mas não me peça para explicar de manhã. : 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

Isso funciona com o 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

Para iniciantes como eu, que estamos aprendendo o básico, eu pessoalmente acho esse exemplo mais fácil de seguir, pois é mais fácil entender exatamente o que está acontecendo e de onde vêm os valores medianos ...

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

No espanto absoluto de alguns dos códigos acima embora !!!


0

Esta é uma resposta tão simples quanto eu poderia sugerir. Funcionou bem com meus dados. Se você deseja excluir determinados valores, adicione uma cláusula where à seleção 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

A seguinte solução funciona com essas premissas:

  • Nenhum valor duplicado
  • Nenhum NULL

Código:

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

Eu tento com várias alternativas, mas como meus registros de dados têm valores repetidos, as versões ROW_NUMBER parecem não ser uma opção para mim. Então aqui a consulta que eu usei (uma versão com 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

Com base na resposta de Jeff Atwood acima, aqui está o GROUP BY e uma subconsulta correlacionada para obter a mediana de cada grupo.

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

Freqüentemente, talvez seja necessário calcular a mediana não apenas para toda a tabela, mas para agregados com relação a algum ID. Em outras palavras, calcule a mediana para cada ID em nossa tabela, onde cada ID possui muitos registros. (baseado na solução editada por @gdoron: bom desempenho e funciona em muitos 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;

Espero que ajude.


0

Para sua pergunta, Jeff Atwood já havia dado a solução simples e eficaz. Mas, se você estiver procurando alguma abordagem alternativa para calcular a mediana, o código SQL abaixo o ajudará.

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 você deseja calcular a mediana no MySQL, este link do github será útil.


0

Esta é a solução mais ideal para encontrar medianas que eu possa pensar. Os nomes no exemplo são baseados no exemplo de Justin. Verifique se existe um índice para a tabela Sales.SalesOrderHeader com as colunas de índice CustomerId e TotalDue nessa ordem.

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

ATUALIZAR

Eu estava um pouco inseguro sobre qual método tem melhor desempenho, então fiz uma comparação entre meu método Justin Grants e Jeff Atwoods executando a consulta com base nos três métodos em um lote e o custo do lote de cada consulta:

Sem índice:

  • Mina 30%
  • Justin concede 13%
  • Jeff Atwoods 58%

E com índice

  • Mina 3%.
  • Justin concede 10%
  • Jeff Atwoods 87%

Tentei ver quão bem as consultas são dimensionadas se você tiver um índice criando mais dados a partir de 14.000 linhas por um fator de 2 a 512, o que significa, no final, cerca de 7,2 milhões de linhas. Nota: Certifiquei-me de que o campo CustomeId fosse único para cada vez que fiz uma única cópia; portanto, a proporção de linhas comparada à instância exclusiva do CustomerId era mantida constante. Enquanto fazia isso, executei execuções onde reconstruí o índice posteriormente e notei que os resultados se estabilizaram em torno de um fator de 128 com os dados que eu tinha para esses valores:

  • Mina 3%.
  • Justin concede 5%
  • Jeff Atwoods 92%

Perguntei-me como o desempenho poderia ter sido afetado pelo escalonamento do número de linhas, mas mantendo constante CustomerId exclusivo, então configurei um novo teste no qual fiz exatamente isso. Agora, em vez de estabilizar, a taxa de custo do lote continuou divergindo, também em vez de cerca de 20 linhas por CustomerId por média que eu tinha no final em torno de 10.000 linhas por esse ID exclusivo. Os números em que:

  • Mina 4%
  • Justins 60%
  • Jeffs 35%

Assegurei-me de implementar cada método corretamente, comparando os resultados. Minha conclusão é que o método que usei geralmente é mais rápido, desde que o índice exista. Observe também que este método é o recomendado para este problema específico neste artigo https://www.microsoftpressstore.com/articles/article.aspx?p=2314819&seqNum=5

Uma maneira de melhorar ainda mais o desempenho das chamadas subseqüentes a essa consulta é persistir as informações de contagem em uma tabela auxiliar. Você pode até mantê-lo com um gatilho que atualiza e mantém informações sobre a contagem de linhas SalesOrderHeader dependentes do CustomerId, é claro que você também pode armazenar a mediana também.


0

Para conjuntos de dados em grande escala, você pode tentar este GIST:

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

Ele funciona agregando os valores distintos que você encontraria em seu conjunto (como idade, ano de nascimento etc.) e usa as funções da janela SQL para localizar qualquer posição de percentil que você especificar na consulta.

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.