sqlmariadbmariadb-10.3

Get a work schedule template based on date range


I have a table in my MariaDB 10.3 database with schedule templates for my workers, which are then used in a schedule planner. When there is a modification to the employee's employment contract, a new set of records is added with the new schedule.

So far I have the following query:

SELECT
    ehp.id_pers_empleado employee_id,
    ehp.diasem weekday,
    ehp.hora_inicio `from`,
    ehp.hora_fin `to`,
    ehp.validez `starts`,
    TIMESTAMPDIFF( MINUTE, ehp.hora_inicio, ehp.hora_fin ) / 60 worked
FROM
    empleados_horarios_predeterminados ehp 
WHERE
    id_pers_empleado = 476 
ORDER BY
    `starts`,
    weekday 

That query returns the following fields:

field type description
employee_id int Employee ID
weekday int 1 (Monday) to 7 (Sunday)
from time Start time of the work shift
to time End time of the work shift
starts date Date from which this record becomes valid
hours float Hours the employee will work (to save time)

In this way, the records I obtain are the following:

employee_id weekday from to starts worked
00000476 1 09:00:00 14:00:00 2024-04-01 5.0000
00000476 2 09:00:00 14:00:00 2024-04-01 5.0000
00000476 3 09:00:00 14:00:00 2024-04-01 5.0000
00000476 4 09:00:00 14:00:00 2024-04-01 5.0000
00000476 5 09:00:00 14:00:00 2024-04-01 5.0000
00000476 1 07:00:00 15:30:00 2024-05-01 8.5000
00000476 2 07:00:00 15:00:00 2024-05-01 8.0000
00000476 3 07:00:00 15:00:00 2024-05-01 8.0000
00000476 4 07:00:00 15:00:00 2024-05-01 8.0000
00000476 5 07:00:00 15:00:00 2024-05-01 8.0000
5 1 09:00:00 14:00:00 2020-04-30 5.0000
5 1 18:00:00 21:00:00 2020-04-30 3.0000
5 2 09:00:00 14:00:00 2020-04-30 5.0000
5 2 18:00:00 21:00:00 2020-04-30 3.0000
5 3 09:00:00 14:00:00 2020-04-30 5.0000
5 3 18:00:00 21:00:00 2020-04-30 3.0000
5 4 09:00:00 14:00:00 2020-04-30 5.0000
5 4 18:00:00 21:00:00 2020-04-30 3.0000
5 5 09:00:00 14:00:00 2020-04-30 5.0000
5 5 18:00:00 21:00:00 2020-04-30 3.0000
5 1 09:00:00 14:00:00 2020-05-01 5.0000
5 1 18:00:00 21:00:00 2020-05-01 3.0000
5 2 09:00:00 14:00:00 2020-05-01 5.0000
5 2 18:00:00 21:00:00 2020-05-01 3.0000
5 3 09:00:00 14:00:00 2020-05-01 5.0000
5 3 18:00:00 21:00:00 2020-05-01 3.0000
5 4 09:00:00 14:00:00 2020-05-01 5.0000
5 4 18:00:00 21:00:00 2020-05-01 3.0000
5 6 10:00:00 14:00:00 2020-05-01 4.0000
5 6 20:00:00 00:00:00 2020-05-01 4.0000
5 1 08:00:00 14:00:00 2020-05-27 6.0000
5 1 17:00:00 20:00:00 2020-05-27 3.0000
5 2 08:00:00 14:00:00 2020-05-27 6.0000
5 3 08:00:00 14:00:00 2020-05-27 6.0000
5 3 17:00:00 20:00:00 2020-05-27 3.0000
5 4 08:00:00 14:00:00 2020-05-27 6.0000
5 4 17:00:00 20:00:00 2020-05-27 3.0000
5 5 08:00:00 15:00:00 2020-05-27 7.0000
5 1 08:00:00 15:00:00 2020-07-03 7.0000
5 2 08:00:00 15:00:00 2020-07-03 7.0000
5 3 08:00:00 15:00:00 2020-07-03 7.0000
5 4 08:00:00 15:00:00 2020-07-03 7.0000
5 5 08:00:00 14:00:00 2020-07-03 6.0000
5 1 08:00:00 15:00:00 2020-08-25 7.0000
5 1 16:00:00 20:00:00 2020-08-25 4.0000
5 2 08:00:00 15:00:00 2020-08-25 7.0000
5 3 08:00:00 15:00:00 2020-08-25 7.0000
5 3 16:00:00 20:00:00 2020-08-25 4.0000
5 4 08:00:00 15:00:00 2020-08-25 7.0000
5 4 16:00:00 20:00:00 2020-08-25 4.0000
5 5 08:00:00 14:00:00 2020-08-25 6.0000
5 1 08:00:00 15:00:00 2020-10-28 7.0000
5 2 08:00:00 15:00:00 2020-10-28 7.0000
5 2 16:00:00 18:30:00 2020-10-28 2.5000
5 3 08:00:00 15:00:00 2020-10-28 7.0000
5 4 08:00:00 15:00:00 2020-10-28 7.0000
5 4 16:00:00 18:30:00 2020-10-28 2.5000
5 5 08:00:00 15:00:00 2020-10-28 7.0000
5 1 07:30:00 15:00:00 2022-09-19 7.5000
5 2 07:30:00 15:00:00 2022-09-19 7.5000
5 3 07:30:00 15:00:00 2022-09-19 7.5000
5 4 07:30:00 15:00:00 2022-09-19 7.5000
5 4 16:00:00 18:30:00 2022-09-19 2.5000
5 5 07:30:00 15:00:00 2022-09-19 7.5000
5 1 07:00:00 15:00:00 2023-06-19 8.0000
5 2 07:00:00 15:00:00 2023-06-19 8.0000
5 3 07:00:00 15:00:00 2023-06-19 8.0000
5 4 07:00:00 15:00:00 2023-06-19 8.0000
5 5 07:00:00 15:00:00 2023-06-19 8.0000
5 1 07:30:00 15:00:00 2023-09-26 7.5000
5 2 07:30:00 15:00:00 2023-09-26 7.5000
5 3 07:30:00 15:00:00 2023-09-26 7.5000
5 4 07:30:00 15:00:00 2023-09-26 7.5000
5 4 16:00:00 18:30:00 2023-09-26 2.5000
5 5 07:30:00 15:00:00 2023-09-26 7.5000

I need to know what work schedule the employee had in a specific date range.

For example, if I query the week from 4/29/2024 to 5/5/2024, I would expect to receive:

That is, the following set of records for employee 476:

employee_id date from to starts worked
00000476 2024-04-29 09:00:00 14:00:00 2024-04-01 5.0000
00000476 2024-04-30 09:00:00 14:00:00 2024-04-01 5.0000
00000476 2024-05-01 07:00:00 15:00:00 2024-05-01 8.0000
00000476 2024-05-02 07:00:00 15:00:00 2024-05-01 8.0000
00000476 2024-05-03 07:00:00 15:00:00 2024-05-01 8.0000

With this data I want to know how many hours that employee had worked that week, or automatically create the schedule for future weeks.

I've been racking my brain all day trying to find a way to make it possible, but everything I try doesn't give the expected result.

Last try was:

WITH RECURSIVE date_range (`date`) AS (
    SELECT '2023-09-25' -- start date
    UNION ALL
    SELECT `date` + INTERVAL 1 DAY FROM date_range WHERE `date` < '2023-10-01' -- end date
),
ehp AS (
    SELECT *, LEAD(validez) OVER (PARTITION BY diasem ORDER BY validez) - INTERVAL 1 DAY AS validez_fin
    FROM empleados_horarios_predeterminados
    WHERE id_pers_empleado = 5
)
SELECT 
  ehp.id_pers_empleado employee_id,
  dr.`date`,
  ehp.hora_inicio `from`,
  ehp.hora_fin `to`,
  ehp.validez `starts`,
  ehp.validez_fin `ends`,
  (TIME_TO_SEC(ehp.hora_fin) - TIME_TO_SEC(ehp.hora_inicio)) / 3600 AS worked
FROM date_range dr
JOIN ehp ON WEEKDAY(dr.`date`) + 1 = ehp.diasem
  AND ehp.validez <= dr.`date`
  AND (ehp.validez_fin >= dr.`date` OR ehp.validez_fin IS NULL);

Which gave me the correct resultset for the specific case I've been talking, but I'm encountering problems with other users with older schedule templates or multiple shifts in same day.

When I'm querying for employee 5 (an old one) in date range between "2023-09-25" and "2023-10-01", it's resulting in the following recordset:

employee_id date weekday from to starts ends worked
5 2023-09-25 1 07:00:00 15:00:00 2023-06-19 2023-09-25 8.0000
5 2023-09-26 2 07:30:00 15:00:00 2023-09-26 null 7.5000
5 2023-09-27 3 07:30:00 15:00:00 2023-09-26 null 7.5000
5 2023-09-28 4 16:00:00 18:30:00 2023-09-26 null 2.5000
5 2023-09-29 5 07:30:00 15:00:00 2023-09-26 null 7.5000
5 2023-09-30 6 20:00:00 00:00:00 2020-05-01 null -20.0000

First problem: it's not showing the shift from 07:30 to 15:00 for weekday 4.

Second problem: it's showing a record for weekday 6 from a very old schedule template, long time ago deprecated by newer ones.

Third problem: it's not calculating correctly shifts ending after 23:59, resulting in negative working hours.


Solution

  • To correctly handle the three problems stated in your update, you can add a CTE (schedules below) to identify the validez dates of the shift templates applicable for the employee and date range required:

    SET @start_date = '2023-09-25', @end_date = '2023-10-01', @employee = 5;
    
    WITH RECURSIVE date_range (dt) AS (
        SELECT @start_date
        UNION ALL
        SELECT dt + INTERVAL 1 DAY FROM date_range WHERE dt < @end_date
    ),
    schedules (validez, validez_fin) AS (
        SELECT validez, COALESCE(LEAD(validez) OVER (ORDER BY validez) - INTERVAL 1 DAY, @end_date)
        FROM empleados_horarios_predeterminados
        WHERE validez BETWEEN (SELECT MAX(validez) FROM empleados_horarios_predeterminados WHERE id_pers_empleado = @employee AND validez <= @start_date)
                          AND (SELECT MAX(validez) FROM empleados_horarios_predeterminados WHERE id_pers_empleado = @employee AND validez <= @end_date)
        AND id_pers_empleado = @employee
        GROUP BY validez
    )
    SELECT
        dr.dt,
        ehp.*,
        (TIME_TO_SEC(IF(ehp.hora_fin < ehp.hora_inicio, ehp.hora_fin + INTERVAL 24 HOUR, ehp.hora_fin)) - TIME_TO_SEC(ehp.hora_inicio)) / 3600 AS worked
    FROM date_range dr
    JOIN schedules s
        ON dr.dt BETWEEN s.validez AND s.validez_fin
    JOIN empleados_horarios_predeterminados ehp
        ON ehp.id_pers_empleado = @employee
        AND s.validez = ehp.validez
        AND WEEKDAY(dr.dt) + 1 = ehp.diasem
    ORDER BY dr.dt, ehp.hora_inicio;
    

    Here's another db<>fiddle.

    Or you could add a correlated subquery to the result of the date_range CTE:

    SET @start_date = '2023-09-25', @end_date = '2023-10-01', @employee = 5;
    
    WITH RECURSIVE date_range (dt) AS (
        SELECT @start_date
        UNION ALL
        SELECT dt + INTERVAL 1 DAY FROM date_range WHERE dt < @end_date
    ),
    dr_with_validez AS (
        SELECT *,
            (
                SELECT MAX(validez)
                FROM empleados_horarios_predeterminados
                WHERE id_pers_empleado = @employee
                AND validez <= dr.dt
            ) AS validez
        FROM date_range dr
    )
    SELECT
        dr.dt,
        ehp.*,
        (TIME_TO_SEC(IF(ehp.hora_fin < ehp.hora_inicio, ehp.hora_fin + INTERVAL 24 HOUR, ehp.hora_fin)) - TIME_TO_SEC(ehp.hora_inicio)) / 3600 AS worked
    FROM dr_with_validez dr
    JOIN empleados_horarios_predeterminados ehp
        ON ehp.id_pers_empleado = @employee
        AND dr.validez = ehp.validez
        AND WEEKDAY(dr.dt) + 1 = ehp.diasem
    ORDER BY dr.dt, ehp.hora_inicio;
    

    db<>fiddle

    We don't know all your use cases but you might want to consider splitting the current table to separate the shift template and the days/times of the individual shifts:

    shift_templates (id, id_pers_empleado, validez)
    shift_template_shifts (shift_template_id, diasem, hora_inicio, hora_fin)