2009-08-27 41 views
163

De acuerdo con 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 (utilizando la función Create Aggregate, la función definida por el usuario o algún otro método).Función para calcular la mediana en SQL Server

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

+7

http://www.sqlperformance.com/2012/08/t-sql-queries/median –

Respuesta

103

Hay un montón de maneras de hacer esto, con un rendimiento que varía dramáticamente. Aquí hay una solución particularmente bien optimizada, desde Medians, ROW_NUMBERs, and performance. Esta es una solución particularmente óptima cuando se trata de E/S reales generadas durante la ejecución: parece más costosa que otras soluciones, pero en realidad es mucho más rápida.

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 desambiguador en caso de que haya múltiples filas con el mismo valor de la columna mediana.

Al igual que con todos los escenarios de rendimiento de base de datos, siempre trate de probar una solución a cabo con los datos reales sobre bienes hardware-- nunca se sabe cuando un cambio en optimizador de SQL Server o una peculiaridad en su entorno harán normalmente una solución rápida-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; 
+7

No creo que esto funcione si tienes engañados, particularmente muchos engañados, en tus datos. No puede garantizar que los row_numbers se alineen. Puede obtener respuestas realmente locas para su mediana, o peor, ninguna mediana en absoluto. –

+23

Es por eso que tener una desambiguación (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 uniforme tanto hacia atrás como hacia delante. A menudo, una clave primaria única hace una desambigadora ideal porque está disponible sin una búsqueda de índice por separado. Si no hay una columna de desambiguación disponible (por ejemplo, si la tabla no tiene una clave uniquifying), 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 espejo de Números de filas de ASC, los resultados son impredecibles. –

+2

Gracias, al cambiar las columnas a mi base de datos, dejé caer el desambiguador, pensando que no era relevante. En ese caso, esta solución funciona realmente bien. –

1

En una UDF, escribe:

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

En el caso de un número par de elementos, la mediana es el promedio de los dos elementos del medio, que no está cubierto por esta UDF. –

+1

¿Puedes reescribirlo en todo el UDF? –

1

Ver otras soluciones para el cálculo de la mediana en SQL aquí: "Simple way to calculate median with MySQL" (las soluciones son en su mayoría independiente del proveedor).

+0

Marcado como CW, por lo que no obtengo puntos solo por vincular. :) –

0

Quería encontrar una solución yo solo, pero mi cerebro se tropezó y se cayó en el camino. Creo que funciona, pero no me pidas que lo explique por la mañana. : 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] 
146

Si está utilizando SQL 2005 o mejor este es un bonito, sencillo y tantos cálculo mediana 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 
+33

Eso es inteligente y relativamente simple dado que no existe ninguna función agregada Median(). ¿Pero cómo es que no existe la función Median()? Estoy un poco PISO() ed, francamente. –

+0

Bueno, agradable y simple, pero por lo general se necesita una mediana para cada categoría de grupo, es decir, como 'seleccionar gid, mediana (puntuación) del grupo T por gid'. ¿Necesitas una subconsulta correlacionada para eso? – TMS

+1

... Me refiero a como en [este caso] (http://meta.stackexchange.com/a/186808/166308) (la segunda consulta denominada "Usuarios con puntaje de respuesta medio más alto"). – TMS

20

Mi respuesta rápida original:

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 intercuartílico de una sola vez. Si realmente solo quieres una fila que sea la mediana, entonces quita el comentario de la cláusula where.

Cuando pega eso en un plan de explicación, el 60% del trabajo consiste en ordenar los datos que son inevitables al calcular las estadísticas dependientes de la posición como esta.

he modificado 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 debe calcular la mediana correcta y valores del percentil cuando se 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 de percentiles.

+1

Esto realmente funciona bastante bien y permite la partición de los datos. –

+3

Si está bien desconectarse en uno, 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. –

+1

Puede corregir el defecto de la imprecisión tomando max y min de cada cuartil en una subconsulta, luego AVGing el MAX del anterior y MIN del siguiente? – Rbjz

1

Aunque la solución de Justin Grant parece sólida, descubrí que cuando tienes una cantidad de valores duplicados dentro de una clave de partición determinada los números de fila para los valores duplicados ASC terminan fuera de secuencia para que no se alineen correctamente.

Aquí es 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 

que utiliza código de Justin como base para esta solución. Aunque no es tan eficiente dado el uso de tablas derivadas múltiples, resuelve el problema de ordenamiento de filas que encontré. Cualquier mejora sería bienvenida ya que no soy tan experimentado 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 
3

Acabo de encontrar esta página mientras buscaba una solución basada en 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 
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 

) 
62

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

+10

Este análisis experto presenta un argumento convincente contra las funciones PERCENTILE debido a un rendimiento deficiente. http://sqlperformance.com/2012/08/t-sql-queries/median –

+0

¿No necesita agregar un 'DISTINCT' o' GROUPY BY SalesOrderID'?De lo contrario, tendrás muchas filas duplicadas. – Konstantin

3

simple, rápida, precisa

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

Aún 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); 

Desde el Maestro mismo, Itzik Ben-Gan!

2

Si desea utilizar la función Create Aggregate en SQL Server, esta es la forma de hacerlo. Hacerlo de esta manera tiene la ventaja de poder escribir consultas claras. Tenga en cuenta que este proceso podría adaptarse para calcular un valor 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). A continuación, cree un archivo de clase y poner en el código siguiente, o el equivalente 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)))/[email protected] 
     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 

entonces compilar y copiar el archivo DLL y el archivo PDB a su equipo de SQL Server y ejecute el comando siguiente 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 

a continuación, puede escribir una consulta para calcular la mediana de la siguiente manera: SELECT dbo.Median (campo) de la tabla

0

para los novatos como yo, que están aprendiendo los conceptos básicos, yo personalmente considero este ejemplo más fácil de seguir , ya que es más fácil de entender exactamente lo que está sucediendo y donde los valores medios están viniendo de ...

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 

En el temor absoluto de algunos de los códigos anteriores, aunque !!!

0

Esta es una respuesta tan simple como podría haberlo hecho. 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 
2

El ejemplo anterior de Justin 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 el Percentile_Cont es que no le dará un valor real del conjunto de datos. Para obtener una "mediana" que sea un valor real del conjunto de datos, 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 
0

la siguiente solución funciona bajo estos supuestos:

  • Sin 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) ; 
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 [email protected] 
0

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

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

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

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í. Así que aquí la consulta 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; 
0

Sobre la base de la respuesta de Jeff Atwood por encima de aquí es con GROUP BY y una consulta correlacionada para obtener la mediana para 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 
2

La consulta siguiente devuelve mediana de una lista de valores en una columna. No se puede usar como una función agregada o junto con ella, pero puede usarla como una sub consulta 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 
1

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

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 
0

frecuentes, es posible que tengamos para calcular La mediana no sólo para el toda la tabla, pero para los agregados con respecto a algunos ID. En otras palabras, calcule la mediana para cada ID en nuestra tabla, donde cada ID tiene muchos registros.(Basado en la solución editado por @gdoron: buen rendimiento y trabaja 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 que ayude.

0

Para su pregunta, Jeff Atwood ya había dado la solución simple y efectiva. Pero, si está buscando algún método 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 [email protected]_no or [email protected][email protected]_even;

Si usted está buscando para el cálculo de la mediana en MySQL, este github link será útil.

0

Esta es la solución más óptima para encontrar medianas que pueda imaginar. 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 

ACTUALIZACIÓN

estaba un poco inseguro sobre qué método tiene mejor rendimiento, por lo que hizo una comparación entre mi método Justin Subvenciones y Jeff Atwoods mediante la ejecución de consulta basada en los tres métodos en un lote y el costo lote de cada consulta fueron:

Sin índice:

  • Mine 30%
  • Justin Subvenciones 13%
  • Jeff Atwoods 58%

Y con índice de

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

me trató de ver lo bien que escalar las consultas si tiene índice mediante la creación de más datos de alrededor de 14 000 filas por un factor de 2 hasta 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 única 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 me encontré con las ejecuciones en el que reconstruyó el índice después, y me di cuenta de los resultados se estabilizó en torno a un factor de 128 con los datos que tenía que estos valores:

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

Me preguntaba cómo el rendimiento podría haber sido afectada por la ampliación número de filas, pero manteniendo constante CustomerId único, por lo que la configuración que una nueva prueba donde hice sólo esta. Ahora, en lugar de estabilizar, la relación del costo del lote se mantuvo divergente, también en lugar de alrededor de 20 filas por ID de cliente por promedio, tuve al final alrededor de 10000 filas por cada identificador único.Los números donde:

  • Mine 4%
  • Justins 60%
  • Jeffs 35%

I se aseguró de que implementa cada método correcto mediante la comparación de los resultados. Mi conclusión es que el método que utilicé generalmente es más rápido mientras exista el índice. También se dio cuenta de que este método es el recomendado para este problema en particular en este artículo https://www.microsoftpressstore.com/articles/article.aspx?p=2314819&seqNum=5

Una manera de mejorar aún más el rendimiento de las llamadas posteriores a esta consulta es mantener la información de conteo en una tabla auxiliar. Incluso podría mantenerlo activando un desencadenador que actualice y contenga información sobre el recuento de las filas de SalesOrderHeader dependientes de CustomerId; por supuesto, también puede almacenar la mediana fácilmente.

Cuestiones relacionadas