I like to tackle these problems in stages. The solution may not be the most elegant or efficient, but it works.
First, let's get some data we can work with:
DROP TEMPORARY TABLE IF EXISTS OST_TICKET;
CREATE TEMPORARY TABLE OST_TICKET (
TKT_ID INT AUTO_INCREMENT PRIMARY KEY,
CREATED DATETIME,
CLOSED DATETIME
);
INSERT INTO OST_TICKET (CREATED, CLOSED)
VALUES
('2023-07-31 08:15:00', '2023-08-04 14:33:48'),
('2023-07-30 07:15:00', '2023-08-03 19:33:12'),
('2023-08-01 09:45:00', '2023-08-01 12:33:27'),
('2023-08-03 12:45:00', '2023-08-03 17:21:18'),
('2023-08-03 14:13:00', '2023-08-03 16:11:33');
Now, what we need to know is where the timespans overlap the shifts. I took the approach of finding all the shifts that overlap the timespan, adjusting the first and last shift and then adding them together. The first thing to do is find all the days in the time span. We can do that with the following recursive CTE:
Code Segment 1
WITH RECURSIVE ALL_DAYS AS (
SELECT
TKT_ID,
CREATED,
CLOSED,
DATE(CREATED) AS WORK_DATE
FROM
OST_TICKET
UNION ALL
SELECT
TKT_ID,
CREATED,
CLOSED,
DATE_ADD(WORK_DATE, INTERVAL 1 DAY)
FROM
ALL_DAYS
WHERE
WORK_DATE < DATE(CLOSED)
),
For the sample data set, this produces:
+--------+---------------------+---------------------+------------+
| TKT_ID | CREATED | CLOSED | WORK_DATE |
+--------+---------------------+---------------------+------------+
| 1 | 2023-07-31 08:15:00 | 2023-08-04 14:33:48 | 2023-07-31 |
| 1 | 2023-07-31 08:15:00 | 2023-08-04 14:33:48 | 2023-08-01 |
| 1 | 2023-07-31 08:15:00 | 2023-08-04 14:33:48 | 2023-08-02 |
| 1 | 2023-07-31 08:15:00 | 2023-08-04 14:33:48 | 2023-08-03 |
| 1 | 2023-07-31 08:15:00 | 2023-08-04 14:33:48 | 2023-08-04 |
| 2 | 2023-07-30 07:15:00 | 2023-08-03 19:33:12 | 2023-07-30 |
| 2 | 2023-07-30 07:15:00 | 2023-08-03 19:33:12 | 2023-07-31 |
| 2 | 2023-07-30 07:15:00 | 2023-08-03 19:33:12 | 2023-08-01 |
| 2 | 2023-07-30 07:15:00 | 2023-08-03 19:33:12 | 2023-08-02 |
| 2 | 2023-07-30 07:15:00 | 2023-08-03 19:33:12 | 2023-08-03 |
| 3 | 2023-08-01 09:45:00 | 2023-08-01 12:33:27 | 2023-08-01 |
| 4 | 2023-08-03 12:45:00 | 2023-08-03 17:21:18 | 2023-08-03 |
| 5 | 2023-08-03 14:13:00 | 2023-08-03 16:11:33 | 2023-08-03 |
+--------+---------------------+---------------------+------------+
From there, we can create rows for each shift by cross joining ALL_DAYS
with a time table. This stage can also eliminate those weekends. So the second CTE is:
Code Segment 2
ALLSHIFTS AS (
SELECT
TKT_ID,
CREATED,
CLOSED,
TIMESTAMPADD(HOUR, SHIFTS.S_START, WORK_DATE) AS SHIFT_START,
TIMESTAMPADD(HOUR, SHIFTS.S_END, WORK_DATE) AS SHIFT_END
FROM
ALL_DAYS
CROSS JOIN ( SELECT 9 AS S_START
,13 AS S_END
UNION ALL
SELECT 14
,18) AS SHIFTS
WHERE
DAYOFWEEK(WORK_DATE) NOT IN (1, 7)
),
Which produces:
+--------+---------------------+---------------------+---------------------+---------------------+
| TKT_ID | CREATED | CLOSED | SHIFT_START | SHIFT_END |
+--------+---------------------+---------------------+---------------------+---------------------+
| 1 | 2023-07-31 08:15:00 | 2023-08-04 14:33:48 | 2023-07-31 09:00:00 | 2023-07-31 13:00:00 |
| 1 | 2023-07-31 08:15:00 | 2023-08-04 14:33:48 | 2023-07-31 14:00:00 | 2023-07-31 18:00:00 |
| 1 | 2023-07-31 08:15:00 | 2023-08-04 14:33:48 | 2023-08-01 09:00:00 | 2023-08-01 13:00:00 |
| 1 | 2023-07-31 08:15:00 | 2023-08-04 14:33:48 | 2023-08-01 14:00:00 | 2023-08-01 18:00:00 |
| 1 | 2023-07-31 08:15:00 | 2023-08-04 14:33:48 | 2023-08-02 09:00:00 | 2023-08-02 13:00:00 |
| 1 | 2023-07-31 08:15:00 | 2023-08-04 14:33:48 | 2023-08-02 14:00:00 | 2023-08-02 18:00:00 |
| 1 | 2023-07-31 08:15:00 | 2023-08-04 14:33:48 | 2023-08-03 09:00:00 | 2023-08-03 13:00:00 |
| 1 | 2023-07-31 08:15:00 | 2023-08-04 14:33:48 | 2023-08-03 14:00:00 | 2023-08-03 18:00:00 |
| 1 | 2023-07-31 08:15:00 | 2023-08-04 14:33:48 | 2023-08-04 09:00:00 | 2023-08-04 13:00:00 |
| 1 | 2023-07-31 08:15:00 | 2023-08-04 14:33:48 | 2023-08-04 14:00:00 | 2023-08-04 18:00:00 |
| 2 | 2023-07-30 07:15:00 | 2023-08-03 19:33:12 | 2023-07-31 09:00:00 | 2023-07-31 13:00:00 |
| 2 | 2023-07-30 07:15:00 | 2023-08-03 19:33:12 | 2023-07-31 14:00:00 | 2023-07-31 18:00:00 |
| 2 | 2023-07-30 07:15:00 | 2023-08-03 19:33:12 | 2023-08-01 09:00:00 | 2023-08-01 13:00:00 |
| 2 | 2023-07-30 07:15:00 | 2023-08-03 19:33:12 | 2023-08-01 14:00:00 | 2023-08-01 18:00:00 |
| 2 | 2023-07-30 07:15:00 | 2023-08-03 19:33:12 | 2023-08-02 09:00:00 | 2023-08-02 13:00:00 |
| 2 | 2023-07-30 07:15:00 | 2023-08-03 19:33:12 | 2023-08-02 14:00:00 | 2023-08-02 18:00:00 |
| 2 | 2023-07-30 07:15:00 | 2023-08-03 19:33:12 | 2023-08-03 09:00:00 | 2023-08-03 13:00:00 |
| 2 | 2023-07-30 07:15:00 | 2023-08-03 19:33:12 | 2023-08-03 14:00:00 | 2023-08-03 18:00:00 |
| 3 | 2023-08-01 09:45:00 | 2023-08-01 12:33:27 | 2023-08-01 09:00:00 | 2023-08-01 13:00:00 |
| 3 | 2023-08-01 09:45:00 | 2023-08-01 12:33:27 | 2023-08-01 14:00:00 | 2023-08-01 18:00:00 |
| 4 | 2023-08-03 12:45:00 | 2023-08-03 17:21:18 | 2023-08-03 09:00:00 | 2023-08-03 13:00:00 |
| 4 | 2023-08-03 12:45:00 | 2023-08-03 17:21:18 | 2023-08-03 14:00:00 | 2023-08-03 18:00:00 |
| 5 | 2023-08-03 14:13:00 | 2023-08-03 16:11:33 | 2023-08-03 09:00:00 | 2023-08-03 13:00:00 |
| 5 | 2023-08-03 14:13:00 | 2023-08-03 16:11:33 | 2023-08-03 14:00:00 | 2023-08-03 18:00:00 |
+--------+---------------------+---------------------+---------------------+---------------------+
Now it's time to pull in the cases where start and end times are inside the shifts. Note that we also want to eliminate any situations that don't make sense. This CTE does both steps:
Code Segment 4
ACTUALS AS (
SELECT
TKT_ID,
CREATED,
SHIFT_START,
GREATEST(CREATED, SHIFT_START) AS ACTUAL_START,
CLOSED,
SHIFT_END,
LEAST(CLOSED, SHIFT_END) AS ACTUAL_END
FROM
ALLSHIFTS
WHERE
GREATEST(CREATED, SHIFT_START) < LEAST(CLOSED, SHIFT_END)
)
Producing:
+--------+---------------------+---------------------+
| TKT_ID | ACTUAL_START | ACTUAL_END |
+--------+---------------------+---------------------+
| 1 | 2023-07-31 09:00:00 | 2023-07-31 13:00:00 |
| 1 | 2023-07-31 14:00:00 | 2023-07-31 18:00:00 |
| 1 | 2023-08-01 09:00:00 | 2023-08-01 13:00:00 |
| 1 | 2023-08-01 14:00:00 | 2023-08-01 18:00:00 |
| 1 | 2023-08-02 09:00:00 | 2023-08-02 13:00:00 |
| 1 | 2023-08-02 14:00:00 | 2023-08-02 18:00:00 |
| 1 | 2023-08-03 09:00:00 | 2023-08-03 13:00:00 |
| 1 | 2023-08-03 14:00:00 | 2023-08-03 18:00:00 |
| 1 | 2023-08-04 09:00:00 | 2023-08-04 13:00:00 |
| 1 | 2023-08-04 14:00:00 | 2023-08-04 14:33:48 |
| 2 | 2023-07-31 09:00:00 | 2023-07-31 13:00:00 |
| 2 | 2023-07-31 14:00:00 | 2023-07-31 18:00:00 |
| 2 | 2023-08-01 09:00:00 | 2023-08-01 13:00:00 |
| 2 | 2023-08-01 14:00:00 | 2023-08-01 18:00:00 |
| 2 | 2023-08-02 09:00:00 | 2023-08-02 13:00:00 |
| 2 | 2023-08-02 14:00:00 | 2023-08-02 18:00:00 |
| 2 | 2023-08-03 09:00:00 | 2023-08-03 13:00:00 |
| 2 | 2023-08-03 14:00:00 | 2023-08-03 18:00:00 |
| 3 | 2023-08-01 09:45:00 | 2023-08-01 12:33:27 |
| 4 | 2023-08-03 12:45:00 | 2023-08-03 13:00:00 |
| 4 | 2023-08-03 14:00:00 | 2023-08-03 17:21:18 |
| 5 | 2023-08-03 14:13:00 | 2023-08-03 16:11:33 |
+--------+---------------------+---------------------+
And with that, all that is left is to calculated the date differences and sum the hours.
Code Segment 4
SELECT
TKT_ID,
ROUND(
SUM(
TIMESTAMPDIFF( SECOND
,ACTUAL_START
,ACTUAL_END )
) / 3600.0
,2
) AS HOURS_WORKED
FROM
ACTUALS
GROUP BY
TKT_ID
ORDER BY
TKT_ID
;
Which, for my sample set, produces these results:
+--------+--------------+
| TKT_ID | HOURS_WORKED |
+--------+--------------+
| 1 | 36.56 |
| 2 | 32.00 |
| 3 | 2.81 |
| 4 | 3.61 |
| 5 | 1.98 |
+--------+--------------+
To run the actual solution you have to combine all four code segments into one query. You will also need to adjust field and table names to suit your unique situation.