Función para calcular la mediana en SQL Server

227

Según MSDN , Median no está disponible como una función agregada en Transact-SQL. Sin embargo, me gustaría saber si es posible crear esta funcionalidad (usando la función Crear Agregado , la función definida por el usuario o algún otro método).

¿Cuál sería la mejor manera (si es posible) de hacer esto: permitir el cálculo de un valor medio (suponiendo un tipo de datos numéricos) en una consulta agregada?

Yaakov Ellis
fuente

Respuestas:

145

ACTUALIZACIÓN 2019: en los 10 años desde que escribí esta respuesta, se han descubierto más soluciones que pueden arrojar mejores resultados. Además, las versiones de SQL Server desde entonces (especialmente SQL 2012) han introducido nuevas características de T-SQL que pueden usarse para calcular medianas. Las versiones de SQL Server también han mejorado su optimizador de consultas que puede afectar el rendimiento de varias soluciones medianas. Net-net, mi publicación original de 2009 todavía está bien, pero puede haber mejores soluciones para las aplicaciones modernas de SQL Server. Eche un vistazo a este artículo de 2012, que es un gran recurso: https://sqlperformance.com/2012/08/t-sql-queries/median

Este artículo encontró que el siguiente patrón es mucho, mucho más rápido que todas las demás alternativas, al menos en el esquema simple que probaron. Esta solución fue 373 veces más rápida (!!!) que la PERCENTILE_CONTsolución más lenta ( ) probada. Tenga en cuenta que este truco requiere dos consultas separadas que pueden no ser prácticas en todos los casos. También requiere SQL 2012 o 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;

Por supuesto, solo porque una prueba en un esquema en 2012 arrojó excelentes resultados, su kilometraje puede variar, especialmente si está en SQL Server 2014 o posterior. Si el rendimiento es importante para su cálculo medio, le sugiero encarecidamente que pruebe y pruebe varias de las opciones recomendadas en ese artículo para asegurarse de que ha encontrado la mejor para su esquema.

También sería especialmente cuidadoso al usar la función (nueva en SQL Server 2012) PERCENTILE_CONTque se recomienda en una de las otras respuestas a esta pregunta, porque el artículo vinculado anteriormente encontró que esta función incorporada es 373 veces más lenta que la solución más rápida. Es posible que esta disparidad haya mejorado en los últimos 7 años, pero personalmente no usaría esta función en una mesa grande hasta que verifique su rendimiento en comparación con otras soluciones.

EL POST ORIGINAL 2009 ES ABAJO:

Hay muchas formas de hacerlo, con un rendimiento que varía drásticamente. Aquí hay una solución particularmente bien optimizada, de Medianas, ROW_NUMBERs y rendimiento . Esta es una solución particularmente óptima cuando se trata de E / S reales generadas durante la ejecución: parece más costoso que otras soluciones, pero en realidad es mucho más rápido.

Esa página también contiene una discusión de otras soluciones y detalles de pruebas de rendimiento. Tenga en cuenta el uso de una columna única como desambigador en caso de que haya varias filas con el mismo valor de la columna mediana.

Al igual que con todos los escenarios de rendimiento de la base de datos, siempre intente probar una solución con datos reales en hardware real; nunca se sabe cuándo un cambio en el optimizador de SQL Server o una peculiaridad en su entorno hará que una solución normalmente más lenta sea más lenta.

SELECT
   CustomerId,
   AVG(TotalDue)
FROM
(
   SELECT
      CustomerId,
      TotalDue,
      -- SalesOrderId in the ORDER BY is a disambiguator to break ties
      ROW_NUMBER() OVER (
         PARTITION BY CustomerId
         ORDER BY TotalDue ASC, SalesOrderId ASC) AS RowAsc,
      ROW_NUMBER() OVER (
         PARTITION BY CustomerId
         ORDER BY TotalDue DESC, SalesOrderId DESC) AS RowDesc
   FROM Sales.SalesOrderHeader SOH
) x
WHERE
   RowAsc IN (RowDesc, RowDesc - 1, RowDesc + 1)
GROUP BY CustomerId
ORDER BY CustomerId;
Justin Grant
fuente
12
No creo que esto funcione si tienes engaños, particularmente muchos engaños, en tus datos. No puede garantizar que los row_numbers se alineen. Puede obtener algunas respuestas realmente locas para su mediana, o peor aún, ninguna mediana.
Jonathan Beerhalter
26
Es por eso que tener un desambigador (SalesOrderId en el ejemplo de código anterior) es importante, por lo que puede asegurarse de que el orden de las filas del conjunto de resultados sea consistente tanto hacia atrás como hacia adelante. A menudo, una clave primaria única es un desambigador ideal porque está disponible sin una búsqueda de índice separada. Si no hay una columna de desambiguación disponible (por ejemplo, si la tabla no tiene una clave que no sea clara), entonces se debe usar otro enfoque para calcular la mediana, porque como usted señala correctamente, si no puede garantizar que los números de fila DESC sean imágenes especulares de Números de fila ASC, luego los resultados son impredecibles.
Justin Grant
44
Gracias, al cambiar las columnas a mi base de datos, dejé caer el desambigador, pensando que no era relevante. En ese caso, esta solución funciona realmente muy bien.
Jonathan Beerhalter
8
Sugiero agregar un comentario al código mismo, describiendo la necesidad del desambigador.
hoffmanc
44
¡Increíble! Hace tiempo que conozco su importancia, pero ahora puedo darle un nombre ... ¡el desambigador! ¡Gracias Justin!
CodeMonkey
204

Si está utilizando SQL 2005 o mejor, este es un buen cálculo de mediana simple para una sola columna en una tabla:

SELECT
(
 (SELECT MAX(Score) FROM
   (SELECT TOP 50 PERCENT Score FROM Posts ORDER BY Score) AS BottomHalf)
 +
 (SELECT MIN(Score) FROM
   (SELECT TOP 50 PERCENT Score FROM Posts ORDER BY Score DESC) AS TopHalf)
) / 2 AS Median
Jeff Atwood
fuente
62
Eso es inteligente y relativamente simple dado que no existe una función agregada Median (). ¿Pero cómo es que no existe la función Median ()? Soy un poco FLOOR () ed, francamente.
Charlie Kilian el
Bueno, agradable y simple, pero generalmente necesitas una mediana por categoría de grupo determinado, es decir, como select gid, median(score) from T group by gid. ¿Necesita una subconsulta correlacionada para eso?
TMS
1
... Quiero decir, como en este caso (la segunda consulta denominada "Usuarios con la puntuación de respuesta media más alta").
TMS
Tomás: ¿lograste resolver tu problema "por categoría de grupo"? Como tengo el mismo problema. Gracias.
Stu Harper
3
¿Cómo usar esta solución con GROUP BY?
Przemyslaw Remin
82

En SQL Server 2012 debe 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

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

Simon_Weaver
fuente
12
Este análisis experto presenta un argumento convincente contra las funciones PERCENTILE debido al bajo rendimiento. sqlperformance.com/2012/08/t-sql-queries/median
carl.anderson
44
¿No necesitas agregar un DISTINCTo GROUPY BY SalesOrderID? De lo contrario, tendrá muchas filas duplicadas.
Konstantin
1
esta es la respuesta. no sé por qué tuve que desplazarme tan lejos
FistOfFury 05 de
También hay una versión discreta usandoPERCENTILE_DISC
johnDanger
enfatizando el punto anterior de @carl.anderson: se midió que una solución PERCENTILE_CONT era 373 veces más lenta (!!!!) en comparación con la solución más rápida que probaron en SQL Server 2012 en su esquema de prueba particular. Lea el artículo que carl enlazó para obtener más detalles.
Justin Grant
21

Mi respuesta rápida original fue:

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

Esto le dará la mediana y el rango intercuartil de una sola vez. Si realmente solo quieres una fila que sea la mediana, descomenta la cláusula where.

Cuando inserta eso en un plan de explicación, el 60% del trabajo está ordenando los datos que son inevitables al calcular estadísticas dependientes de la posición como esta.

Modifiqué la respuesta para seguir la excelente sugerencia de Robert Ševčík-Robajz en los comentarios a continuación:

;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

Esto debería calcular los valores correctos de mediana y percentil cuando tiene un número par de elementos de datos. Nuevamente, elimine el comentario de la cláusula where final si solo desea la mediana y no toda la distribución del percentil.

Sir Wobin
fuente
1
En realidad, esto funciona bastante bien y permite la partición de los datos.
Jonathan Beerhalter
3
Si está bien estar apagado por uno, entonces la consulta anterior está bien. Pero si necesita la mediana exacta, entonces tendrá problemas. Por ejemplo, para la secuencia (1,3,5,7) la mediana es 4 pero la consulta anterior devuelve 3. Para (1,2,3,503,603,703) la mediana es 258 pero la consulta anterior devuelve 503.
Justin Grant
1
Podrías solucionar el defecto de imprecisión tomando max y min de cada cuartil en una subconsulta, y luego promediando el MAX del anterior y el MIN del siguiente.
Rbjz
18

Aun mejor:

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

¡Del maestro mismo, Itzik Ben-Gan !

l --''''''--------- '' '' '' '' '' ''
fuente
4

Simple, 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)  
Tobbi
fuente
4

Si desea utilizar la función Crear agregado en SQL Server, así es cómo hacerlo. Hacerlo de esta manera tiene la ventaja de poder escribir consultas limpias. Tenga en cuenta que este proceso podría adaptarse para calcular un valor de percentil con bastante facilidad.

Cree un nuevo proyecto de Visual Studio y establezca el marco de destino en .NET 3.5 (esto es para SQL 2008, puede ser diferente en SQL 2012). Luego cree un archivo de clase y ponga el siguiente código, o equivalente en 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

Luego compílelo y copie el archivo DLL y PDB en su máquina SQL Server y ejecute el siguiente comando en 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

Luego puede escribir una consulta para calcular la mediana de esta manera: SELECCIONE dbo.Median (Field) FROM Table

Rono
fuente
3

Acabo de encontrar esta página mientras buscaba una solución basada en un conjunto para la mediana. Después de ver algunas de las soluciones aquí, se me ocurrió lo siguiente. La esperanza es ayuda / 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
brian
fuente
3

La siguiente consulta devuelve la mediana de una lista de valores en una columna. No se puede usar como o junto con una función agregada, pero aún se puede usar como una subconsulta con una cláusula WHERE en la selección 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
PyQL
fuente
3

Aunque la solución de Justin Grant parece sólida, descubrí que cuando tiene varios valores duplicados dentro de una clave de partición dada, los números de fila para los valores duplicados de ASC terminan fuera de secuencia, por lo que no se alinean correctamente.

Aquí hay un fragmento de mi 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

Usé el código de Justin como base para esta solución. Aunque no es tan eficiente dado el uso de múltiples tablas derivadas, resuelve el problema de ordenamiento de filas que encontré. Cualquier mejora sería bienvenida ya que no tengo mucha experiencia en T-SQL.

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

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

El ejemplo de Justin anterior es muy bueno. Pero esa necesidad de la clave primaria debe establecerse muy claramente. He visto ese código en la naturaleza sin la clave y los resultados son malos.

La queja que recibo sobre Percentile_Cont es que no le dará un valor real del conjunto de datos. Para llegar a una "mediana" que sea un valor real del conjunto de datos, utilice Percentile_Disc.

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

En un UDF, escriba:

 Select Top 1 medianSortColumn from Table T
  Where (Select Count(*) from Table
         Where MedianSortColumn <
           (Select Count(*) From Table) / 2)
  Order By medianSortColumn
Charles Bretana
fuente
77
En caso de un número par de elementos, la mediana es el promedio de los dos elementos intermedios, que no está cubierto por este UDF.
Yaakov Ellis
1
¿Puedes reescribirlo en todo el UDF?
Przemyslaw Remin
2

Hallazgo mediano

Este es el método más simple para encontrar la mediana de un 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)
Nivesh Krishna
fuente
¿Cómo manejará el caso cuando el número de filas sea par?
priojeet priyom
1

Para una variable / medida continua 'col1' de 'table1'

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

Usando el agregado COUNT, primero puede contar cuántas filas hay y almacenar en una variable llamada @cnt. Luego, puede calcular los parámetros para que el filtro OFFSET-FETCH especifique, en función del orden de la cantidad, cuántas filas omitir (valor de compensación) y cuántas filtrar (valor de recuperación).

El número de filas a omitir es (@cnt - 1) / 2. Está claro que para un recuento impar este cálculo es correcto porque primero resta 1 para el valor medio único, antes de dividir por 2.

Esto también funciona correctamente para un recuento par porque la división utilizada en la expresión es división entera; entonces, al restar 1 de un conteo par, te quedas con un valor impar.

Al dividir ese valor impar entre 2, la parte de fracción del resultado (.5) se trunca. El número de filas para recuperar es 2 - (@cnt% 2). La idea es que cuando el recuento es impar, el resultado de la operación del módulo es 1, y debe obtener 1 fila. Cuando el recuento es par, el resultado de la operación del módulo es 0, y debe buscar 2 filas. Al restar el resultado 1 o 0 de la operación de módulo de 2, obtiene el 1 o 2 deseado, respectivamente. Finalmente, para calcular la cantidad mediana, tome las cantidades de uno o dos resultados y aplique un promedio después de convertir el valor entero de entrada a uno numérico de la siguiente manera:

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

Quería encontrar una solución por mí mismo, pero mi cerebro tropezó y cayó en el camino. Yo creo que funciona, pero no me pidas que explicarlo por la mañana. :PAGS

DECLARE @table AS TABLE
(
    Number int not null
);

insert into @table select 2;
insert into @table select 4;
insert into @table select 9;
insert into @table select 15;
insert into @table select 22;
insert into @table select 26;
insert into @table select 37;
insert into @table select 49;

DECLARE @Count AS INT
SELECT @Count = COUNT(*) FROM @table;

WITH MyResults(RowNo, Number) AS
(
    SELECT RowNo, Number FROM
        (SELECT ROW_NUMBER() OVER (ORDER BY Number) AS RowNo, Number FROM @table) AS Foo
)
SELECT AVG(Number) FROM MyResults WHERE RowNo = (@Count+1)/2 OR RowNo = ((@Count+1)%2) * ((@Count+2)/2)
Gavin
fuente
0
--Create Temp Table to Store Results in
DECLARE @results AS TABLE 
(
    [Month] datetime not null
 ,[Median] int not null
);

--This variable will determine the date
DECLARE @IntDate as int 
set @IntDate = -13


WHILE (@IntDate < 0) 
BEGIN

--Create Temp Table
DECLARE @table AS TABLE 
(
    [Rank] int not null
 ,[Days Open] int not null
);

--Insert records into Temp Table
insert into @table 

SELECT 
    rank() OVER (ORDER BY DATEADD(mm, DATEDIFF(mm, 0, DATEADD(ss, SVR.close_date, '1970')), 0), DATEDIFF(day,DATEADD(ss, SVR.open_date, '1970'),DATEADD(ss, SVR.close_date, '1970')),[SVR].[ref_num]) as [Rank]
 ,DATEDIFF(day,DATEADD(ss, SVR.open_date, '1970'),DATEADD(ss, SVR.close_date, '1970')) as [Days Open]
FROM
 mdbrpt.dbo.View_Request SVR
 LEFT OUTER JOIN dbo.dtv_apps_systems vapp 
 on SVR.category = vapp.persid
 LEFT OUTER JOIN dbo.prob_ctg pctg 
 on SVR.category = pctg.persid
 Left Outer Join [mdbrpt].[dbo].[rootcause] as [Root Cause] 
 on [SVR].[rootcause]=[Root Cause].[id]
 Left Outer Join [mdbrpt].[dbo].[cr_stat] as [Status]
 on [SVR].[status]=[Status].[code]
 LEFT OUTER JOIN [mdbrpt].[dbo].[net_res] as [net] 
 on [net].[id]=SVR.[affected_rc]
WHERE
 SVR.Type IN ('P') 
 AND
 SVR.close_date IS NOT NULL 
 AND
 [Status].[SYM] = 'Closed'
 AND
 SVR.parent is null
 AND
 [Root Cause].[sym] in ( 'RC - Application','RC - Hardware', 'RC - Operational', 'RC - Unknown')
 AND
 (
  [vapp].[appl_name] in ('3PI','Billing Rpts/Files','Collabrent','Reports','STMS','STMS 2','Telco','Comergent','OOM','C3-BAU','C3-DD','DIRECTV','DIRECTV Sales','DIRECTV Self Care','Dealer Website','EI Servlet','Enterprise Integration','ET','ICAN','ODS','SB-SCM','SeeBeyond','Digital Dashboard','IVR','OMS','Order Services','Retail Services','OSCAR','SAP','CTI','RIO','RIO Call Center','RIO Field Services','FSS-RIO3','TAOS','TCS')
 OR
  pctg.sym in ('Systems.Release Health Dashboard.Problem','DTV QA Test.Enterprise Release.Deferred Defect Log')
 AND  
  [Net].[nr_desc] in ('3PI','Billing Rpts/Files','Collabrent','Reports','STMS','STMS 2','Telco','Comergent','OOM','C3-BAU','C3-DD','DIRECTV','DIRECTV Sales','DIRECTV Self Care','Dealer Website','EI Servlet','Enterprise Integration','ET','ICAN','ODS','SB-SCM','SeeBeyond','Digital Dashboard','IVR','OMS','Order Services','Retail Services','OSCAR','SAP','CTI','RIO','RIO Call Center','RIO Field Services','FSS-RIO3','TAOS','TCS')
 )
 AND
 DATEADD(mm, DATEDIFF(mm, 0, DATEADD(ss, SVR.close_date, '1970')), 0) = DATEADD(mm, DATEDIFF(mm,0,DATEADD(mm,@IntDate,getdate())), 0)
ORDER BY [Days Open]



DECLARE @Count AS INT
SELECT @Count = COUNT(*) FROM @table;

WITH MyResults(RowNo, [Days Open]) AS
(
    SELECT RowNo, [Days Open] FROM
        (SELECT ROW_NUMBER() OVER (ORDER BY [Days Open]) AS RowNo, [Days Open] FROM @table) AS Foo
)


insert into @results
SELECT 
 DATEADD(mm, DATEDIFF(mm,0,DATEADD(mm,@IntDate,getdate())), 0) as [Month]
 ,AVG([Days Open])as [Median] FROM MyResults WHERE RowNo = (@Count+1)/2 OR RowNo = ((@Count+1)%2) * ((@Count+2)/2) 


set @IntDate = @IntDate+1
DELETE FROM @table
END

select *
from @results
order by [Month]
Gregg Silverman
fuente
0

Esto funciona 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

)
SQLMason
fuente
0

Para los novatos como yo que están aprendiendo los conceptos básicos, personalmente considero que este ejemplo es más fácil de seguir, ya que es más fácil entender exactamente qué está sucediendo y de dónde provienen los valores medios ...

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

Sin embargo, en absoluto asombro de algunos de los códigos anteriores.

Justine
fuente
0

Esta es una respuesta tan simple como se me ocurrió. Funcionó bien con mis datos. Si desea excluir ciertos valores, simplemente agregue una cláusula where a la selección 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
John P.
fuente
0

La siguiente solución funciona bajo estos supuestos:

  • No hay valores duplicados.
  • No 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) ; 
Maria Ines Parnisari
fuente
0
DECLARE @Obs int
DECLARE @RowAsc table
(
ID      INT IDENTITY,
Observation  FLOAT
)
INSERT INTO @RowAsc
SELECT Observations FROM MyTable
ORDER BY 1 
SELECT @Obs=COUNT(*)/2 FROM @RowAsc
SELECT Observation AS Median FROM @RowAsc WHERE ID=@Obs
Arie Yehieli
fuente
0

Intento con varias alternativas, pero debido a que mis registros de datos tienen valores repetidos, las versiones ROW_NUMBER parecen no ser una opción para mí. Entonces, aquí la consulta que utilicé (una versión 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;
Galled
fuente
0

Sobre la base de la respuesta de Jeff Atwood aquí arriba, es con GROUP BY y una subconsulta correlacionada para obtener la 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
Jim B
fuente
0

Con frecuencia, es posible que necesitemos calcular la mediana no solo para toda la tabla, sino también para los agregados con respecto a alguna ID. En otras palabras, calcule la mediana de cada ID en nuestra tabla, donde cada ID tiene muchos registros. (basado en la solución editada por @gdoron: buen rendimiento y funciona en muchos 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 eso ayude.

Danylo Zherebetskyy
fuente
0

Para su pregunta, Jeff Atwood ya había dado la solución simple y efectiva. Pero, si está buscando un enfoque alternativo para calcular la mediana, el siguiente código SQL lo ayudará.

create table employees(salary int);

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

select * from employees;

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


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


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

Si está buscando calcular la mediana en MySQL, este enlace github será útil.

Veeramani Natarajan
fuente
0

Esta es la solución más óptima para encontrar medianas que se me ocurra. Los nombres en el ejemplo se basan en el ejemplo de Justin. Asegúrese de que exista un índice para la tabla Sales.SalesOrderHeader con las columnas de índice CustomerId y TotalDue en ese orden.

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

ACTUALIZAR

No estaba seguro de qué método tiene el mejor rendimiento, así que hice una comparación entre mi método Justin Grants y Jeff Atwoods ejecutando una consulta basada en los tres métodos en un lote y el costo del lote de cada consulta fue:

Sin índice:

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

Y con indice

  • Mina 3%.
  • Justin otorga un 10%
  • Jeff Atwoods 87%

Traté de ver qué tan bien se escalan las consultas si tiene un índice creando más datos de alrededor de 14 000 filas por un factor de 2 a 512, lo que significa al final alrededor de 7,2 millones de filas. Tenga en cuenta que me aseguré de que el campo CustomeId fuera único para cada vez que hice una sola copia, por lo que la proporción de filas en comparación con la instancia única de CustomerId se mantuvo constante. Mientras hacía esto, ejecuté ejecuciones donde reconstruí el índice después, y noté que los resultados se estabilizaron en un factor de alrededor de 128 con los datos que tenía para estos valores:

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

Me preguntaba cómo podría haber afectado el rendimiento al escalar el número de filas pero manteniendo constante el CustomerId único, así que configuré una nueva prueba donde hice exactamente esto. Ahora, en lugar de estabilizarse, la relación de costo del lote se mantuvo divergente, también en lugar de aproximadamente 20 filas por CustomerId por promedio que tenía al final alrededor de 10000 filas por dicho ID único. Los números donde:

  • Mina 4%
  • Justin 60%
  • Jeffs 35%

Me aseguré de implementar correctamente cada método comparando los resultados. Mi conclusión es que el método que utilicé es generalmente más rápido siempre que exista un índice. También noté que este método es lo que se recomienda para este problema en particular en este artículo https://www.microsoftpressstore.com/articles/article.aspx?p=2314819&seqNum=5

Una forma de mejorar aún más el rendimiento de las llamadas posteriores a esta consulta es persistir la información de conteo en una tabla auxiliar. Incluso podría mantenerlo al tener un activador que se actualiza y contiene información sobre el recuento de filas SalesOrderHeader que dependen de CustomerId, por supuesto, también puede almacenar la mediana también.

Kaveh Hadjari
fuente
0

Para conjuntos de datos a gran escala, puede probar este GIST:

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

Funciona agregando los distintos valores que encontraría en su conjunto (como las edades o el año de nacimiento, etc.), y utiliza las funciones de la ventana SQL para localizar cualquier posición de percentil que especifique en la consulta.

Chris Knoll
fuente