Функция для вычисления медианы в SQL Server
В соответствии с MSDN Медиана недоступна как агрегированная функция в Transact-SQL. Тем не менее, я хотел бы узнать, можно ли создать эту функциональность (используя Create Aggregate, функцию, определенную пользователем, или некоторые другие метод).
Каким будет лучший способ (если возможно) сделать это - разрешить вычисление медианного значения (предполагая числовой тип данных) в сводном запросе?
Ответы
Ответ 1
Существует множество способов сделать это с резко отличающейся производительностью. Здесь одно особенно хорошо оптимизированное решение, от медианы, ROW_NUMBER и производительность. Это особенно оптимальное решение, когда дело доходит до фактических операций ввода-вывода, генерируемых во время исполнения - оно выглядит более дорогостоящим, чем другие решения, но на самом деле оно намного быстрее.
Эта страница также содержит информацию о других решениях и деталях тестирования производительности. Обратите внимание на использование уникального столбца в качестве дизассемблера в случае, если имеется несколько строк с одинаковым значением медианного столбца.
Как и во всех сценариях производительности базы данных, всегда старайтесь протестировать решение с реальными данными на реальном оборудовании - вы никогда не знаете, когда изменение оптимизатора SQL Server или особенность в вашей среде сделает медленное решение с нормальной скоростью медленнее.
SELECT
CustomerId,
AVG(TotalDue)
FROM
(
SELECT
CustomerId,
TotalDue,
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;
Ответ 2
Если вы используете SQL 2005 или выше, это хороший, простой и средний расчет для одного столбца в таблице:
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
Ответ 3
В SQL Server 2012 вы должны использовать 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
Смотрите также: http://blog.sqlauthority.com/2011/11/20/sql-server-introduction-to-percentile_cont-analytic-functions-introduced-in-sql-server-2012/
Ответ 4
Мой первоначальный быстрый ответ:
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
Это даст вам медианный и межквартильный диапазон одним махом. Если вам действительно нужна только одна строка, которая является срединной, тогда раскомментируйте предложение where.
Когда вы вставляете это в план объяснения, 60% работы сортирует данные, которые неизбежны при вычислении статистики, зависящей от позиции, как это.
Я изменил ответ, чтобы следовать отличному предложению Роберта Шевчика-Робаза в комментариях ниже:
;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
Это должно подсчитать правильные значения медианы и процентиля, если у вас есть четное количество элементов данных. Опять же, раскомментируйте окончательное предложение where, если вы хотите только медианное, а не полное распределение процентили.
Ответ 5
Еще лучше:
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);
От самого мастера, Ицик Бен-Ган!
Ответ 6
MS SQL Server 2012 (и позже) имеет функцию PERCENTILE_DISC, которая вычисляет определенный процентиль для отсортированных значений. PERCENTILE_DISC (0.5) вычислит медиану - https://msdn.microsoft.com/en-us/library/hh231327.aspx
Ответ 7
Я просто наткнулся на эту страницу, ища решение на основе набора для медиана. Посмотрев на некоторые из решений здесь, я придумал следующее. Надежда помогает/работает.
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
Ответ 8
Простой, быстрый, точный
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)
Ответ 9
Если вы хотите использовать функцию Create Aggregate в SQL Server, вот как это сделать. Выполнение этого способа имеет преимущество, заключающееся в возможности писать чистые запросы. Обратите внимание, что этот процесс может быть адаптирован для простого вычисления значения Percentile.
Создайте новый проект Visual Studio и настройте целевую структуру на .NET 3.5 (это для SQL 2008, это может быть иначе в SQL 2012). Затем создайте файл класса и введите следующий код или эквивалент С#:
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
Затем скомпилируйте его и скопируйте файл DLL и PDB на ваш компьютер SQL Server и выполните следующую команду в 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
Затем вы можете написать запрос для вычисления медианы следующим образом: SELECT dbo.Median(поле) из таблицы
Ответ 10
Приведенный выше пример Justin очень хорош. Но эту Первичную ключевую потребность следует изложить очень четко. Я видел этот код в дикой природе без ключа, и результаты плохие.
Жалоба, которую я получаю о Percentile_Cont, заключается в том, что она не даст вам фактическое значение из набора данных.
Чтобы перейти к "медианной", которая является фактическим значением из набора данных, используйте 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
Ответ 11
Следующий запрос возвращает медианный из списка значений в одном столбце. Он не может использоваться как или вместе с агрегатной функцией, но вы все равно можете использовать его в качестве подзапроса с предложением WHERE во внутреннем выборе.
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
Ответ 12
В UDF напишите:
Select Top 1 medianSortColumn from Table T
Where (Select Count(*) from Table
Where MedianSortColumn <
(Select Count(*) From Table) / 2)
Order By medianSortColumn
Ответ 13
Смотрите другие решения для медианного вычисления в SQL здесь:
"Простой способ вычисления медианного с MySQL" (решения в основном независимы от поставщиков).
Ответ 14
Несмотря на то, что решение Justin grant кажется твердым, я обнаружил, что, когда у вас есть несколько повторяющихся значений в данном ключе раздела, номера строк для дублирующих значений ASC оказываются вне последовательности, поэтому они не выравниваются должным образом.
Вот фрагмент из моего результата:
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
Я использовал код Justin в качестве основы для этого решения. Хотя это не так эффективно при использовании нескольких производных таблиц, оно устраняет проблему упорядочения строк, с которой я столкнулся. Любые улучшения будут приветствоваться, поскольку я не настолько опытен в 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
Ответ 15
Для непрерывной переменной/меры 'col1' из '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
Ответ 16
Я хотел разработать решение самостоятельно, но мой мозг споткнулся и упал в пути. Я думаю, что это работает, но не просите меня объяснить это утром.: 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)
Ответ 17
--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]
Ответ 18
Это работает с 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
)
Ответ 19
Для новичков, таких как я, которые изучают самые основы, я лично считаю, что этот пример легче следовать, поскольку легче понять, что происходит и откуда происходят медианные значения...
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
В абсолютном благоговении некоторых из вышеперечисленных кодов!!!!!!
Ответ 20
Это так же просто ответ, как я мог придумать. Хорошо работал с моими данными. Если вы хотите исключить определенные значения, просто добавьте предложение where во внутренний select.
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
Ответ 21
В этих предположениях используется следующее решение:
- Нет повторяющихся значений
- Нет NULL
код:
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) ;
Ответ 22
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]
Ответ 23
Для массивов больших масштабов вы можете попробовать этот GIST:
https://gist.github.com/chrisknoll/1b38761ce8c5016ec5b2
Он работает путем агрегирования различных значений, которые вы найдете в своем наборе (например, возраста или года рождения и т.д.), и использует функции окна SQL для определения любой позиции процентиля, указанной в запросе.
Ответ 24
Я попробую несколько альтернатив, но из-за того, что в моих записях данных повторяются значения, версии ROW_NUMBER кажутся для меня не выбором. Итак, вот запрос, который я использовал (версия с 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;
Ответ 25
Основываясь на Джеффе Этвуде, ответ на этот вопрос здесь, с GROUP BY и коррелированным подзапросом, чтобы получить медиану для каждой группы.
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
Ответ 26
Часто нам может понадобиться вычислять медиану не только для всей таблицы, но и для агрегатов по отношению к некоторому ID. Другими словами, вычислить медианную для каждого идентификатора в нашей таблице, где каждый идентификатор имеет много записей. (на основе решения, отредактированного @gdoron: хорошая производительность и работает во многих 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;
Надеюсь, что это поможет.
Ответ 27
По вашему вопросу Джефф Этвуд уже дал простое и эффективное решение. Но, если вы ищете альтернативный подход к вычислению медианы, ниже кода SQL поможет вам.
create table employees(salary int);
insert into employees values(8); insert into employees values(23); insert into employees values(45); insert into employees values(123); insert into employees values(93); insert into employees values(2342); insert into employees values(2238);
select * from employees;
declare @odd_even int; declare @cnt int; declare @middle_no int;
set @cnt=(select count(*) from employees); set @middle_no=(@cnt/2)+1; select @odd_even=case when (@cnt%2=0) THEN -1 ELse 0 END ;
select AVG(tbl.salary) from (select salary,ROW_NUMBER() over (order by salary) as rno from employees group by salary) tbl where [email protected]_no or [email protected][email protected]_even;
Ответ 28
Это наиболее оптимальное решение для поиска медиан, о котором я могу думать. Имена в примере основаны на примере Justin. Удостоверьтесь, что индекс для таблицы
Sales.SalesOrderHeader существует с индексными столбцами CustomerId и TotalDue в этом порядке.
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
UPDATE
Я был немного уверен в том, какой метод имеет лучшую производительность, поэтому я сделал сравнение между моим методом Justin Grants и Jeff Atwoods, выполнив запрос на основе всех трех методов в одной партии, а стоимость пакета для каждого запроса:
Без индекса:
- Шахта 30%
- Джастин Грантс 13%
- Джефф Атвудс 58%
И с индексом
- Шахта 3%.
- Джастин Грантс 10%
- Jeff Atwoods 87%
Я попытался понять, насколько хорошо масштабируются запросы, если у вас есть индекс, создавая больше данных из примерно 14 000 строк в 2 раза до 512, что означает, в конце концов, около 7,2 миллионов строк. Заметьте, что я сделал поле CustomeId уникальным для каждого раза, когда я сделал одну копию, поэтому доля строк по сравнению с уникальным экземпляром CustomerId оставалась постоянной. В то время как я делал это, я запускал выполнение, после чего я восстановил индекс, и я заметил, что результаты стабилизировались примерно в 128 раз с данными, которые у меня были для этих значений:
- Шахта 3%.
- Джастин Грантс 5%
- Jeff Atwoods 92%
Я задавался вопросом, как на производительность может повлиять масштабирование числа строк, но постоянная константа CustomerId, поэтому я настраиваю новый тест, где я это сделал. Теперь вместо стабилизации соотношение затрат партии продолжало расходиться, а вместо примерно 20 строк на CustomerId в среднем я имел в конце около 10000 строк на такой уникальный идентификатор. Числа, где:
- Шахта 4%
- Юстины 60%
- Джеффс 35%
Я убедился, что я выполнил каждый метод правильно, сравнив результаты.
Мой вывод - метод, который я использовал, обычно быстрее, пока существует индекс. Также заметил, что этот метод является тем, что рекомендуется для этой конкретной проблемы в этой статье https://www.microsoftpressstore.com/articles/article.aspx?p=2314819&seqNum=5
Еще один способ еще более повысить производительность последующих вызовов этого запроса - это сохранить информацию о счете во вспомогательной таблице. Вы даже можете поддерживать его, имея триггер, который обновляет и хранит информацию о количестве строк SalesOrderHeader, зависящих от CustomerId, конечно же, вы можете просто сохранить медиану, а также.