在 SQL Server 中计算中位数的函数

Posted

技术标签:

【中文标题】在 SQL Server 中计算中位数的函数【英文标题】:Function to Calculate Median in SQL Server 【发布时间】:2010-11-23 11:53:08 【问题描述】:

根据MSDN,Median 在 Transact-SQL 中不能用作聚合函数。但是,我想知道是否可以创建此功能(使用Create Aggregate 函数、用户定义函数或其他方法)。

最好的方法(如果可能的话)是什么 - 允许在聚合查询中计算中间值(假设为数字数据类型)?

【问题讨论】:

sqlperformance.com/2012/08/t-sql-queries/median 【参考方案1】:

这段代码有点长,但是很容易理解

medi 是具有列 'val' 的表,该列具有数据集, smedi 是一个 cte,其中列 idx 作为行号,vals 作为 medi 表中的“val”,按升序排序。 然后是它的基本数学,如果行号是奇数,那么它的中间值来自 smedi。 当它甚至是中间两个值的平均值时。

with smedi(idx,vals) as(
                select ROW_NUMBER() over(order by val),val from medi
                )
select (case
            when (select count(*) from medi)%2!=0 then (select vals from smedi where (((select count(*) from medi)/2))=idx)
            else (select avg(vals) from smedi where idx in ((select count(*)/2 from medi),(select (count(*)/2)+1 from medi)))
            end)

【讨论】:

【参考方案2】:

在我的解决方案表中是一个只有分数列的学生表,我正在计算分数的中位数,这个解决方案基于 SQL Server 2019

with total_c as ( --Total_c CTE counts total number of rows in a table
    select count(*) as n from student
),
even as ( --Even CTE extract two middle rows if the number of rows are even
    select marks from student 
    order by marks 
    offset (select n from total_c)/2 -1 rows
    fetch next 2 rows only
),
odd as ( --Odd CTE extract middle row if the number of rows are odd
    select marks from student 
    order by marks 
    offset (select n + 1 from total_c)/2 -1 rows
    fetch next 1 rows only
    )
--Case statement helps to select odd or even CTE based on number of rows
select                                                        
case when n%2 = 0 then (select avg(cast(marks as float)) from even)
    else (select marks from odd)
end as med_marks
from total_c

【讨论】:

您的答案可以通过添加有关代码的作用以及它如何帮助 OP 的更多信息来改进。 @Tyler2P 我已将 cmets 添加到代码中。希望对您有所帮助,如果我可以通过其他方式改进它,请告诉我【参考方案3】:

从员工表中获取工资的中值

with cte as (select salary, ROW_NUMBER() over (order by salary asc) as num from employees)

select avg(salary) from cte where num in ((select (count(*)+1)/2 from employees), (select (count(*)+2)/2 from employees));

【讨论】:

【参考方案4】:

中值发现

这是查找属性中位数的最简单方法。

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)

【讨论】:

行数为偶数的情况下如何处理?【参考方案5】:

使用单个语句 - 一种方法是使用 ROW_NUMBER()、COUNT() 窗口函数并过滤子查询。这是找到工资中位数:

 SELECT AVG(e_salary) 
 FROM                                                             
    (SELECT 
      ROW_NUMBER() OVER(ORDER BY e_salary) as row_no, 
      e_salary,
      (COUNT(*) OVER()+1)*0.5 AS row_half
     FROM Employee) t
 WHERE row_no IN (FLOOR(row_half),CEILING(row_half))

我在网上看到过使用 FLOOR 和 CEILING 的类似解决方案,但尝试使用单个语句。 (已编辑)

【讨论】:

【参考方案6】:

使用 COUNT 聚合, 您可以先计算有多少行并存储在一个名为@cnt 的变量中。然后 您可以根据数量排序计算 OFFSET-FETCH 过滤器的参数以指定, 跳过多少行(偏移值)和过滤多少行(获取值)。

行数 要跳过的是 (@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;

【讨论】:

【参考方案7】:

以下是我的解决方案:

with tempa as

 (

    select value,row_number() over (order by value) as Rn,/* Assigning a 
                                                           row_number */
           count(value) over () as Cnt /*Taking total count of the values */
    from numbers
    where value is not null /* Excluding the null values */
 ),

tempb as

  (

    /* Since we don't know whether the number of rows is odd or even, we shall 
     consider both the scenarios */

    select round(cnt/2) as Ref from tempa where mod(cnt,2)=1
    union all
    select round(cnt/2) a Ref from tempa where mod(cnt,2)=0
     union all
    select round(cnt/2) + 1 as Ref from tempa where mod(cnt,2)=0
   )
  select avg(value) as Median_Value

  from tempa where rn in

    ( select Ref from tempb);

【讨论】:

【参考方案8】:

尝试以下逻辑找出中位数:

考虑一个包含以下数字的表格: 1,1,2,3,4,5

中位数为 2.5

以 tempa 作为 ( 选择 num,count(num) over() 作为 Cnt, row_number() over (order by num) as Rnum 从温度), 温度为 ( 选择 round(cnt/2) 作为 ref_value 来自 mod(cnt,2)0 的 tempa 联合所有 从 mod(cnt,2)=0 的 tempa 中选择 round(cnt/2) 联合所有 选择回合(cnt/2+1) 从 mod(cnt,2)=0 的 tempa ) 从 tempa 中选择 avg(num) where rnum in (select * from tempb);

【讨论】:

【参考方案9】:

2019 年更新:在我写下这个答案的 10 年里,已经发现了更多可能产生更好结果的解决方案。此外,此后的 SQL Server 版本(尤其是 SQL 2012)引入了可用于计算中位数的新 T-SQL 功能。 SQL Server 版本还改进了它的查询优化器,这可能会影响各种中值解决方案的性能。 Net-net,我最初的 2009 年帖子仍然可以,但对于现代 SQL Server 应用程序可能有更好的解决方案。看看这篇 2012 年的文章,这是一个很好的资源:@​​987654321@

本文发现以下模式比所有其他替代方案都要快得多,至少在他们测试的简单模式上是这样。此解决方案比测试的最慢 (PERCENTILE_CONT) 解决方案快 373 倍 (!!!)。请注意,此技巧需要两个单独的查询,这可能并非在所有情况下都实用。它还需要 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 或更高版本。如果性能对您的中位数计算很重要,我强烈建议您尝试并测试该文章中推荐的几个选项,以确保您找到了最适合您的架构的选项。

我还会特别小心使用(SQL Server 2012 中的新功能)函数 PERCENTILE_CONT,该函数在该问题的 other answers 之一中推荐,因为上面链接的文章发现这个内置函数是 373x比最快的解决方案慢。从那以后的 7 年里,这种差异可能有所改善,但我个人不会在大桌子上使用这个函数,直到我验证了它与其他解决方案的性能。

下面是 2009 年的原始帖子:

有很多方法可以做到这一点,但性能差异很大。这是一个特别优化的解决方案,来自 Medians, ROW_NUMBERs, and performance。对于执行期间生成的实际 I/O,这是一个特别优化的解决方案——它看起来比其他解决方案成本更高,但实际上速度要快得多。

该页面还包含对其他解决方案和性能测试详细信息的讨论。请注意使用唯一列作为消歧器,以防多行的中值列具有相同的值。

与所有数据库性能方案一样,始终尝试使用真实硬件上的真实数据来测试解决方案——您永远不知道何时更改 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;

【讨论】:

如果您的数据中有欺骗者,尤其是很多欺骗者,我认为这不起作用。你不能保证 row_numbers 会排成一行。你的中位数可以得到一些非常疯狂的答案,或者更糟糕的是,根本没有中位数。 这就是为什么有一个消歧器(上面代码示例中的SalesOrderId)很重要,因此您可以确保结果集行的顺序前后一致。通常,唯一的主键是理想的消歧器,因为它无需单独的索引查找即可使用。如果没有可用的消歧列(例如,如果表没有唯一键),则必须使用另一种方法来计算中位数,因为正如您正确指出的那样,如果您不能保证 DESC 行号是ASC 行号,则结果是不可预测的。 谢谢,在将列切换到我的数据库时,我放弃了消歧器,认为它不相关。在这种情况下,这个解决方案真的很有效。 我建议在代码本身添加注释,描述消歧器的必要性。 太棒了!我早就知道它的重要性,但现在我可以给它一个名字......消歧器!谢谢贾斯汀!【参考方案10】:

对于大规模数据集,您可以试试这个 GIST:

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

它通过聚合您在集合中找到的不同值(例如年龄或出生年份等)来工作,并使用 SQL 窗口函数来定位您在查询中指定的任何百分位位置。

【讨论】:

【参考方案11】:

在 UDF 中,写:

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

【讨论】:

在偶数项的情况下,中位数为中间两项的平均值,不包含在这个UDF中。 你能在整个UDF中重写它吗?【参考方案12】:

这是我能想到的寻找中位数的最佳解决方案。示例中的名称基于 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

更新

我有点不确定哪种方法的性能最好,所以我通过在一批中运行基于所有三种方法的查询,对我的方法 Justin Grants 和 Jeff Atwoods 进行了比较,每个查询的批处理成本为:

无索引:

我的 30% 贾斯汀赠款 13% 杰夫·阿特伍德 58%

还有索引

我的 3%。 贾斯汀赠款 10% 杰夫·阿特伍德 87%

我试图通过从大约 14,000 行创建更多数据,乘以 2 到 512(这意味着最终大约 720 万行)来查看查询的扩展性。请注意,我确保每次执行单个副本时,CustomeId 字段都是唯一的,因此与 CustomerId 的唯一实例相比,行的比例保持不变。在执行此操作时,我运行了执行程序,之后我重建了索引,我注意到结果稳定在 128 倍左右,而我拥有这些值的数据:

我的 3%。 贾斯汀赠款 5% 杰夫阿特伍德 92%

我想知道如何通过缩放行数但保持唯一的 CustomerId 不变来影响性能,所以我设置了一个新的测试,我就是这样做的。现在批量成本比率并没有稳定下来,而是不断变化,而不是平均每个 CustomerId 大约 20 行,我最终每个这样的唯一 ID 大约 10000 行。其中的数字:

我的 4% 贾斯汀 60% 杰夫斯 35%

我通过比较结果确保我正确实施了每种方法。 我的结论是,只要索引存在,我使用的方法通常会更快。还注意到此方法是本文中针对此特定问题推荐的方法https://www.microsoftpressstore.com/articles/article.aspx?p=2314819&seqNum=5

进一步提高对该查询的后续调用性能的一种方法是将计数信息保存在辅助表中。您甚至可以通过一个触发器来维护它,该触发器更新并保存有关依赖于 CustomerId 的 SalesOrderHeader 行数的信息,当然您也可以简单地存储中位数。

【讨论】:

【参考方案13】:

虽然贾斯汀格兰特的解决方案看起来很可靠,但我发现当给定分区键中有多个重复值时,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

【讨论】:

【参考方案14】:

对于您的问题,Jeff Atwood 已经给出了简单有效的解决方案。但是,如果您正在寻找一些替代方法来计算中位数,下面的 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 link 会很有用。

【讨论】:

【参考方案15】:

通常,我们可能不仅需要为整个表计算 Median,还需要针对某个 ID 计算聚合。换句话说,计算我们表中每个 ID 的中位数,其中每个 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;

希望对你有帮助。

【讨论】:

【参考方案16】:

对于来自“table1”的连续变量/度量“col1”

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

【讨论】:

【参考方案17】:

以下查询从一列中的值列表返回中位数。它不能用作聚合函数或与聚合函数一起使用,但您仍然可以将其用作子查询,并在内部选择中使用 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

【讨论】:

【参考方案18】:

MS SQL Server 2012(及更高版本)具有 PE​​RCENTILE_DISC 函数,该函数计算排序值的特定百分位数。 PERCENTILE_DISC (0.5) 将计算中位数 - https://msdn.microsoft.com/en-us/library/hh231327.aspx

【讨论】:

【参考方案19】:

在上面 Jeff Atwood 的回答的基础上,使用 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

【讨论】:

【参考方案20】:

更好:

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

来自大师本人,Itzik Ben-Gan!

【讨论】:

【参考方案21】:

我尝试了几种替代方案,但由于我的数据记录具有重复值,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;

【讨论】:

【参考方案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 ID=@Obs

【讨论】:

【参考方案23】:

以下解决方案适用于这些假设:

没有重复值 没有空值

代码:

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

【讨论】:

【参考方案24】:

上面贾斯汀的例子非常好。但是应该非常清楚地说明主键需求。我在野外看到没有密钥的代码,结果很糟糕。

我对 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

【讨论】:

【参考方案25】:

我最初的快速回答是:

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% 的工作是对数据进行排序,这在计算这样的位置相关统计数据时是不可避免的。

我已根据以下 cmets 中 Robert Ševčík-Robajz 的出色建议修改了答案:

;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,5,7),中位数为 4,但上面的查询返回 3。对于 (1,2,3,503,603,703),中位数为 258,但上面的查询返回 503。 你可以通过在子查询中取每个四分位数的最大值和最小值,然后平均前一个的最大值和下一个的最小值来解决不精确的缺陷吗?【参考方案26】:

这是我能想到的最简单的答案。与我的数据配合得很好。如果您想排除某些值,只需在内部选择中添加一个 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

【讨论】:

【参考方案27】:

对于像我这样正在学习基础知识的新手来说,我个人觉得这个例子更容易理解,因为更容易准确理解正在发生的事情以及中值的来源......

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

对上面的一些代码绝对敬畏!!!

【讨论】:

【参考方案28】:

如果您想在 SQL Server 中使用 Create Aggregate 函数,请按照以下步骤操作。这样做的好处是能够编写干净的查询。请注意,此过程可以很容易地用于计算百分比值。

创建一个新的 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(Field)

【讨论】:

【参考方案29】:

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

【讨论】:

由于性能不佳,该专家分析对 PERCENTILE 函数提出了令人信服的论据。 sqlperformance.com/2012/08/t-sql-queries/median 您不需要添加DISTINCTGROUPY BY SalesOrderID 吗?否则你会有很多重复的行。 这就是答案。不知道为什么我不得不滚动这么远 还有一个使用PERCENTILE_DISC的谨慎版本 强调@carl.anderson 的上述观点:PERCENTILE_CONT 解决方案被测量为比他们在 SQL Server 2012 上在其特定测试架构上测试的最快解决方案慢 373 倍 (!!!!)。阅读 carl 链接的文章了解更多详情。【参考方案30】:

简单、快速、准确

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)  

【讨论】:

以上是关于在 SQL Server 中计算中位数的函数的主要内容,如果未能解决你的问题,请参考以下文章

sql server数据库实现保留指定位数小数的函数

试图在 SQL Server 中找到总和的中位数

如何计算oracle pl/sql中数字的位数?

在 BigQuery SQL 中计算每个经理的工资中位数

SQl Server 函数篇 数学函数,字符串函数,转换函数,时间日期函数

如何在 SQL Server 2000/2005/2008 中使用 FLOAT 转换小数位数