【问题标题】:aggregate data (by median) in mysql by quarter and by customer_type按季度和按客户类型在 mysql 中汇总数据(按中位数)
【发布时间】:2014-10-05 17:04:54
【问题描述】:

我用的是 MySQL,我的 SQL 表是这样的:

sales_year (INT), sales_month (INT), sales_day (INT), price (float), customer_type (TEXT)

我想知道哪个sql_query 会按季度汇总价格数据(计算每个季度的中位数价格,以及用于计算中位数的观察次数),并按客户类型分组。

我正在努力完成两个主要步骤: mySQL 似乎不支持中位数,以及如何按季度汇总数据 - 似乎按客户类型分组,一旦解决这两个问题就很容易了。

STRUGGLE - 计算中位数......

例如,我刚刚尝试创建四分之一列,它可以工作,但它计算的是 AVG 而不是中位数:

    select avg(price) as avg_price, floor(sales_month/3.0+1) as
    sales_quarter, count(*) as n_transactions, sales_year, customer_type
    from mydb.mytable
    group by sales_quarter, sales_year, customer_type;

此命令运行良好。但理想情况下,我可以通过 MEDIAN 更改 avg 但 mySQL 没有这样的支持,关于如何更改此代码以使其用于中值目的的任何建议?

注意:我还尝试从 site 中的用户定义函数安装我自己的中值函数,但 C 代码没有在我的 mac os X 上编译。

所以输出应该是这样的:

sales_quarter (INT)
sales_year (INT)
median_price (FLOAT)
number_users_used_to_compute_median (INT)
customer_type (TEXT)

【问题讨论】:

  • 您的问题here 的中间部分有一些潜在的答案。接受的答案似乎已过时。其他一些可能值得一试。
  • @PatrickQ 我正在调查,谢谢!虽然看起来很复杂,特别是现在我已经有一个很长的代码......
  • 浮动?当然是十进制,并考虑将日期存储为日期。另外,你的主键是什么?

标签: mysql sql


【解决方案1】:

哦,只需将平均值称为中位数。与您交谈的人通常不会知道其中的区别 (;)。

好的,说真的,您可以在 MySQL 中执行此操作。有一种使用group_concat()substring_index() 的方法,但存在溢出中间字符串值的风险。相反,枚举值并进行简单的算术运算。为此,您需要一个枚举和一个总数。枚举为:

  select t.*,
         @rn := if(@q = quarter and @y = @year and @ct = customer_type,
                   @rn + 1,
                   if(@q := quarter, if(@y := @year, if(@ct := customer_type, 1, 1), 1), 1)
                  ) as rn
  from mydb.mytable t cross join
       (select @q := '', @y := '', @ct := '', @rn := 0) vars
  order by sales_quarter, sales_year, customer_type, price;

这是精心制定的。 order by 列对应于定义的变量。在select 中只有一个语句分配变量。嵌套的if() 语句确保设置每个变量(使用andor 可能导致短路)。重要的是要记住,MySQL 不保证 select 中表达式的求值顺序,因此只有一个语句集变量对于确保正确性很重要。

现在,获取中位数非常容易。您需要总计数、顺序值 (rn) 和一些算法来处理偶数个值的情况:

select trn.sales_quarter, trn.sales_year, trn.customer_type, avg(price) as median
from (select t.*,
             @rn := if(@q = quarter and @y = @year and @ct = customer_type,
                       @rn + 1,
                       if(@q := quarter, if(@y := @year, if(@ct := customer_type, 1, 1), 1), 1)
                      ) as rn
      from mydb.mytable t cross join
           (select @q := '', @y := '', @ct := '', @rn := 0) vars
      order by sales_quarter, sales_year, customer_type, price
     ) trn join
     (select sales_quarter, sales_year, customer_type, count(*) as numrows
      from mydb.mytable t
      group by sales_quarter, sales_year, customer_type
     ) s
     on trn.sales_quarter = s.sales_quarter and
        trn.sales_year = s.sales_year and
        trn.customer_type = s.customer_type
where 2*rn in (numrows, numrows - 1, numrows + 1)
group by trn.sales_quarter, trn.sales_year, trn.customer_type;

只是为了强调最终的平均值不是在进行平均计算。它正在计算中位数。正常的定义是,对于偶数个值,中位数是中间两个的平均值。 where 子句处理偶数和奇数情况。

【讨论】:

  • @JaugarChang 。 . .感谢您赶上失踪的小组。
【解决方案2】:

要获得中位数,您可以尝试类似的方法,

SELECT *
FROM table
LIMIT COUNT(*)/2, 1

这基本上是说:“给我从第 n/2 个元素开始的 1 个项目,其中 n 是集合的大小。”

因此,如果您按季度进行,那么添加一些 GROUP BY 季度类型的东西也是一样的。如果您希望我对此进行更多扩展,请告诉我。

【讨论】:

    【解决方案3】:

    参考velcrow's answer并发布SqlFiddle here

    select quarter,
           group_concat(val order by row_number) ValSortString,
           floor((max(row_number) - min(row_number))/2)+1 as FirstPosition,
           ceil((max(row_number) - min(row_number))/2) +1 as SecondPosition,
           split_str(group_concat(val order by row_number),',',floor((max(row_number) - min(row_number))/2)+1) as FirstVal,
           split_str(group_concat(val order by row_number),',',ceil((max(row_number) - min(row_number))/2)+1) as SecondVal,
           (split_str(group_concat(val order by row_number),',',floor((max(row_number) - min(row_number))/2)+1) +
            split_str(group_concat(val order by row_number),',',ceil((max(row_number) - min(row_number))/2)+1) )/2 as Median
    from (
          SELECT data.quarter,@rownum:=@rownum+1 as row_number, data.val,total_rows
                 FROM data ,  
                      (select quarter,count(*) as total_rows from data group by quarter) as t,
                      (SELECT @rownum:=0) r
                 where t.quarter = data.quarter
                 order by data.quarter,val
         ) as b
    group by quarter
    

    此代码仅按季度分组,便于按列展开其他分组。

    我使用 group_concat 和 split_str 来简化它,只使用一个子查询。

    所以你必须创建 split_str 函数:

    CREATE FUNCTION SPLIT_STR(
      x VARCHAR(255),
      delim VARCHAR(12),
      pos INT
    )
    RETURNS VARCHAR(255)
    RETURN REPLACE(SUBSTRING(SUBSTRING_INDEX(x, delim, pos),
           LENGTH(SUBSTRING_INDEX(x, delim, pos -1)) + 1),
           delim, '');
    

    问题是 group_concat 和 split_str 有限制。但是这个版本可以解决只有子查询的问题,并且易于理解。

    更新
    根据 Gordon Linoff 的指标,我添加了另一个没有 group_concat 的解决方案。

    select quarter,
           floor((total_rows + 1)/2) as FirstPosition,
           ceil((total_rows + 1)/2) as SecondPosition,
           avg(val) as median
    from (
          SELECT data.quarter,
                 @rownum:= if (@q = data.quarter ,@rownum+1,if(@q := data.quarter, 1, 1) )as row_number, 
                 data.val,
                 total_rows
                 FROM data ,  
                      (select quarter,count(*) as total_rows from data group by quarter) as t,
                      (SELECT @q := '', @rownum:=0) r
                 where t.quarter = data.quarter
                 order by data.quarter,val
         ) as b
    where row_number in (floor((total_rows + 1)/2), ceil((total_rows + 1)/2))
    group by quarter
    

    还有新的Sql Fiddle here

    我是mysql的新手,这个问题很容易被MSSql、DB2或Oracle解决,他们都有Row_number()(Partition by ...)

    我没有足够的声誉来评论 Gordon Linoff 的回答,我必须感谢他学习如何实现 row_number()(Partition by ...) 功能。

    【讨论】:

      【解决方案4】:

      我知道有两种方法可以做到这一点。第一个使用两个选择和一个连接,第一个选择获取值和排名,第二个选择获取计数,然后连接它们。第二个使用 json 函数在一次选择中获取所有内容。它们都有点冗长,但它们可以工作并且相当快。

      解决方案#1(两个选择和一个连接,一个获取计数,一个获取排名)

      SELECT  x.group_field, 
              avg(
                  if( 
                      x.rank - y.vol/2 BETWEEN 0 AND 1, 
                      value_field, 
                      null
                  )
              ) as median
      FROM (
          SELECT  group_field, value_field, 
                  @r:= IF(@current=group_field, @r+1, 1) as rank, 
                  @current:=group_field
          FROM (
              SELECT group_field, value_field
              FROM table_name
              ORDER BY group_field, value_field
          ) z, (SELECT @r:=0, @current:='') v
      ) x, (
          SELECT group_field, count(*) as vol 
          FROM table_name
          GROUP BY group_field
      ) y WHERE x.group_field = y.group_field
      GROUP BY x.group_field;
      

      解决方案 #2(使用 json 对象来存储计数并避免连接)

      SELECT group_field, 
          avg(
              if(
                  rank - json_extract(@vols, path)/2 BETWEEN 0 AND 1,
                  value_field,
                  null
              )
          ) as median
      FROM (
          SELECT group_field, value_field, path, 
              @rnk := if(@curr = group_field, @rnk+1, 1) as rank,
              @vols := json_set(
                  @vols, 
                  path, 
                  coalesce(json_extract(@vols, path), 0) + 1
              ) as vols,
              @curr := group_field
          FROM (
              SELECT p.group_field, p.value_field, concat('$.', p.group_field) as path
              FROM table_name
              JOIN (SELECT @curr:='', @rnk:=1, @vols:=json_object()) v
              ORDER BY group_field, value_field DESC
          ) z
      ) y GROUP BY group_field;
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2016-11-15
        • 1970-01-01
        • 1970-01-01
        • 2017-10-19
        • 2021-04-23
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多