Split row data base on timestamp SQL Oracle - sql

Good day everyone. I have a table as below. Duration is the time from current state to next state.
Timestamp
State
Duration(minutes)
10/9/2022 8:50:00 AM
A
35
10/9/2022 9:25:00 AM
B
10
10/9/2022 9:35:00 AM
C
...
How do I split data at 9:00 AM of each day like below:
Timestamp
State
Duration(minutes)
10/9/2022 8:50:00 AM
A
10
10/9/2022 9:00:00 AM
A
25
10/9/2022 9:25:00 AM
B
10
10/9/2022 9:35:00 AM
C
...
Thank you.

Use a row-generator function to generate extra rows when the timestamp is before 09:00 and the next timestamp is after 09:00 (and calculate the diff value rather than storing it in the table):
SELECT l.ts AS timestamp,
t.state,
ROUND((l.next_ts - l.ts) * 24 * 60, 2) As diff
FROM (
SELECT timestamp,
LEAD(timestamp) OVER (ORDER BY timestamp) AS next_timestamp,
state
FROM table_name
) t
CROSS APPLY (
SELECT GREATEST(
t.timestamp,
TRUNC(t.timestamp - INTERVAL '9' HOUR) + INTERVAL '9' HOUR + LEVEL - 1
) AS ts,
LEAST(
t.next_timestamp,
TRUNC(t.timestamp - INTERVAL '9' HOUR) + INTERVAL '9' HOUR + LEVEL
) AS next_ts
FROM DUAL
CONNECT BY
TRUNC(t.timestamp - INTERVAL '9' HOUR) + INTERVAL '9' HOUR + LEVEL - 1 < t.next_timestamp
) l;
Which, for your sample data:
CREATE TABLE table_name (Timestamp, State) AS
SELECT DATE '2022-10-09' + INTERVAL '08:50' HOUR TO MINUTE, 'A' FROM DUAL UNION ALL
SELECT DATE '2022-10-09' + INTERVAL '09:25' HOUR TO MINUTE, 'B' FROM DUAL UNION ALL
SELECT DATE '2022-10-09' + INTERVAL '09:35' HOUR TO MINUTE, 'C' FROM DUAL UNION ALL
SELECT DATE '2022-10-12' + INTERVAL '09:35' HOUR TO MINUTE, 'D' FROM DUAL;
Outputs:
TIMESTAMP
STATE
DIFF
2022-10-09 08:50:00
A
10
2022-10-09 09:00:00
A
25
2022-10-09 09:25:00
B
10
2022-10-09 09:35:00
C
1405
2022-10-10 09:00:00
C
1440
2022-10-11 09:00:00
C
1440
2022-10-12 09:00:00
C
35
2022-10-12 09:35:00
D
null
fiddle

Related

find number of hours gap when no one is working

i have a table view of table
taking scenario of a hotel that works 24/7 non stop, i want to calculate total number of hours in a month when no one is present .
in image you can see we have shift hours in 24hh format, shifts are 8 hours long and different employees may have different week offs , we have date range for which this shift would be valid , i.e. a typical month and we have planned leaves when employee is completely off.
can you suggest oracle SQL to find total number of hours when no employee is working in a month.
You can generate the shifts and then find the shifts where no-one worked:
WITH shifts (shift_start) AS (
SELECT DATE '2022-07-01' + INTERVAL '6' HOUR + INTERVAL '8' HOUR * (LEVEL - 1)
FROM DUAL
CONNECT BY
DATE '2022-07-01' + INTERVAL '6' HOUR + INTERVAL '8' HOUR * (LEVEL - 1)
< DATE '2022-08-01'
)
SELECT s.shift_start
FROM shifts s
WHERE NOT EXISTS(
SELECT 1
FROM work_master w
WHERE w.shift_date_frm <= s.shift_start
AND s.shift_start < w.shift_date_to + INTERVAL '1' DAY
AND ':' || w.week_off_day || ':' NOT LIKE '%:' || TO_CHAR(s.shift_start, 'Dy') || ':%'
AND EXTRACT(HOUR FROM CAST(s.shift_start AS TIMESTAMP)) = w.shift_start
AND (
(
w.vac_date_frm IS NULL
AND w.vac_date_to IS NULL
)
OR NOT (
w.vac_date_frm <= s.shift_start
AND s.shift_start < w.vac_date_to + INTERVAL '1' DAY
)
)
)
Which, for the sample data:
CREATE TABLE work_master (
employee_name,
shift_start,
shift_end,
shift_date_frm,
shift_date_to,
vac_date_frm,
vac_date_to,
week_off_day
) AS
SELECT 'emp1', 22, 6, DATE '2022-07-01', DATE '2022-07-31', NULL, NULL, 'Sat:Sun' FROM DUAL UNION ALL
SELECT 'emp2', 14, 22, DATE '2022-07-01', DATE '2022-07-31', NULL, NULL, 'Sat:Sun' FROM DUAL UNION ALL
SELECT 'emp3', 6, 14, DATE '2022-07-01', DATE '2022-07-31', DATE '2022-07-27', DATE '2022-07-27', 'Sat:Sun' FROM DUAL UNION ALL
SELECT 'emp4', 14, 22, DATE '2022-07-01', DATE '2022-07-31', NULL, NULL, 'Fri:Sat' FROM DUAL UNION ALL
SELECT 'emp5', 22, 6, DATE '2022-07-01', DATE '2022-07-31', NULL, NULL, 'Wed:Thu' FROM DUAL;
Outputs:
SHIFT_START
2022-07-02 06:00:00 (Sat)
2022-07-02 14:00:00 (Sat)
2022-07-03 06:00:00 (Sun)
2022-07-09 06:00:00 (Sat)
2022-07-09 14:00:00 (Sat)
2022-07-10 06:00:00 (Sun)
2022-07-16 06:00:00 (Sat)
2022-07-16 14:00:00 (Sat)
2022-07-17 06:00:00 (Sun)
2022-07-23 06:00:00 (Sat)
2022-07-23 14:00:00 (Sat)
2022-07-24 06:00:00 (Sun)
2022-07-27 06:00:00 (Wed)
2022-07-30 06:00:00 (Sat)
2022-07-30 14:00:00 (Sat)
2022-07-31 06:00:00 (Sun)
If you just want the total hours then:
WITH shifts (shift_start) AS (
SELECT DATE '2022-07-01' + INTERVAL '6' HOUR + INTERVAL '8' HOUR * (LEVEL - 1)
FROM DUAL
CONNECT BY
DATE '2022-07-01' + INTERVAL '6' HOUR + INTERVAL '8' HOUR * (LEVEL - 1)
< DATE '2022-08-01'
)
SELECT COUNT(*) * 8 AS hours_not_worked
FROM shifts s
WHERE NOT EXISTS(
SELECT 1
FROM work_master w
WHERE w.shift_date_frm <= s.shift_start
AND s.shift_start < w.shift_date_to + INTERVAL '1' DAY
AND ':' || w.week_off_day || ':' NOT LIKE '%:' || TO_CHAR(s.shift_start, 'Dy') || ':%'
AND EXTRACT(HOUR FROM CAST(s.shift_start AS TIMESTAMP)) = w.shift_start
AND (
(
w.vac_date_frm IS NULL
AND w.vac_date_to IS NULL
)
OR NOT (
w.vac_date_frm <= s.shift_start
AND s.shift_start < w.vac_date_to + INTERVAL '1' DAY
)
)
)
Which outputs:
HOURS_NOT_WORKED
128
db<>fiddle here

Oracle modify a recursive CTE to JOIN rows with a table

I have recursive CTE, which is working fine. It's designed to generate a numbers of rows based on the count(*) of the locations table, which in this test CASE is 15 or stop before crossing midnight.
My goal is to populate the schedule table. The schedule_id can be hard coded to 1 for now as I plan to wrap this code in a procedure to pass in values.
First, instead of creating rows for a single date I would prefer to use the function generate_dates_pipelined, which creates rows for a range of dates. For each date add the number of seconds ie 83760, which = a time 23:16:00 to create a start_time.
Second, associating a location_id with a date range row being generated.
Note: a unique location_id must be associated with every row. Secondly, though in my test CASE the location_id are consequently ordered that may not be the case in production. Third, there are only 3 rows instead of 15 because the next row would have crossed midnight.
Thanks in advance for your expertise and to all that answer.
Current output:
START_DATE END_DATE
08192021 23:30:00 08192021 23:35:00
08192021 23:40:00 08192021 23:45:00
08192021 23:50:00 08192021 23:55:00
Desired output:
SCHEDULE_ID LOCATION_ID START_DATE END_DATE
1 1 08192021 23:30:00 08192021 23:35:00
1 2 08192021 23:40:00 08192021 23:45:00
1 3 08192021 23:50:00 08192021 23:55:00
ALTER SESSION SET NLS_DATE_FORMAT = 'MMDDYYYY HH24:MI:SS';
CREATE OR REPLACE TYPE nt_date IS TABLE OF DATE;
CREATE OR REPLACE FUNCTION generate_dates_pipelined(
p_from IN DATE,
p_to IN DATE
)
RETURN nt_date PIPELINED DETERMINISTIC
IS
v_start DATE := TRUNC(LEAST(p_from, p_to));
v_end DATE := TRUNC(GREATEST(p_from, p_to));
BEGIN
LOOP
PIPE ROW (v_start);
EXIT WHEN v_start >= v_end;
v_start := v_start + INTERVAL '1' DAY;
END LOOP;
RETURN;
END generate_dates_pipelined;
/
create table schedule(
schedule_id NUMBER(4),
location_id number(4),
start_date DATE,
end_date DATE,
CONSTRAINT start_min check (start_date=trunc(start_date,'MI')),
CONSTRAINT end_min check (end_date=trunc(end_date,'MI')),
CONSTRAINT end_gt_start CHECK (end_date >= start_date),
CONSTRAINT same_day CHECK (TRUNC(end_date) = TRUNC(start_date))
);
CREATE TABLE locations AS
SELECT level AS location_id,
'Door ' || level AS location_name,
CASE round(dbms_random.value(1,3))
WHEN 1 THEN 'A'
WHEN 2 THEN 'T'
WHEN 3 THEN 'T'
END AS location_type
FROM dual
CONNECT BY level <= 15;
ALTER TABLE locations
ADD ( CONSTRAINT locations_pk
PRIMARY KEY (location_id));
WITH input (start_time) AS (
SELECT TRUNC(SYSDATE) + INTERVAL '23:30' HOUR TO MINUTE FROM DUAL
)
SELECT start_time + (LEVEL-1) * INTERVAL '10' MINUTE
AS start_date,
start_time + (LEVEL-1) * INTERVAL '10' MINUTE + INTERVAL '5' MINUTE
AS end_date
FROM input
CONNECT BY (LEVEL-1) * INTERVAL '10' MINUTE < INTERVAL '1' DAY
AND LEVEL <= (SELECT COUNT(*) FROM locations)
AND start_time + (LEVEL-1) * INTERVAL '10' MINUTE < TRUNC(start_time) + INTERVAL '1' DAY;
One statement to generate the data:
-- The date range
WITH args (start_date, end_date) AS (
SELECT current_date, current_date + 2 FROM dual
)
-- generate entire date range
, dates (adate) AS (
SELECT trunc(start_date) FROM args UNION ALL
SELECT adate + INTERVAL '1' DAY FROM dates, args
WHERE adate < end_date - 1
)
-- generate initial date/times
, init_datetimes (adatetime) AS (
SELECT adate + INTERVAL '83760' SECOND FROM dates
)
-- generate the rest of the times per date until midnight
, init_schedules (adatetime, end_time, n) AS (
SELECT adatetime, adatetime + INTERVAL '5' MINUTE, 1 FROM init_datetimes UNION ALL
SELECT adatetime + INTERVAL '10' MINUTE, adatetime + INTERVAL '10' MINUTE + INTERVAL '5' MINUTE, n+1
FROM init_schedules
WHERE trunc(adatetime) = trunc(adatetime + 2*INTERVAL '10' MINUTE) -- stop before midnight
-- AND n < 10 -- Just for protection
)
-- generate some test locations
, locations (alocation) AS (
SELECT 1 FROM dual UNION
SELECT 3 FROM dual UNION
SELECT 6 FROM dual UNION
SELECT 7 FROM dual UNION
SELECT 9 FROM dual UNION
SELECT 10 FROM dual
)
-- add row_number to location list
, location_list (alocation, n) AS (
SELECT alocation, ROW_NUMBER() OVER (ORDER BY alocation) FROM locations
)
-- Now apply locations to the schedule for each date/time
, location_dates (alocation, adatetime, end_time, n) AS (
SELECT alocation, adatetime, end_time, dat.n
FROM location_list loc
JOIN init_schedules dat
ON loc.n = dat.n -- one location per row per date
)
SELECT *
FROM location_dates
ORDER BY adatetime, alocation
;
The result:
loc date/time end_time n
1 08192021 23:16:00 08192021 23:21:00 1
3 08192021 23:26:00 08192021 23:31:00 2
6 08192021 23:36:00 08192021 23:41:00 3
7 08192021 23:46:00 08192021 23:51:00 4
1 08202021 23:16:00 08202021 23:21:00 1
3 08202021 23:26:00 08202021 23:31:00 2
6 08202021 23:36:00 08202021 23:41:00 3
7 08202021 23:46:00 08202021 23:51:00 4
1 08212021 23:16:00 08212021 23:21:00 1
3 08212021 23:26:00 08212021 23:31:00 2
6 08212021 23:36:00 08212021 23:41:00 3
7 08212021 23:46:00 08212021 23:51:00 4
Nice and compact. I do cross midnight now as another process was fixed that could not handle that situation. Thanks to all who answered.
WITH params AS
(
SELECT 1 AS schedule_id,
TO_DATE ( '2021-08-21 00:00:00'
, 'YYYY-MM-DD HH24:MI:SS'
) AS base_date
, INTERVAL '83760' SECOND AS offset
, INTERVAL '10' MINUTE AS incr
, INTERVAL '5' MINUTE AS duration
FROM dual
)
SELECT p.schedule_id
, l.location_id
, p.base_date
, p.base_date + offset
+ (incr * (ROWNUM - 1)) AS start_date
, p.base_date + offset
+ (incr * (ROWNUM - 1))
+ p.duration AS end_date
FROM locations l
CROSS JOIN params p
ORDER BY start_date
;

Group by with Unix time stamps

I am trying write a query where time stamps are in Unix format.
The objective of the query is group by these time stamps in five minute segments and to count each unique Id in those segments.
Is there a simple way of doing this?
The result looking for this
Time_utc Id count
25/07/2019 1600 1 3
25/07/2019 1600 2 1
25/07/2019 1605 1 4
You haven't shown data, so as a starting point you can group the Unix timestamps by dividing by 300 (for 5 minutes worth of seconds):
select 300 * floor(unix_ts/300) as unix_five_minute,
timestamp '1970-01-01 00:00:00 UTC'
+ (300*floor(unix_ts/300)) * interval '1' second as oracle_timestamp,
count(*)
from cte2
group by floor(unix_ts/300);
or if you have millisecond precision adjust by a factor of 1000:
select 300000 * floor(unix_ts/300000) as unix_five_minute,
timestamp '1970-01-01 00:00:00 UTC'
+ (300*floor(unix_ts/300000)) * interval '1' second as oracle_timestamp,
count(*)
from cte2
group by floor(unix_ts/300000);
Demo using made-up data generated from current time:
-- CTEs to generate some sample data
with cte1 (oracle_interval) as (
select systimestamp - level * interval '42' second
- timestamp '1970-01-01 00:00:00.0 UTC'
from dual
connect by level <= 30
),
cte2 (unix_ts) as (
select trunc(
extract(day from oracle_interval) * 86400000
+ extract(hour from oracle_interval) * 3600000
+ extract(minute from oracle_interval) * 60000
+ extract(second from oracle_interval) * 1000
)
from cte1
)
-- actual query
select 300000 * floor(unix_ts/300000) as unix_five_minute,
timestamp '1970-01-01 00:00:00 UTC'
+ (300*floor(unix_ts/300000)) * interval '1' second as oracle_timestamp,
count(*)
from cte2
group by floor(unix_ts/300000);
UNIX_FIVE_MINUTE ORACLE_TIMESTAMP COUNT(*)
---------------- ------------------------- ----------------
1564072500000 2019-07-25 16:35:00.0 UTC 7
1564072200000 2019-07-25 16:30:00.0 UTC 7
1564071600000 2019-07-25 16:20:00.0 UTC 4
1564071900000 2019-07-25 16:25:00.0 UTC 8
1564072800000 2019-07-25 16:40:00.0 UTC 4
Unix time stamps such as 155639.600 or 155639.637
Those are unusual values; Unix/epoch times are usually 10-digit numbers, or 13 digits for millisecond precision. Assuming (or rather, guessing) that they are tenths of a second for some reason:
-- CTE for sample data
with cte (unix_ts) as (
select 155639.600 from dual
union all
select 155639.637 from dual
)
-- actual query
select 300 * floor(unix_ts*10000/300) as unix_five_minute,
timestamp '1970-01-01 00:00:00 UTC'
+ (300*floor(unix_ts*10000/300)) * interval '1' second as oracle_timestamp,
count(*)
from cte
group by floor(unix_ts*10000/300);
UNIX_FIVE_MINUTE ORACLE_TIMESTAMP COUNT(*)
---------------- ------------------------- ----------------
1556396100 2019-04-27 20:15:00.0 UTC 1
1556395800 2019-04-27 20:10:00.0 UTC 1
The 10000/300 could be simplified to 100/3, but I think it's clearer left as it is.

cast two separate columns which has hour ( datatype number) and minutes ( datatype number) to time datatype and subtract 90 minutes in oracle

I have two separate columns for hours and minutes in my table and I have a report where i should be subtracting 90 minutes from total time put together or ( 1 hour from hour field) and 30 minutes from minutes field. The output can be in minutes or hours.
I tried "to_char ( hours_column -1,'00' ) || ':' || to_char ( minutes_column -30,'00' ) AS "MAX_TIME" " - this fails when I have time like 9:00 I get 8:-30 as the output when I need to get 7:30.
I came up with some sql code with DATEADD and cast functions which worked but it fails when I implement it in Oracle.
Select Substring(Cast(DATEADD(minute, -90, Cast(hourscolumn + ':' + minutes column as Time)) as varchar(20)),1,5) as max_time
Can someone help me to implement the above code in Oracle? I'm just trying to deduct 90 minutes by putting the hours and minutes columns together.
Something like this?
test CTE represents your data. How come you got that (bad) idea? Who/what prevents you from storing 32 hours and 87 minutes into those columns?
query itself contains
time: the way you create a valid date value. It'll fail if hours and/or minutes are invalid (such as previously mentioned 32:87)
subtracted: subtract 90 minutes from time; (24 * 60) represents 24 hours in a day, 60 minutes in an hour. It'll contain both date and time component
the final result is achieved by applying to_char with appropriate format mask (hh24:mi) to the subtracted value
SQL> alter session set nls_Date_format = 'dd.mm.yyyy hh24:mi';
Session altered.
SQL> with test (hours, minutes) as
2 (select '09', '00' from dual union all
3 select '23', '30' from dual union all
4 select '00', '20' from dual
5 )
6 select hours,
7 minutes,
8 to_date(hours||minutes, 'hh24mi') time,
9 --
10 to_date(hours||minutes, 'hh24mi') - 90 / (24 * 60) subtracted,
11 --
12 to_char(to_date(hours||minutes, 'hh24mi') - 90 / (24 * 60), 'hh24:mi') result
13 from test;
HO MI TIME SUBTRACTED RESUL
-- -- ---------------- ---------------- -----
09 00 01.07.2019 09:00 01.07.2019 07:30 07:30
23 30 01.07.2019 23:30 01.07.2019 22:00 22:00
00 20 01.07.2019 00:20 30.06.2019 22:50 22:50
SQL>
Use NUMTODSINTERVAL to convert the hours and minutes to INTERVAL data types and then you can subtract INTERVAL '90' MINUTE and EXTRACT the resulting hour and minute components.
Oracle Setup:
CREATE TABLE table_name ( hours_column, minutes_column ) AS
SELECT 0, 0 FROM DUAL UNION ALL
SELECT 1, 30 FROM DUAL UNION ALL
SELECT 2, 45 FROM DUAL UNION ALL
SELECT 3, 0 FROM DUAL UNION ALL
SELECT 27, 59 FROM DUAL
Query:
SELECT EXTRACT( HOUR FROM time ) + EXTRACT( DAY FROM time ) * 24 AS hours,
EXTRACT( MINUTE FROM time ) AS minutes,
time,
TO_CHAR( EXTRACT( HOUR FROM time ) + EXTRACT( DAY FROM time ) * 24, '00' )
|| ':' || TO_CHAR( ABS( EXTRACT( MINUTE FROM time ) ), 'FM00' ) AS as_string
FROM (
SELECT NUMTODSINTERVAL( hours_column, 'HOUR' )
+ NUMTODSINTERVAL( minutes_column, 'MINUTE' )
- INTERVAL '90' MINUTE AS time
FROM table_name
)
Output:
HOURS | MINUTES | TIME | AS_STRING
----: | ------: | :---------------------------- | :--------
-1 | -30 | -000000000 01:30:00.000000000 | -01:30
0 | 0 | +000000000 00:00:00.000000000 | 00:00
1 | 15 | +000000000 01:15:00.000000000 | 01:15
1 | 30 | +000000000 01:30:00.000000000 | 01:30
26 | 29 | +000000001 02:29:00.000000000 | 26:29
db<>fiddle here

how to convert HH:MM representation to minutes in oracle sql

how to convert varchar(hh:mm) to minutes in oracle sql.
For example:
HH:MM Minutes
08:00 480
08:45 525
07:57 477
This will work even if the duration is 24 hours or greater:
SQL Fiddle
Oracle 11g R2 Schema Setup:
CREATE TABLE durations ( duration ) AS
SELECT '00:30' FROM DUAL UNION ALL
SELECT '07:57' FROM DUAL UNION ALL
SELECT '08:00' FROM DUAL UNION ALL
SELECT '12:00' FROM DUAL UNION ALL
SELECT '20:01' FROM DUAL UNION ALL
SELECT '23:59' FROM DUAL UNION ALL
SELECT '24:00' FROM DUAL UNION ALL
SELECT '24:59' FROM DUAL;
Query 1:
SELECT duration,
( (
DATE '1970-01-01'
+ NUMTODSINTERVAL( SUBSTR( duration, 1, INSTR( duration, ':' ) - 1 ), 'HOUR' )
+ NUMTODSINTERVAL( SUBSTR( duration, INSTR( duration, ':' ) + 1 ), 'MINUTE' )
)
- DATE '1970-01-01'
) * 24 * 60 AS Minutes
FROM durations
Results:
| DURATION | MINUTES |
|----------|---------|
| 00:30 | 30 |
| 07:57 | 477 |
| 08:00 | 480 |
| 12:00 | 720 |
| 20:01 | 1201 |
| 23:59 | 1439 |
| 24:00 | 1440 |
| 24:59 | 1499 |
However, there is an INTERVAL DAY TO SECOND data type that would be better suited to your data:
CREATE TABLE your_table (
duration INTERVAL DAY TO SECOND
);
Then you can just do:
INSERT INTO your_table ( duration ) VALUES ( INTERVAL '08:00' HOUR TO MINUTE );
To get the number of minutes you can then simply do:
SELECT ( ( DATE '1970-01-01' + duration ) - DATE '1970-01-01' ) *24*60 AS minutes
FROM your_table
Try this
TO_NUMBER(SUBSTR('(08:00)',2,INSTR('(08:00)',':')-2))*60+TO_NUMBER(SUBSTR('(08:00)',INSTR('(08:00)',':')+1,2))
If you can convert your input to a real date first, the task becomes much easier. Here, I have shamelessly appended the time to a fake date to create a date such as 2017-01-01 00:30. To find out the number of minutes since midnight, you simply subtract the date for "midnight". It will return the difference in days, so you need to multiply by number of minutes per day to get what you want.
select time
,(to_date('2017-01-01 ' || time, 'yyyy-mm-dd hh24:mi') - date '2017-01-01') * 24 * 60 as minutes
from (select '00:30' as time from dual union all
select '08:00' as time from dual union all
select '08:30' as time from dual union all
select '12:00' as time from dual union all
select '23:59' as time from dual
);
Here is some sample input and output
time minutes
==== =======
00:30 30
08:00 480
08:30 510
12:00 720
23:59 1 439
If you require to Print 08:00 hours as 480 minutes,
Extract the Digit before : and multply with 60 and add the digit after :. So you can convert the HH:MM representation in to minutes.
SELECT REGEXP_SUBSTR(ATT.workdur,'[^:]+',1,1)*60 + REGEXP_SUBSTR(ATT.workdur,'[^:]+',1,2) MINUTES FROM DUAL;