Функция для вычисления медианы в SQL Server


227

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

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


Ответы:


145

ОБНОВЛЕНИЕ 2019: За 10 лет, с тех пор как я написал этот ответ, было найдено больше решений, которые могут дать лучшие результаты. Кроме того, выпуски SQL Server с тех пор (особенно SQL 2012) представили новые функции T-SQL, которые можно использовать для вычисления медиан. В выпусках SQL Server также улучшен оптимизатор запросов, который может влиять на различные срединные решения. Net-net, мой оригинальный пост за 2009 год все еще в порядке, но могут быть более эффективные решения для современных приложений SQL Server. Взгляните на эту статью 2012 года, которая является отличным ресурсом: https://sqlperformance.com/2012/08/t-sql-queries/median

Эта статья обнаружила, что следующий шаблон намного, намного быстрее, чем все другие альтернативы, по крайней мере, на простой проверенной схеме. Это решение было в 373 раза быстрее (!!!), чем самое медленное ( PERCENTILE_CONT) из протестированных. Обратите внимание, что этот прием требует двух отдельных запросов, которые могут быть не практичными во всех случаях Это также требует SQL 2012 или позже.

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;

Конечно, только из-за того, что один тест на одной схеме в 2012 году дал отличные результаты, пробег может отличаться, особенно если вы используете SQL Server 2014 или более позднюю версию. Если perf важен для расчета медианы, я настоятельно рекомендую попробовать и выполнить тестирование нескольких вариантов, рекомендованных в этой статье, чтобы убедиться, что вы нашли лучший вариант для своей схемы.

Я также был бы особенно осторожен при использовании функции (новая в SQL Server 2012), PERCENTILE_CONTкоторая рекомендована в одном из других ответов на этот вопрос, потому что в статье, приведенной выше, установлено, что эта встроенная функция в 373 раза медленнее, чем самое быстрое решение. Вполне возможно, что это несоответствие улучшилось за последние 7 лет, но лично я бы не использовал эту функцию на большом столе, пока не проверил ее производительность по сравнению с другими решениями.

ОРИГИНАЛЬНЫЙ ПОЧТА 2009 НИЖЕ:

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

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

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

SELECT
   CustomerId,
   AVG(TotalDue)
FROM
(
   SELECT
      CustomerId,
      TotalDue,
      -- SalesOrderId in the ORDER BY is a disambiguator to break ties
      ROW_NUMBER() OVER (
         PARTITION BY CustomerId
         ORDER BY TotalDue ASC, SalesOrderId ASC) AS RowAsc,
      ROW_NUMBER() OVER (
         PARTITION BY CustomerId
         ORDER BY TotalDue DESC, SalesOrderId DESC) AS RowDesc
   FROM Sales.SalesOrderHeader SOH
) x
WHERE
   RowAsc IN (RowDesc, RowDesc - 1, RowDesc + 1)
GROUP BY CustomerId
ORDER BY CustomerId;

12
Я не думаю, что это работает, если у вас есть обманщики, особенно много обманщиков, в ваших данных. Вы не можете гарантировать, что row_numbers выстроятся в линию. Вы можете получить действительно сумасшедшие ответы для своей медианы или, что еще хуже, без медианы.
Джонатан Бирхальтер

26
Вот почему наличие неоднозначности (SalesOrderId в приведенном выше примере кода) важно, поэтому вы можете убедиться, что порядок строк набора результатов согласован как в обратном, так и в обратном направлении. Часто уникальный первичный ключ делает идеальным устранение неоднозначности, потому что это доступно без отдельного поиска индекса. Если нет доступного столбца устранения неоднозначности (например, если в таблице нет уникального ключа), то для вычисления медианы должен использоваться другой подход, потому что, как вы правильно заметили, если вы не можете гарантировать, что номера строк DESC являются зеркальными изображениями Номера строк ASC, тогда результаты непредсказуемы.
Джастин Грант

4
Спасибо, когда переключал столбцы в мою БД, я отбросил неоднозначность, думая, что это не имеет значения. В этом случае это решение работает действительно очень хорошо.
Джонатан Бирхальтер

8
Я предлагаю добавить комментарий к самому коду, описывающий необходимость устранения неоднозначности.
hoffmanc

4
Потрясающие! давно я знал его важность, но теперь я могу дать ему имя ... двусмысленность! Спасибо, Джастин!
CodeMonkey

204

Если вы используете 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

62
Это умно и относительно просто, учитывая, что агрегатная функция Median () не существует. Но как получается, что никакой функции Median () не существует !? Я немного FLOOR (), честно говоря.
Чарли Килиан

Ну, красиво и просто, но вам нужно обычно работают на жидком медиану в определенной группе категории, то есть как select gid, median(score) from T group by gid. Вам нужен коррелированный подзапрос для этого?
TMS

1
... Я имею в виду, как в этом случае (2-й запрос под названием «Пользователи с наибольшим средним баллом ответа»).
TMS

Томас - вам удалось решить проблему "для определенной группы"? У меня такая же проблема. Спасибо.
Стю Харпер

3
Как использовать это решение с GROUP BY?
Пшемыслав Ремин

82

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


12
Этот экспертный анализ дает убедительный аргумент против функций PERCENTILE из-за низкой производительности. sqlperformance.com/2012/08/t-sql-queries/median
carl.anderson

4
Вам не нужно добавлять DISTINCTили GROUPY BY SalesOrderID? В противном случае у вас будет много повторяющихся строк.
Константин

1
это ответ. не знаю, почему я должен был прокрутить это далеко
FistOfFury

Существует также небезрассудная версия с использованиемPERCENTILE_DISC
johnDanger

подчеркивая точку зрения @ carl.anderson, приведенную выше: было решено, что решение PERCENTILE_CONT было в 373 раза медленнее (!!!!) по сравнению с самым быстрым решением, которое они тестировали на SQL Server 2012 на своей конкретной схеме тестирования. Прочитайте статью, на которую ссылается Карл, для более подробной информации.
Джастин Грант

21

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

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


1
Это на самом деле работает довольно хорошо, и позволяет разделить данные.
Джонатан Бирхальтер

3
Если это нормально, чтобы быть выключенным на один, то запрос выше в порядке. Но если вам нужна точная медиана, у вас будут проблемы. Например, для последовательности (1,3,5,7) медиана равна 4, но запрос выше возвращает 3. Для (1,2,3,503,603,703) медиана равна 258, но запрос выше возвращает 503.
Джастин Грант,

1
Вы могли бы исправить недостаток неточности, взяв максимум и минимум каждого квартиля в подзапросе, затем AVGing MAX предыдущего и MIN следующего?
Rbjz



4

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

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

Если вы хотите использовать функцию «Создать агрегат» в 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

Затем вы можете написать запрос для вычисления медианы следующим образом: SELECT dbo.Median (Field) FROM Table


3

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

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

Следующий запрос возвращает медиану из списка значений в одном столбце. Его нельзя использовать как или вместе с агрегатной функцией, но вы все равно можете использовать его как подзапрос с предложением 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

3

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

2

Пример Джастина выше очень хорош. Но этот первичный ключ должен быть изложен очень четко. Я видел этот код в дикой природе без ключа, и результаты плохие.

Жалоба на 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

2

В UDF напишите:

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

7
В случае четного количества предметов, медиана - это среднее двух средних предметов, которое не распространяется на эту UDF.
Яаков Эллис

1
Можете ли вы переписать его во всей UDF?
Пшемыслав Ремин

2

Медианный поиск

Это самый простой способ найти медиану атрибута.

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)

как обработать случай, когда количество строк четное?
priojeet priyom


1

Для непрерывной переменной / меры '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

1

Используя агрегат COUNT, вы можете сначала подсчитать количество строк и сохранить их в переменной с именем @cnt. Затем вы можете вычислить параметры для фильтра OFFSET-FETCH, чтобы указать, основываясь на qty-порядке, сколько строк пропустить (значение смещения) и сколько фильтровать (значение выборки).

Число пропускаемых строк равно (@cnt - 1) / 2. Очевидно, что для нечетного числа это вычисление правильное, потому что сначала вы вычитаете 1 для одного среднего значения, а затем делите на 2.

Это также работает правильно для четного счета, потому что деление, используемое в выражении, является целочисленным делением; поэтому, вычитая 1 из четного числа, вы остаетесь с нечетным значением.

При делении этого нечетного значения на 2 дробная часть результата (.5) усекается. Количество извлекаемых строк - 2 - (@cnt% 2). Идея состоит в том, что когда счет нечетный, результат операции по модулю равен 1, и вам нужно выбрать 1 строку. Когда счет является четным, результат операции по модулю равен 0, и вам нужно выбрать 2 строки. Вычитая 1 или 0 результата операции по модулю из 2, вы получаете желаемые 1 или 2 соответственно. Наконец, чтобы вычислить срединное значение, возьмите одно или два результирующих значения и примените среднее значение после преобразования входного целочисленного значения в числовое значение следующим образом:

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

0

Я хотел выработать решение самостоятельно, но мой мозг сломался и упал на пути. Я думаю, что это работает, но не проси меня объяснить это утром. :П

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

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

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

WITH MyResults(RowNo, Number) AS
(
    SELECT RowNo, Number FROM
        (SELECT ROW_NUMBER() OVER (ORDER BY Number) AS RowNo, Number FROM @table) AS Foo
)
SELECT AVG(Number) FROM MyResults WHERE RowNo = (@Count+1)/2 OR RowNo = ((@Count+1)%2) * ((@Count+2)/2)

0
--Create Temp Table to Store Results in
DECLARE @results AS TABLE 
(
    [Month] datetime not null
 ,[Median] int not null
);

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


WHILE (@IntDate < 0) 
BEGIN

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

--Insert records into Temp Table
insert into @table 

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



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

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


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


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

select *
from @results
order by [Month]

0

Это работает с SQL 2000:

DECLARE @testTable TABLE 
( 
    VALUE   INT
)
--INSERT INTO @testTable -- Even Test
--SELECT 3 UNION ALL
--SELECT 5 UNION ALL
--SELECT 7 UNION ALL
--SELECT 12 UNION ALL
--SELECT 13 UNION ALL
--SELECT 14 UNION ALL
--SELECT 21 UNION ALL
--SELECT 23 UNION ALL
--SELECT 23 UNION ALL
--SELECT 23 UNION ALL
--SELECT 23 UNION ALL
--SELECT 29 UNION ALL
--SELECT 40 UNION ALL
--SELECT 56

--
--INSERT INTO @testTable -- Odd Test
--SELECT 3 UNION ALL
--SELECT 5 UNION ALL
--SELECT 7 UNION ALL
--SELECT 12 UNION ALL
--SELECT 13 UNION ALL
--SELECT 14 UNION ALL
--SELECT 21 UNION ALL
--SELECT 23 UNION ALL
--SELECT 23 UNION ALL
--SELECT 23 UNION ALL
--SELECT 23 UNION ALL
--SELECT 29 UNION ALL
--SELECT 39 UNION ALL
--SELECT 40 UNION ALL
--SELECT 56


DECLARE @RowAsc TABLE
(
    ID      INT IDENTITY,
    Amount  INT
)

INSERT INTO @RowAsc
SELECT  VALUE 
FROM    @testTable 
ORDER BY VALUE ASC

SELECT  AVG(amount)
FROM @RowAsc ra
WHERE ra.id IN
(
    SELECT  ID 
    FROM    @RowAsc
    WHERE   ra.id -
    (
        SELECT  MAX(id) / 2.0 
        FROM    @RowAsc
    ) BETWEEN 0 AND 1

)

0

Для таких новичков, как я, которые изучают самые основы, мне лично легче следовать этому примеру, так как легче понять, что именно происходит и откуда берутся медианные значения ...

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

В абсолютном страхе от некоторых кодов выше, хотя !!!


0

Это настолько простой ответ, насколько я мог придумать. Работал хорошо с моими данными. Если вы хотите исключить определенные значения, просто добавьте предложение where к внутреннему выбору.

SELECT TOP 1 
    ValueField AS MedianValue
FROM
    (SELECT TOP(SELECT COUNT(1)/2 FROM tTABLE)
        ValueField
    FROM 
        tTABLE
    ORDER BY 
        ValueField) A
ORDER BY
    ValueField DESC

0

Следующее решение работает при этих предположениях:

  • Нет повторяющихся значений
  • Нет 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) ; 

0
DECLARE @Obs int
DECLARE @RowAsc table
(
ID      INT IDENTITY,
Observation  FLOAT
)
INSERT INTO @RowAsc
SELECT Observations FROM MyTable
ORDER BY 1 
SELECT @Obs=COUNT(*)/2 FROM @RowAsc
SELECT Observation AS Median FROM @RowAsc WHERE ID=@Obs

0

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

0

Опираясь на ответ Джеффа Этвуда, приведенный выше, он использует 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

0

Часто нам может потребоваться рассчитать медиану не только для всей таблицы, но и для агрегатов по некоторому идентификатору. Другими словами, рассчитайте медиану для каждого идентификатора в нашей таблице, где каждый идентификатор имеет много записей. (на основе решения, отредактированного @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;

Надеюсь, поможет.


0

На ваш вопрос Джефф Этвуд уже дал простое и эффективное решение. Но, если вы ищете какой-то альтернативный подход для вычисления медианы, вам поможет код 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 tbl.rno=@middle_no or tbl.rno=@middle_no+@odd_even;

Если вы хотите рассчитать медиану в MySQL, эта ссылка на github будет полезна.


0

Это самое оптимальное решение для поиска медиан, о которых я могу думать. Имена в примере основаны на примере Джастина. Убедитесь, что существует индекс для таблицы 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

ОБНОВИТЬ

Я был немного не уверен в том, какой метод имеет лучшую производительность, поэтому я провел сравнение между моим методом Джастином Грантом и Джеффом Этвудсом, выполнив запрос на основе всех трех методов в одном пакете, и стоимость пакета каждого запроса составила:

Без индекса:

  • Мой 30%
  • Джастин Грантс 13%
  • Джефф Этвудс 58%

И с индексом

  • Мой 3%.
  • Джастин Грантс 10%
  • Джефф Этвудс 87%

Я попытался увидеть, насколько хорошо масштабируются запросы, если у вас есть индекс, создавая больше данных из примерно 14 000 строк в 2–512 раз, что в итоге составляет около 7,2 млн. Строк. Обратите внимание, что я удостоверился, что поле CustomeId было уникальным для каждого раза, когда я делал одну копию, чтобы пропорция строк по сравнению с уникальным экземпляром CustomerId оставалась постоянной. Пока я делал это, я запускал исполнения, где впоследствии перестраивал индекс, и заметил, что результаты стабилизировались примерно в 128 раз с данными, которые у меня были к этим значениям:

  • Мой 3%.
  • Джастин Грантс 5%
  • Джефф Этвудс 92%

Мне было интересно, как на производительность могло повлиять масштабирование числа строк, но сохранение уникального константы CustomerId, поэтому я настроил новый тест, в котором я сделал именно это. Теперь вместо стабилизации соотношение стоимости партии продолжало расходиться, также вместо примерно 20 строк на CustomerId в среднем у меня было в итоге около 10000 строк на такой уникальный Id. Числа где:

  • Мой 4%
  • Джастин 60%
  • Джеффс 35%

Я убедился, что реализовал каждый метод правильно, сравнивая результаты. Мой вывод заключается в том, что метод, который я использовал, как правило, быстрее, пока существует индекс. Также заметил, что этот метод является то, что рекомендуется для этой конкретной проблемы в этой статье https://www.microsoftpressstore.com/articles/article.aspx?p=2314819&seqNum=5

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


0

Для крупномасштабных наборов данных вы можете попробовать эту GIST:

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

Он работает путем агрегирования различных значений, которые вы найдете в вашем наборе (например, возраст или год рождения и т. Д.), И использует оконные функции SQL, чтобы найти любую процентильную позицию, указанную в запросе.

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.