【问题标题】:Calculate average for each month for a given date range计算给定日期范围内每个月的平均值
【发布时间】:2019-06-23 15:01:19
【问题描述】:

我有 employees 表,其中每个员工都有一个相关的 start_dateend_datesalary

注意:在底部您可以找到用于导入结构和数据的 SQL 代码。

+----+-------+------------+------------+---------+
| id | name  | start_date | end_date   | salary  |
+----+-------+------------+------------+---------+
|  1 | Mark  | 2017-05-01 | 2020-01-31 | 2000.00 |
|  2 | Tania | 2018-02-01 | 2019-08-31 | 5000.00 |
|  3 | Leo   | 2018-02-01 | 2018-09-30 | 3000.00 |
|  4 | Elsa  | 2018-12-01 | 2020-05-31 | 4000.00 |
+----+-------+------------+------------+---------+

问题

对于给定的日期范围,我想提取给定日期范围内每个月的平均工资。

更新:我希望有 MySQL 5.6 的解决方案,但如果也有 MySQL 8+ 的解决方案会很棒(仅供个人了解)。

示例

如果日期范围是 2018-08-01 - 2019-01-31,则 SQL语句应该从 2018 年 8 月到 2019 年 1 月循环,它必须计算每个月的平均工资:

  • 2018 年 8 月,在职员工是 MarkTaniaLeo(因为 2018 年 8 月在他们的start_dateend_date) 所以平均值是 3333.33
  • 2018 年 9 月,在职员工是 MarkTaniaLeo(因为 2018 年 9 月是他们的start_dateend_date) 所以平均值是 3333.33
  • 2018 年 10 月活跃员工为 MarkTania,因此平均值为 3500.00
  • 2018 年 11 月活跃员工为 MarkTania,因此平均值为 3500.00
  • 2018 年 12 月,在职员工是 MarkTaniaElsa,因此平均值为 3666.6667
  • 2019 年 1 月活跃员工为 MarkTaniaElsa,因此平均值为 3666.6667

您可以看到以下日期范围的预期结果 2018-08-01 - 2019-01-31

+------+-------+------------+
| year | month | avg_salary |
+------+-------+------------+
| 2018 | 08    | 3333.33    |
| 2018 | 09    | 3333.33    |
| 2018 | 10    | 3500.00    |
| 2018 | 11    | 3500.00    |
| 2018 | 12    | 3666.67    |
| 2019 | 01    | 3666.67    |
+------+-------+------------+

注意:我解决了这个混合 MySQL 和 PHP 代码的问题,但是对于大的日期范围,它必须执行太多的查询(每个月一个)。所以我想要一个解决方案仅使用 MySQL

SQL 导入结构和数据

CREATE TABLE `employees` (
  `id` int(10) UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT,
  `name` varchar(50) NOT NULL,
  `start_date` date NOT NULL,
  `end_date` date NOT NULL,
  `salary` decimal(10,2) DEFAULT NULL
);

INSERT INTO `employees` (`id`, `name`, `start_date`, `end_date`, `salary`) VALUES
(1, 'Mark', '2017-05-01', '2020-01-31', '2000.00'),
(2, 'Tania', '2018-02-01', '2019-08-31', '5000.00'),
(3, 'Leo', '2018-02-01', '2018-09-30', '3000.00'),
(4, 'Elsa', '2018-12-01', '2020-05-31', '4000.00');

【问题讨论】:

  • 你可能应该提到哪个版本,因为这种事情在8.0+中被大大简化了
  • 感谢您指出。我希望有 MySQL 5.6 的解决方案,但如果仅出于个人知识,也有适用于 8+ 的解决方案将是完美的。

标签: mysql sql date group-by aggregate-functions


【解决方案1】:

您可以简单地键入所需的月份(或使用 PHP 代码生成它们)并加入它:

SELECT ym, AVG(salary)
FROM (
    SELECT '2018-08-01' + INTERVAL 0 MONTH AS ym UNION ALL
    SELECT '2018-08-01' + INTERVAL 1 MONTH UNION ALL
    SELECT '2018-08-01' + INTERVAL 2 MONTH UNION ALL
    SELECT '2018-08-01' + INTERVAL 3 MONTH UNION ALL
    SELECT '2018-08-01' + INTERVAL 4 MONTH UNION ALL
    SELECT '2018-08-01' + INTERVAL 5 MONTH
) AS yearmonths
JOIN employees ON ym BETWEEN start_date AND end_date
GROUP BY ym

如果您有一个包含数字 0、1、... 的表格,那么您可以使用它。您甚至可以使用具有足够行数的任何表:

SELECT ym, AVG(salary)
FROM (
    SELECT '2018-08-01' + INTERVAL @n := @n + 1 MONTH AS ym
    FROM anytable, (SELECT @n := -1) x
    LIMIT 100
) AS yearmonths
JOIN employees ON ym BETWEEN start_date AND end_date
WHERE ym <= '2019-01-01'
GROUP BY ym

【讨论】:

    【解决方案2】:

    以下是 Postgresql 的做法。通过更改Mysql中generate_series()linkExtract()的等价物可以将其转换为Mysql查询

    WITH cte1 AS
      (SELECT generate_series('2018-08-01', '2019-01-31', '1 month'::interval)::date AS date),
         cte2 AS
      (SELECT id,
              name,
              salary,
              generate_series(start_date, end_date, '1 month'::interval)::date AS date
       FROM employees)
    SELECT extract(YEAR
                   FROM cte1.date),
           extract(MONTH
                   FROM cte1.date),
           avg(salary)
    FROM cte1
    JOIN cte2 ON extract(MONTH
                         FROM cte1.date)=extract(MONTH
                                                 FROM cte2.date)
    AND extract(YEAR
                FROM cte1.date)=extract(YEAR
                                        FROM cte2.date)
    GROUP BY extract(YEAR
                     FROM cte1.date),
             extract(MONTH
                     FROM cte1.date);
    

    【讨论】:

    • OP 要求一个 MySQL 解决方案,而不是一个使用 PostgreSQL 的解决方案
    【解决方案3】:

    这是一种 MySQL 8.0 递归 CTE 方法。 CTE 在employees 表中创建一个包含最小值start_date 和最大值end_date 之间的所有year, month 组合的列表,然后将LEFT JOINed 到employees 表中以获得所有人的平均工资在该特定年份和月份工作的员工:

    WITH RECURSIVE months (year, month) AS
    (
      SELECT YEAR(MIN(start_date)) AS year, MONTH(MIN(start_date)) AS month FROM employees
      UNION ALL
      SELECT year + (month = 12), (month % 12) + 1 FROM months
      WHERE STR_TO_DATE(CONCAT_WS('-', year, month, '01'), '%Y-%m-%d') <= (SELECT MAX(end_date) FROM employees)
    )
    SELECT m.year, m.month, ROUND(AVG(e.salary), 2) AS avg_salary
    FROM months m
    LEFT JOIN employees e ON STR_TO_DATE(CONCAT_WS('-', m.year, m.month, '01'), '%Y-%m-%d') BETWEEN e.start_date AND e.end_date
    WHERE STR_TO_DATE(CONCAT_WS('-', m.year, m.month, '01'), '%Y-%m-%d') BETWEEN '2018-08-01' AND '2019-01-31'
    GROUP BY m.year, m.month
    

    输出:

    year    month   avg_salary
    2018    8       3333.33
    2018    9       3333.33
    2018    10      3500.00
    2018    11      3500.00
    2018    12      3666.67
    2019    1       3666.67
    

    Demo on dbfiddle

    【讨论】:

    • 是否可以使用 MySQL 5.6 获得相同的解决方案?如果我之前没有指定,我很抱歉。我刚刚更新了问题。非常感谢您的帮助!
    • @Dan 不幸的是 MySQL 仅在 8.0 版中实现了 CTE。 GMB 的解决方案可能是 MySQL 5.6 的最佳解决方案
    【解决方案4】:

    要完成此操作,您需要根据日期范围生成日期列表。这是关于 SO 的常见问题,我使用了来自 this post 的公认解决方案。它使用简单的算术方法,可以生成广泛的日期列表(尽管性能可能会受到影响)。

    然后,我们只需要与原始表进行JOIN,即可计算出该时间点的平均工资。

    select
      year(x.date), 
      month(x.date),
      avg(coalesce(e.salary, 0)) avg_salary
    from (
      select a.date 
      from (
          select curdate() - INTERVAL (a.a + (10 * b.a) + (100 * c.a) + (1000 * d.a) ) DAY as Date
          from (select 0 as a union all select 1 union all select 2 union all select 3 union all select 4 union all select 5 union all select 6 union all select 7 union all select 8 union all select 9) as a
          cross join (select 0 as a union all select 1 union all select 2 union all select 3 union all select 4 union all select 5 union all select 6 union all select 7 union all select 8 union all select 9) as b
          cross join (select 0 as a union all select 1 union all select 2 union all select 3 union all select 4 union all select 5 union all select 6 union all select 7 union all select 8 union all select 9) as c
          cross join (select 0 as a union all select 1 union all select 2 union all select 3 union all select 4 union all select 5 union all select 6 union all select 7 union all select 8 union all select 9) as d
      ) a
      where a.date between '2018-08-01' and '2019-01-31'
    ) x left join employees e ON x.date between e.start_date and e.end_date
    group by year(x.date), month(x.date)
    order by 1, 2
    

    Demo on DB fiddle

    | year(x.date) | month(x.date) | avg_salary  |
    | ------------ | ------------- | ----------- |
    | 2018         | 8             | 3333.333333 |
    | 2018         | 9             | 3333.333333 |
    | 2018         | 10            | 3500        |
    | 2018         | 11            | 3500        |
    | 2018         | 12            | 3666.666667 |
    | 2019         | 1             | 3666.666667 |
    

    PS:另一种方法是创建一个日历表,用于存储日期列表,然后:

    select
      year(x.date), 
      month(x.date),
      avg(coalesce(e.salary, 0)) avg_salary
    from 
      mycalendar x
      left join employees e ON x.date between e.start_date and e.end_date
    where x.date between '2018-08-01' and '2019-01-31'
    group by year(x.date), month(x.date)
    order by 1, 2
    

    【讨论】:

    • ...或者只是升级到 8.0+
    【解决方案5】:

    部分答案...

    这是一个“老派”解决方案,使用整数表 (0-9),但请注意,这种事情在新版本的 sql 中是多余的......

    SELECT * FROM ints;
      +---+
      | i |
      +---+
      | 0 |
      | 1 |
      | 2 |
      | 3 |
      | 4 |
      | 5 |
      | 6 |
      | 7 |
      | 8 |
      | 9 |
      +---+
    
    SELECT '2018-08-01' + INTERVAL i2.i * 10 + i1.i MONTH x 
      FROM ints i1
         , ints i2 
     WHERE '2018-08-01' + INTERVAL i2.i * 10 + i1.i MONTH BETWEEN '2018-08-01' AND '2019-01-31';
    
      +------------+
      | x          |
      +------------+
      | 2018-08-01 |
      | 2018-09-01 |
      | 2018-10-01 |
      | 2018-11-01 |
      | 2018-12-01 |
      | 2019-01-01 |
      +------------+
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2021-12-29
      • 2021-11-01
      相关资源
      最近更新 更多