Функция для вычисления медианы в SQL Server - PullRequest
202 голосов
/ 27 августа 2009

Согласно MSDN , Медиана недоступна как агрегатная функция в Transact-SQL. Однако я хотел бы узнать, возможно ли создать эту функцию (используя функцию Create Aggregate , пользовательскую функцию или какой-либо другой метод).

Каков наилучший способ (если это возможно) сделать это - разрешить вычисление медианного значения (принимая числовой тип данных) в агрегированном запросе?

Ответы [ 29 ]

185 голосов
/ 08 января 2010

Если вы используете 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
125 голосов
/ 14 октября 2009

Есть много способов сделать это с резко меняющейся производительностью. Вот одно особенно хорошо оптимизированное решение: Медианы, ROW_NUMBERs и производительность . Это особенно оптимальное решение, когда речь идет о фактических операциях ввода-вывода, сгенерированных во время выполнения, - это выглядит дороже, чем другие решения, но на самом деле это намного быстрее.

На этой странице также обсуждаются другие решения и детали тестирования производительности. Обратите внимание на использование уникального столбца в качестве устранения неоднозначности, если имеется несколько строк с одинаковым значением медианного столбца.

Как и во всех сценариях производительности базы данных, всегда пытайтесь протестировать решение с реальными данными на реальном оборудовании - вы никогда не знаете, когда изменение в оптимизаторе SQL Server или какая-либо особенность в вашей среде замедлит обычно быстрое решение. 1009 *

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;
75 голосов
/ 13 января 2012

В 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/

21 голосов
/ 24 июня 2010

Мой оригинальный быстрый ответ был:

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, если вы хотите получить только медиану, а не все процентили.

18 голосов
/ 31 октября 2012

Еще лучше:

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

От самого мастера, Ицик Бен-Ган !

8 голосов
/ 25 апреля 2016

MS SQL Server 2012 (и более поздние версии) имеет функцию PERCENTILE_DISC, которая вычисляет определенный процентиль для отсортированных значений. PERCENTILE_DISC (0.5) вычислит медиану - https://msdn.microsoft.com/en-us/library/hh231327.aspx

4 голосов
/ 08 мая 2012

Простой, быстрый, точный

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 голосов
/ 23 мая 2013

Если вы хотите использовать функцию «Создать агрегат» в SQL Server, это то, как это сделать. Делая это таким образом, вы получаете возможность писать чистые запросы. Обратите внимание, что этот процесс может быть легко адаптирован для вычисления значения процентиля.

Создайте новый проект Visual Studio и установите целевую платформу на .NET 3.5 (это для SQL 2008, может отличаться в SQL 2012). Затем создайте файл класса и вставьте следующий код или эквивалент 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

Затем скомпилируйте его и скопируйте файл 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

Затем вы можете написать запрос для вычисления медианы следующим образом: ВЫБЕРИТЕ dbo.Median (Поле) ИЗ таблицы

3 голосов
/ 24 мая 2011

Я только что наткнулся на эту страницу, когда искал решение на основе множеств для медианы. Посмотрев некоторые решения здесь, я пришел к следующему. Надежда помогает / работает.

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 голосов
/ 06 апреля 2011

Хотя решение Джастина Гранта выглядит надежным, я обнаружил, что если в заданном ключе раздела есть несколько повторяющихся значений, номера строк для повторяющихся значений 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

Я использовал код Джастина в качестве основы для этого решения. Хотя это и не так эффективно, учитывая использование нескольких производных таблиц, это решает проблему с порядком строк, с которой я столкнулся. Любые улучшения приветствуются, поскольку я не настолько опытен в 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
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...