MySQL中计算中位数的简单方法

266

如何在 MySQL 中计算中位数,最好是简单快速的方法?我已经使用 AVG(x) 找到了平均值,但我很难找到一种简单的方式来计算中位数。目前,我正在将所有行返回到 PHP 中,排序,然后选择中间行,但肯定有一些简单的方法可以在单个 MySQL 查询中完成。

示例数据:

id | val
--------
 1    4
 2    7
 3    2
 4    2
 5    9
 6    8
 7    3

按照val排序得到2 2 3 4 7 8 9,因此中位数应为4,而SELECT AVG(val)的结果为5


7
自MariaDB 10.3版本以来,新增了一个函数——中位数函数,请参考 https://mariadb.com/kb/en/library/median/。 - berturion
50个回答

268

在MariaDB/MySQL中:

SELECT AVG(dd.val) as median_val
FROM (
SELECT d.val, @rownum:=@rownum+1 as `row_number`, @total_rows:=@rownum
  FROM data d, (SELECT @rownum:=0) r
  WHERE d.val is NOT NULL
  -- put some where clause here
  ORDER BY d.val
) as dd
WHERE dd.row_number IN ( FLOOR((@total_rows+1)/2), FLOOR((@total_rows+2)/2) );

Steve Cohen指出,在第一次遍历后,@rownum将包含总行数。这可以用于确定中位数,因此不需要第二次遍历或连接。

当记录数为偶数时,还使用AVG(dd.val)dd.row_number IN(...)来正确生成中位数。原因:

SELECT FLOOR((3+1)/2),FLOOR((3+2)/2); -- when total_rows is 3, avg rows 2 and 2
SELECT FLOOR((4+1)/2),FLOOR((4+2)/2); -- when total_rows is 4, avg rows 2 and 3

最后,MariaDB 10.3.3+ 包含一个中位数函数


4
有没有办法显示分组值?比如选择地点和该地点的中位数,像这样:select place, median_value from table... 有什么办法吗?谢谢。 - saulob
2
@rowNum 将在执行结束时拥有“总计数”。因此,如果您想避免再次执行“全部计数”(这是我的情况,因为我的查询不太简单),则可以使用它。 - Ahmed-Anas
只有一条语句的逻辑:( floor((total_rows+1)/2), floor((total_rows+2)/2) ) 计算中位数所需的行数非常棒!不确定您是如何想到这个的,但真是太聪明了。我不理解的部分是 (SELECT @rownum:=0) r -- 这有什么作用? - Shanemeister
1
我的值来自于两个表的连接,所以我不得不添加另一个子查询,以确保连接后行的排序是正确的!结构有点像 select avg(value) from (select value, row_number from (select a - b as value from a_table join b_table order by value)) - Daniel Buckmaster
1
我知道这很老了,但由于某种原因,这会产生与仅将set @rn:=-1移动到外部select而不是在内部select中实例化为0时非常不同的结果。由于某种原因,我无法使结果匹配。 - davzaman
显示剩余4条评论

76

我刚在评论区发现了另一个在线答案

For medians in almost any SQL:

SELECT x.val from data x, data y
GROUP BY x.val
HAVING SUM(SIGN(1-SIGN(y.val-x.val))) = (COUNT(*)+1)/2

确保您的列有良好的索引,并且索引用于过滤和排序。使用explain计划进行验证。

select count(*) from table --find the number of rows

计算“中位数”行号。可以使用:median_row = floor(count / 2)

然后从列表中挑选出来:

select val from table order by val asc limit median_row,1

这应该会返回一个仅包含所需值的行。

6
@rob,你能帮我编辑一下吗?还是说我应该接受Velcro解决方案?(实际上我不确定如何采纳其他的解决方案)。谢谢,Jacob。 - TheJacobTaylor
4
请注意它执行的是“交叉连接”,对于大型表格来说速度非常缓慢。 - Rick James
3
这个答案对于偶数行返回空值 - kuttumiah
这个答案对于某些数据集根本不起作用,例如,具有值0.1、0.1、0.1、2的微不足道的数据集——如果所有值都不同,则它将起作用,但仅在值相同时才起作用。 - Kem Mason
为什么不将最后一行简化为 HAVING SUM(SIGN(y.lat_n-x.lat_n)) = 0 - Sherman

39

我发现被采纳的解决方案在我的MySQL安装中无法工作,返回一个空集,但是这个查询对我在所有情况下都起作用:

SELECT x.val from data x, data y
GROUP BY x.val
HAVING SUM(SIGN(1-SIGN(y.val-x.val)))/COUNT(*) > .5
LIMIT 1

1
绝对正确,在我的索引表上运行得非常完美和快速。 - Rob
2
这似乎是所有答案中在mysql上最快的解决方案,仅用不到一百万条记录的表就可以达到200毫秒。 - Rob
3
它从一张表中两次进行选择。这个表的名字是 data,它被使用了两个名字 xy - Brian
4
仅仅是说,我使用了这个确切的查询在一个有33,000行的表上导致我的mysqld停止运行。 - Xenonite
4
这个查询对于行数为偶数的情况返回了错误的答案 - kuttumiah
显示剩余3条评论

34

不幸的是,TheJacobTaylor和velcrow的回答都不能返回当前版本的MySQL的准确结果。

上文中velcrow的回答接近正确,但它无法正确计算结果集中行数为偶数的情况。中位数的定义为1)奇数个数字集合的中间数字或2)偶数个数字集合的两个中间数字的平均数。

因此,这里是velcrow解决方案的修补程序,以处理奇数和偶数集合:

SELECT AVG(middle_values) AS 'median' FROM (
  SELECT t1.median_column AS 'middle_values' FROM
    (
      SELECT @row:=@row+1 as `row`, x.median_column
      FROM median_table AS x, (SELECT @row:=0) AS r
      WHERE 1
      -- put some where clause here
      ORDER BY x.median_column
    ) AS t1,
    (
      SELECT COUNT(*) as 'count'
      FROM median_table x
      WHERE 1
      -- put same where clause here
    ) AS t2
    -- the following condition will return 1 record for odd number sets, or 2 records for even number sets.
    WHERE t1.row >= t2.count/2 and t1.row <= ((t2.count/2) +1)) AS t3;

使用方法如下,只需按照以下 3 个简单步骤操作:

  1. 在上面的代码中将 "median_table"(2 处)替换为您的表格名称
  2. 将 "median_column"(3 处)替换为您想查找中位数的列名
  3. 如果您有 WHERE 条件,则将 "WHERE 1"(2 处)替换为您的 WHERE 条件

那么,对于字符串值的中位数,您会做什么? - Rick James

16

我提议一种更快的方法。

获取行数:

SELECT CEIL(COUNT(*)/2) FROM data;

然后在排序子查询中取中间值:

SELECT max(val) FROM (SELECT val FROM data ORDER BY val limit @middlevalue) x;

我使用一个由随机数字组成的 5x10e6 数据集进行了测试,只需不到 10 秒即可找到中位数。


3
为什么不使用以下语句:SELECT val FROM data ORDER BY val LIMIT @middlevalue, 1? - Bryan
1
你如何将第一个代码块的变量输出引入到第二个代码块中? - Trip
3
@middlevalue来自哪里? - Trip
10
由于变量不能在限制条件中使用,因此这种方法不起作用。 - codepk
1
不,@AbhishekSengupta,这并没有考虑到在计算真正的中位数时的奇偶逻辑差异。 - Phoenix
显示剩余4条评论

15

2
我刚刚亲自尝试了一下,值得一提的是,安装非常快速/简单,并且它按照广告所说的那样工作,包括分组,例如“select name, median(x) FROM t1 group by name”-- GitHub源代码在这里:https://github.com/infusion/udf_infusion - Kem Mason

11
如果MySQL拥有ROW_NUMBER,那么中位数可以通过以下SQL Server查询语句进行获得(这里只是启发式的):
WITH Numbered AS 
(
SELECT *, COUNT(*) OVER () AS Cnt,
    ROW_NUMBER() OVER (ORDER BY val) AS RowNum
FROM yourtable
)
SELECT id, val
FROM Numbered
WHERE RowNum IN ((Cnt+1)/2, (Cnt+2)/2)
;

如果你有偶数个条目,则使用IN。

如果你想按组查找中位数,请在OVER子句中按组进行分区。

罗布


1
不,没有ROW_NUMBER OVER,也没有PARTITION BY,这是MySql,不像PostgreSQL、IBM DB2、MS SQL Server等真正的数据库引擎;-)。 - Alex Martelli
2
MySQL现在拥有窗口函数,因此这基本上是可行的。你唯一需要改变的是必须在最后取结果的平均值。 - GuyStalks

9

在MySQL文档的这个页面上,有一条评论提出了以下建议:

-- (mostly) High Performance scaling MEDIAN function per group
-- Median defined in http://en.wikipedia.org/wiki/Median
--
-- by Peter Hlavac
-- 06.11.2008
--
-- Example Table:

DROP table if exists table_median;
CREATE TABLE table_median (id INTEGER(11),val INTEGER(11));
COMMIT;


INSERT INTO table_median (id, val) VALUES
(1, 7), (1, 4), (1, 5), (1, 1), (1, 8), (1, 3), (1, 6),
(2, 4),
(3, 5), (3, 2),
(4, 5), (4, 12), (4, 1), (4, 7);



-- Calculating the MEDIAN
SELECT @a := 0;
SELECT
id,
AVG(val) AS MEDIAN
FROM (
SELECT
id,
val
FROM (
SELECT
-- Create an index n for every id
@a := (@a + 1) mod o.c AS shifted_n,
IF(@a mod o.c=0, o.c, @a) AS n,
o.id,
o.val,
-- the number of elements for every id
o.c
FROM (
SELECT
t_o.id,
val,
c
FROM
table_median t_o INNER JOIN
(SELECT
id,
COUNT(1) AS c
FROM
table_median
GROUP BY
id
) t2
ON (t2.id = t_o.id)
ORDER BY
t_o.id,val
) o
) a
WHERE
IF(
-- if there is an even number of elements
-- take the lower and the upper median
-- and use AVG(lower,upper)
c MOD 2 = 0,
n = c DIV 2 OR n = (c DIV 2)+1,

-- if its an odd number of elements
-- take the first if its only one element
-- or take the one in the middle
IF(
c = 1,
n = 1,
n = c DIV 2 + 1
)
)
) a
GROUP BY
id;

-- Explanation:
-- The Statement creates a helper table like
--
-- n id val count
-- ----------------
-- 1, 1, 1, 7
-- 2, 1, 3, 7
-- 3, 1, 4, 7
-- 4, 1, 5, 7
-- 5, 1, 6, 7
-- 6, 1, 7, 7
-- 7, 1, 8, 7
--
-- 1, 2, 4, 1

-- 1, 3, 2, 2
-- 2, 3, 5, 2
--
-- 1, 4, 1, 4
-- 2, 4, 5, 4
-- 3, 4, 7, 4
-- 4, 4, 12, 4


-- from there we can select the n-th element on the position: count div 2 + 1 

在我看来,这个方案显然是最好的,特别是在需要从复杂的子集中获取中位数时(我需要计算大量数据子集的单独中位数)。 - mblackwell8
对我来说运行良好。5.6.14 MySQL Community Server。拥有1100万条记录的表(约20GB),具有两个非主索引(model_id,price)。在表格中(经过过滤)我们有50万条记录需要计算中位数。结果我们有30K条记录(model_id,median_price)。查询持续时间为1.5-2秒。速度对我来说很快。 - Mikl

9
大多数上述方案仅适用于表格的一个字段,您可能需要针对查询中的许多字段获取中位数(第50个百分位数)。
我使用这个:
SELECT CAST(SUBSTRING_INDEX(SUBSTRING_INDEX(
 GROUP_CONCAT(field_name ORDER BY field_name SEPARATOR ','),
  ',', 50/100 * COUNT(*) + 1), ',', -1) AS DECIMAL) AS `Median`
FROM table_name;

您可以将上面示例中的“50”替换为任何百分位数,这非常高效。

只需确保您有足够的内存来处理GROUP_CONCAT,您可以使用以下命令进行更改:

SET group_concat_max_len = 10485760; #10MB max length

更多细节请参考:http://web.performancerasta.com/metrics-tips-calculating-95th-99th-or-any-percentile-with-single-mysql-query/

注意:对于偶数个值,它会取两个中间值中较大的一个。对于奇数个值,它会取中位数后面较大的一个值。 - giordano
聪明地使用GROUP_CONCAT,无需多个子查询! - yg-dba

7

我有以下代码,是在HackerRank上找到的,它非常简单,在每种情况下都能正常工作。

SELECT M.MEDIAN_COL FROM MEDIAN_TABLE M WHERE  
  (SELECT COUNT(MEDIAN_COL) FROM MEDIAN_TABLE WHERE MEDIAN_COL < M.MEDIAN_COL ) = 
  (SELECT COUNT(MEDIAN_COL) FROM MEDIAN_TABLE WHERE MEDIAN_COL > M.MEDIAN_COL );

5
我认为这只适用于具有奇数个条目数量的表格。对于偶数个条目数量,可能会有问题。 - Y. Chang
@Y.Chang 你说得对。这个程序在有偶数行的时候返回空。 - Ma'ruf

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接