Рассчитать среднее значение за каждый месяц для данного диапазона дат - PullRequest
0 голосов
/ 30 января 2019

У меня есть таблица сотрудников , где у каждого сотрудника есть связанные дата_ начала , конец_даты и оклад .

* 1010.* ПРИМЕЧАНИЕ: внизу вы можете найти код 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 года по январь 2019 года, и он должен рассчитывать среднюю зарплату за каждый месяц:

  • в Август 2018 активные сотрудники Марк , Таня , Лев (потому что август 2018 года находится между стart_date и end_date ), поэтому среднее значение составляет 3333.33
  • в сентября 2018 активные сотрудники Mark , Таня , Лев (поскольку сентябрь 2018 года находится между start_date и end_date ), поэтому среднее значение составляет 3333.33
  • в Октябрь 2018 Активные сотрудники Марк , Таня , поэтому в среднем 3500.00
  • в ноябре 2018 активные сотрудники: Марка , Таня , поэтому среднее значение составляет 3500.00
  • в Декабрь 2018 Активные сотрудники: Марка , Таня , Эльза , поэтому в среднем 3666.6667
  • в Январь 2019 Активные сотрудники Марка , Таня , Эльза так что в среднем 3666.6667

После вы можете увидеть ожидаемый результат для даты range 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');

Ответы [ 5 ]

0 голосов
/ 30 января 2019

Вот рекурсивный способ сделать это в MySQL 8.0.CTE создает список всех комбинаций year, month между минимальной start_date и максимальной end_date в таблице employees, которая затем LEFT JOIN переходит в таблицу 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

Демонстрация на dbfiddle

0 голосов
/ 30 января 2019

Ниже приведен способ Postgresql.Его можно преобразовать в запрос Mysql, изменив эквивалент generate_series() link и Extract() в 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);
0 голосов
/ 30 января 2019

Чтобы сделать это, вам нужно создать список дней из диапазона дат.Это часто задаваемый вопрос о SO, я использовал принятое решение от этого поста .Он использует простой арифметический метод и может генерировать широкие списки дат (хотя производительность может снизиться).

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

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

Демонстрация на скрипте БД :

| 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
0 голосов
/ 30 января 2019

Вы можете просто набрать нужные месяцы (или использовать код 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
0 голосов
/ 30 января 2019

Частичный ответ ...

Вот решение "старой школы", использующее таблицу целых чисел (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 |
  +------------+
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...