web-dev-qa-db-fra.com

MySQL - Fusionner ou fractionner des intervalles de temps (date de début à date de fin)

J'ai un tableau qui stocke une liste d'activités avec un intervalle de temps délimité par 2 dates.

Échantillon:

+------+---------------------+---------------------+-------------+
| name |        start        |         end         | time (calc) |
+------+---------------------+---------------------+-------------+
|  me  | 2017-04-03 11:00:00 | 2017-04-03 11:30:00 |          30 |
|  me  | 2017-04-03 23:45:00 | 2017-04-04 00:15:00 |          30 |
|  me  | 2017-04-04 10:00:00 | 2017-04-04 11:00:00 |          60 |
|  me  | 2017-04-04 10:30:00 | 2017-04-04 11:30:00 |          60 |
|  me  | 2017-04-05 23:00:00 | 2017-04-05 23:30:00 |          30 |
|  me  | 2017-04-05 23:15:00 | 2017-04-07 00:45:00 |        1530 |
+------+---------------------+---------------------+-------------+

Je voudrais savoir combien de minutes sont occupées chaque jour par utilisateur (puis par semaine), j'ai donc besoin de transformer la table actuelle en où les intervalles qui partagent un espace partiel sont fusionnés en un seul, et les intervalles qui sont en plusieurs les jours sont divisés, comme le suivant:

+------+---------------------+---------------------+-------------+
| name |        start        |         end         | time (calc) |
+------+---------------------+---------------------+-------------+
|  me  | 2017-04-03 11:00:00 | 2017-04-03 11:30:00 |          30 |
|  me  | 2017-04-03 23:45:00 | 2017-04-03 23:59:59 |          15 |
|  me  | 2017-04-04 00:00:00 | 2017-04-04 00:15:00 |          15 |
|  me  | 2017-04-04 10:00:00 | 2017-04-04 11:30:00 |          90 |
|  me  | 2017-04-05 23:00:00 | 2017-04-05 23:59:59 |          60 |
|  me  | 2017-04-06 00:00:00 | 2017-04-06 23:59:59 |        1440 |
|  me  | 2017-04-07 00:00:00 | 2017-04-07 00:45:00 |          45 |
+------+---------------------+---------------------+-------------+

Pour ensuite l'interroger facilement afin d'obtenir des minutes par jour:

+------+------------+------+
| name |    day     | time |
+------+------------+------+
|  me  | 2017-04-03 |   45 |
|  me  | 2017-04-04 |  105 |
|  me  | 2017-04-05 |   60 |
|  me  | 2017-04-06 | 1440 |
|  me  | 2017-04-07 |   45 |
+------+------------+------+

Je cherchais des informations et j'ai trouvé comment fusionner plusieurs intervalles de dates ici ( mysql - sum interval dates ), mais je ne suis pas en mesure de diviser un intervalle en plusieurs jours.

Est-il possible de le faire en une seule requête? Comment pourrais-je le faire?

Modifier:

SQL (structure et données):

CREATE TABLE activities (
    id INT PRIMARY KEY NOT NULL AUTO_INCREMENT,
    name VARCHAR(45),
    start DATETIME,
    end DATETIME,
    time INT GENERATED ALWAYS AS (TIMESTAMPDIFF(MINUTE, start, end)) VIRTUAL
);

INSERT INTO activities (name, start, end) VALUES
('me','2017-04-03 11:00','2017-04-03 11:30'),
('me','2017-04-03 23:45','2017-04-04 00:15'),
('me','2017-04-04 10:00','2017-04-04 11:00'),
('me','2017-04-04 10:30','2017-04-04 11:30'),
('me','2017-04-05 23:00','2017-04-05 23:30'),
('me','2017-04-05 23:15','2017-04-07 00:45');

SQL pour fusionner plusieurs intervalles (Soruce: mysql - sum interval dates ):

SELECT name, min(start) AS start, end, TIMESTAMPDIFF(MINUTE, MIN(start), end) AS time
FROM (
    SELECT x.name, x.start, min(y.end) AS end 
    FROM activities AS x 
    JOIN activities AS y 
        ON x.name = y.name 
       AND x.start <= y.end 
       AND NOT EXISTS (
           SELECT 1 
           FROM activities AS z 
           WHERE y.name = z.name 
             AND y.end >= z.start 
             AND y.end < z.end
       ) 
    WHERE NOT EXISTS (
        SELECT 1 
        FROM activities AS u 
        WHERE x.name = u.name 
          AND x.start > u.start 
          AND x.start <= u.start
    ) 
    GROUP BY x.name, x.start
) AS v GROUP BY name, end;
5
vgc

D'abord parce que vous devez générer une série de dates, je vous suggère d'utiliser une table calendar.

CREATE TABLE if not exists calendar (
    mdate date PRIMARY KEY NOT NULL
);

INSERT INTO calendar values
('20170403'),('20170404'),('20170405'),('20170406'),('20170407'),('20170408');

Comment font-ils

Juste pour obtenir des activités qui se chevauchent, j'ai utilisé la requête que vous avez fournie sur votre question.

create view overlaped_activities
as
SELECT name, min(start) AS start, end, TIMESTAMPDIFF(MINUTE, MIN(start), end) AS time
FROM (
    SELECT x.name, x.start, min(y.end) AS end 
    FROM activities AS x 
    JOIN activities AS y 
        ON x.name = y.name 
       AND x.start <= y.end 
       AND NOT EXISTS (
           SELECT 1 
           FROM activities AS z 
           WHERE y.name = z.name 
             AND y.end >= z.start 
             AND y.end < z.end
       ) 
    WHERE NOT EXISTS (
        SELECT 1 
        FROM activities AS u 
        WHERE x.name = u.name 
          AND x.start > u.start 
          AND x.start <= u.start
    ) 
    GROUP BY x.name, x.start
) AS v GROUP BY name, end;

Je calcule d'abord les minutes entre la date de début et minuit:

if(date(start) = date(end), 
  time_to_sec(timediff(end, start)) / 60, 
  (1440 - time_to_sec(time(start)) / 60)) mstart

Ensuite, si début <> fin, je calcule les minutes de minuit à la date de fin:

if(date(start) = date(end), 0, time_to_sec(time(end)) / 60) mend

Cela renvoie un tableau comme celui-ci:

| start               | end                 |     mdiff |  mstart |    mend |
|---------------------|---------------------|----------:|--------:|--------:|
| 03.04.2017 11:00:00 | 03.04.2017 11:30:00 |   30,0000 | 30,0000 |       0 |
| 03.04.2017 23:45:00 | 04.04.2017 00:15:00 |   30,0000 | 15,0000 | 15,0000 |
| 04.04.2017 10:00:00 | 04.04.2017 11:30:00 |   90,0000 | 90,0000 |       0 |
| 05.04.2017 23:00:00 | 07.04.2017 00:45:00 | 1545,0000 | 60,0000 | 45,0000 |

C'est bien, mais il y a un autre problème ici:

| 05.04.2017 23:00:00 | 07.04.2017 00:45:00 | 1545,0000 | 60,0000 | 45,0000 |

Bien sûr: 1545 <> 60 + 45

Nous devons générer une série de dates entre la date de début et la date de fin et ajouter 1440 minutes à chaque jour.

Nous pouvons l'obtenir en utilisant le tableau du calendrier:

   select   name,
            mdate date_activity,
            sum(1440) minutes
   from     calendar
   join     overlaped_activities
   on       calendar.mdate > date(start)
   and      calendar.mdate < date(end)
   where    datediff(end, start) > 1
   group by name, mdate

Ok, nous avons tous les ingrédients, il est temps de cuisiner la recette:

select name, date_activity, sum(minutes) min_activity
from (
       select name, 
              date(start) date_activity,
              if(date(start) = date(end), time_to_sec(timediff(end, start)) / 60, (1440 - time_to_sec(time(start)) / 60)) minutes
       from overlaped_activities

       UNION ALL

       select name, 
              date(end) date_activity,
              if(date(start) = date(end), 0, time_to_sec(time(end)) / 60) minutes
       from overlaped_activities

       UNION ALL

       select   name,
                mdate date_activity,
                sum(1440) minutes
       from     calendar
       join     overlaped_activities
       on       calendar.mdate > date(start)
       and      calendar.mdate < date(end)
       where    datediff(end, start) > 1
       group by name, mdate
    ) act
group by name, date_activity;

Résultat final:

| name |       date_activity | min_activity |
|------|--------------------:|-------------:|
| me   | 03.04.2017 00:00:00 |      45,0000 |
| me   | 04.04.2017 00:00:00 |     105,0000 |
| me   | 05.04.2017 00:00:00 |      60,0000 |
| me   | 06.04.2017 00:00:00 |    1440,0000 |
| me   | 07.04.2017 00:00:00 |      45,0000 |  

Presque l'oublier, la recette: http://rextester.com/EIJOI2098

4
McNets

S'il est simplement divisé par dates et que vous utilisez DATETIME, cela peut être assez simple. Il suffit de sous-estimer l'horodatage et de les regrouper par date

SELECT
who,
SUBSTRING(date_created, 1, 10) AS date,
COUNT(times) AS time
FROM timings
WHERE date_created > '2017-04-03' AND date_created < '2017-04-10'
GROUP BY date
+------+------------+------+
| name |    day     | time |
+------+------------+------+
|  me  | 2017-04-03 |   45 |
|  me  | 2017-04-04 |  105 |
|  me  | 2017-04-05 |   60 |
|  me  | 2017-04-06 | 1440 |
|  me  | 2017-04-07 |   45 |
+------+------------+------+
0
user36388

Dans MySQL, pour diviser les jours, nous devons nous joindre à la table ayant des dates car la fonction Oracle de Connecter par niveau n'est pas prise en charge dans MySQL.

Ajout de plus de détails ...

Create table ActivityDay 
(id INT Primary Key NOT NULL AUTO_INCREMENT, ActivityDay Date NOT NULL);

insert into ActivityDate (ActivityDay) values('2017-04-03'), 
values('2017-04-04'),values('2017-04-05'),
values('2017-04-06'),values('2017-04-07'),
values('2017-04-08');

Select id, Name, ActualStart, ActualEnd, 
(Time_to_Sec(ActualEnd) - Time_to_Sec(ActualStart))/60 time
from (
select a.id, a.name, a.start, Date(start) StartDay, Date(end) EndDay, a.time,
       case when DATE(START) = DATE(ActualStart) then Start else ActivityDay end ActualStart,
       case when Date(end) <> Date(ActualEnd) then date_add(ActivityDay, interval 24*60*60 - 1 second) else end end ActualEnd,
       ad.ActivityDay
from Activities a join ActivityDate ad) d
Where ActivityDay between start and end)
0
SQL.RK