Oracle converting working SQL with CTE to procedure - sql

I have some SQL code, which works as designed. I'm trying to convert the code to a procedure to make it flexible so that different values maybe passed in.
I am running into an error while trying to create the procedure.
Errors: PROCEDURE CREATE_XXX
Line/Col: 28/1 PL/SQL: SQL Statement ignored
Line/Col: 37/3 PL/SQL: ORA-00928: missing SELECT keyword
The problem seems to be occurring in a CTE, which contains a SELECT so I'm a bit confused and can use some assistance.
Below is a test CASE that contains the working SQL along with the procedure I'm trying to create.
Thanks in advance for your help, patience and expertise and to all who answer.
ALTER SESSION SET NLS_DATE_FORMAT = 'MMDDYYYY HH24:MI:SS';
create table schedule(
schedule_id NUMBER(4),
location_id number(4),
base_date DATE,
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));
-- works fine
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
;
-- having problem
CREATE OR REPLACE PROCEDURE CREATE_XXX
(
i_schedule_id IN PLS_INTEGER,
i_base_date IN DATE,
i_offset IN PLS_INTEGER DEFAULT 0,
i_incr IN PLS_INTEGER DEFAULT 10,
i_duration IN PLS_INTEGER DEFAULT 5
)
AS
l_offset interval day to second;
l_incr interval day to second;
l_duration interval day to second;
BEGIN
l_offset :=
NUMTODSINTERVAL(i_offset, 'SECOND') ;
l_incr :=
NUMTODSINTERVAL(i_incr, 'MINUTE') ;
l_duration :=
NUMTODSINTERVAL(i_duration, 'MINUTE') ;
WITH params AS(
SELECT
i_schedule_id
,i_base_date
,l_offset
,l_incr
,l_duration
FROM DUAL
)
INSERT INTO schedule(
schedule_id
,location_id
,base_date
,start_date
,end_date
)
VALUES
(p.schedule_id
,l.location_id
,p.base_date
,start_date
,end_date
);
SELECT p.schedule_id
, l.location_id
, p.base_date
, p.base_date + p.offset
+ (p.incr * (ROWNUM - 1)) AS start_date
, p.base_date + p.offset
+ (p.incr * (ROWNUM - 1))
+ p.duration AS end_date
FROM locations l
CROSS JOIN params p
ORDER BY start_date;
END;
/

The issue is this statement.
WITH params AS(
SELECT
i_schedule_id
,i_base_date
,l_offset
,l_incr
,l_duration
FROM DUAL
)
INSERT INTO schedule(
schedule_id
,location_id
,base_date
,start_date
,end_date
)
VALUES
(p.schedule_id
,l.location_id
,p.base_date
,start_date
,end_date
);
I'm not sure what you're trying to accomplish. Syntactically, if you want to have a CTE as part of an insert, you'd want to do an insert ... select
INSERT INTO schedule(
schedule_id
,location_id
,base_date
,start_date
,end_date
)
WITH params AS(
SELECT
i_schedule_id
,i_base_date
,l_offset
,l_incr
,l_duration
FROM DUAL
)
SELECT
p.schedule_id
,l.location_id
,p.base_date
,start_date
,end_date
FROM params p;
From there, though, you've still got several syntax issues that it's not obvious how you'd want to solve all of them.
p.schedule_id isn't valid because there is no schedule_id column in the params CTE. My guess is that you want to alias i_schedule_id to schedule_id in the CTE.
l.location_id doesn't make sense because you're not selecting from a table that could plausibly be given an alias of l.
There is no base_date, start_date, or end_date column in your params CTE. And it's not obvious how you'd plausibly fix that.
Maybe you actually intended the subsequent select statement to actually be part of this insert statement despite the fact that you terminated the insert with a semicolon? If so, maybe you want
INSERT INTO schedule(
schedule_id
,location_id
,base_date
,start_date
,end_date
)
WITH params AS(
SELECT
i_schedule_id schedule_id
,i_base_date base_date
,l_offset offset
,l_incr incr
,l_duration duration
FROM DUAL
)
SELECT p.schedule_id
, l.location_id
, p.base_date
, p.base_date + p.offset
+ (p.incr * (ROWNUM - 1)) AS start_date
, p.base_date + p.offset
+ (p.incr * (ROWNUM - 1))
+ p.duration AS end_date
FROM locations l
CROSS JOIN params p
ORDER BY start_date;
That would produce code that compiles at least. There doesn't, however, appear to be any reason to bother with a CTE here at all. Just reference the local variables and input parameters directly. I've also eliminated the order by since it doesn't make sense in the context of an insert statement. I assume there is a reason that you need to take input parameters as integers and convert them to local variables with the same name and a different data type rather than just passing in interval parameters to the procedure. If you can do that, you can further simplify things by eliminating the local variables.
CREATE OR REPLACE PROCEDURE CREATE_XXX
(
i_schedule_id IN PLS_INTEGER,
i_base_date IN DATE,
i_offset IN PLS_INTEGER DEFAULT 0,
i_incr IN PLS_INTEGER DEFAULT 10,
i_duration IN PLS_INTEGER DEFAULT 5
)
AS
l_offset interval day to second;
l_incr interval day to second;
l_duration interval day to second;
BEGIN
l_offset :=
NUMTODSINTERVAL(i_offset, 'SECOND') ;
l_incr :=
NUMTODSINTERVAL(i_incr, 'MINUTE') ;
l_duration :=
NUMTODSINTERVAL(i_duration, 'MINUTE') ;
INSERT INTO schedule(
schedule_id
,location_id
,base_date
,start_date
,end_date
)
SELECT i_schedule_id
, l.location_id
, i_base_date
, i_base_date + l_offset
+ (l_incr * (ROWNUM - 1)) AS start_date
, i_base_date + l_offset
+ (l_incr * (ROWNUM - 1))
+ l_duration AS end_date
FROM locations l;
END;
/

Related

Oracle INSERT, SELECT and NOT EXISTS

I have a procedure, which is working fine. There are times when the same VALUES are passed into the procedure and generates a unique KEY violation.
I know this could be easily solved by using
the usual solutions of either
LOG ERRORS INTO
clause, or to use the
/*+ ignore_row_on_dupkey_index ... */
I saw this link and was trying to implement #OMG Ponies NOT EXISTS solution by seeing if the VALUES are already in the PRIMARY KEY but after hours of research and trying different methods I was unsuccessful
Avoid duplicates in INSERT INTO SELECT query in SQL Server
I'm very curious to see how this solution could be implemented in my situation.
Thanks in advance to all who respond and for your help, patience and expertise.
My working test CASE is below (not modified) is below. BTW im testing in live SQL in case anyone wants to use the same environment.
ALTER SESSION SET NLS_DATE_FORMAT = 'MMDDYYYY HH24:MI:SS';
create table schedule(
seq_num NUMBER GENERATED BY DEFAULT AS IDENTITY (START WITH 1) NOT NULL,
schedule_id NUMBER(4),
location_id number(4),
base_date DATE,
start_date DATE,
end_date DATE,
constraint schedule_pk primary key (schedule_id, location_id, base_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)
);
/
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 'G'
END AS location_type
FROM dual
CONNECT BY level <= 15;
ALTER TABLE locations
ADD ( CONSTRAINT locations_pk
PRIMARY KEY (location_id));
CREATE OR REPLACE PROCEDURE CREATE_SCHEDULE
(
i_schedule_id IN PLS_INTEGER,
i_base_date IN DATE,
i_offset IN PLS_INTEGER DEFAULT 0,
i_incr IN PLS_INTEGER DEFAULT 10,
i_duration IN PLS_INTEGER DEFAULT 5
)
AS
l_offset interval day to second;
l_incr interval day to second;
l_duration interval day to second;
BEGIN
l_offset :=
NUMTODSINTERVAL(i_offset, 'SECOND') ;
l_incr :=
NUMTODSINTERVAL(i_incr, 'MINUTE') ;
l_duration :=
NUMTODSINTERVAL(i_duration, 'MINUTE') ;
INSERT INTO schedule(
schedule_id
,location_id
,base_date
,start_date
,end_date
)
SELECT i_schedule_id
, l.location_id
, i_base_date
, i_base_date + l_offset
+ (l_incr * (ROWNUM - 1)) AS start_date
, i_base_date + l_offset
+ (l_incr * (ROWNUM - 1))
+ l_duration AS end_date
FROM locations l;
END;
/
EXEC CREATE_SCHEDULE(1,TRUNC(SYSDATE))
Use a MERGE statement:
CREATE OR REPLACE PROCEDURE CREATE_SCHEDULE(
i_schedule_id IN PLS_INTEGER,
i_base_date IN DATE,
i_offset IN PLS_INTEGER DEFAULT 0,
i_incr IN PLS_INTEGER DEFAULT 10,
i_duration IN PLS_INTEGER DEFAULT 5
)
AS
l_offset interval day to second;
l_incr interval day to second;
l_duration interval day to second;
BEGIN
l_offset := NUMTODSINTERVAL(i_offset, 'SECOND') ;
l_incr := NUMTODSINTERVAL(i_incr, 'MINUTE') ;
l_duration := NUMTODSINTERVAL(i_duration, 'MINUTE') ;
MERGE INTO schedule dst
USING (
SELECT i_schedule_id AS schedule_id,
l.location_id,
i_base_date AS base_date,
i_base_date + l_offset + (l_incr * (ROWNUM - 1))
AS start_date,
i_base_date + l_offset + (l_incr * (ROWNUM - 1)) + l_duration
AS end_date
FROM locations l
) src
ON ( src.schedule_id = dst.schedule_id
AND src.location_id = dst.location_id
AND src.base_date = dst.base_date
)
WHEN NOT MATCHED THEN
INSERT (
schedule_id,
location_id,
base_date,
start_date,
end_date
) VALUES (
src.schedule_id,
src.location_id,
src.base_date,
src.start_date,
src.end_date
);
END;
/

Oracle calling a function within DML

I have a function that takes in a string 'HH:MM:SS' and converts it to the number of seconds. See below example
CREATE OR REPLACE FUNCTION CONVERT_TO_SECONDS(
i_date_string IN VARCHAR2
)
RETURN INTEGER
AS
l_hours NUMBER;
l_minutes NUMBER;
l_seconds NUMBER;
BEGIN
SELECT trim('"'
FROM regexp_substr(i_date_string,'".*?"|[^:]+',1,1)) hours,
trim('"'
FROM regexp_substr(i_date_string,'".*?"|[^:]+',1,2)) minutes,
trim('"'
FROM regexp_substr(i_date_string,'".*?"|[^:]+',1,3)) seconds
INTO l_hours ,
l_minutes ,
l_seconds
FROM dual ;
return
l_hours*3600 +
l_minutes*60 +
l_seconds;
END;
/
SELECT CONVERT_TO_SECONDS('08:08:08') FROM DUAL;
CONVERT_TO_SECONDS('08:08:08')
29288
I have a procedure that works fine, which creates emp_attendance rows.
After the rows are created I am trying to update the end_date of each row with the number of seconds returned by the function. Is this possible? If so, how can I get past the syntax error on the update.
Thanks in advance to all that answer and for your help, patience and expertise.
My test CASE is below.
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 employees(
employee_id NUMBER(6),
first_name VARCHAR2(20),
last_name VARCHAR2(20),
card_num VARCHAR2(10),
work_days VARCHAR2(7)
);
INSERT INTO employees (
employee_id,
first_name,
last_name,
card_num,
work_days
)
WITH names AS (
SELECT 1, 'John', 'Doe', 'D564311','YYYYYNN' FROM dual UNION ALL
SELECT 2, 'Justin', 'Case', 'C224311','YYYYYNN' FROM dual UNION ALL
SELECT 3, 'Mike', 'Jones', 'J288811','YYYYYNN' FROM dual UNION ALL
SELECT 4, 'Jane', 'Smith', 'S564661','YYYYYNN' FROM dual
) SELECT * FROM names;
CREATE TABLE emp_attendance(
seq_num NUMBER GENERATED BY DEFAULT AS IDENTITY (START WITH 1) NOT NULL,
employee_id NUMBER(6),
start_date DATE,
end_date DATE,
week_number NUMBER(2));
CREATE OR REPLACE PROCEDURE create_emp_attendance (
p_start_date IN DATE,
p_end_date IN DATE
)
IS
BEGIN
INSERT INTO emp_attendance ( employee_id, start_date, end_date, week_number)
SELECT
employee_id
, start_date
, start_date+NUMTODSINTERVAL(FLOOR(DBMS_RANDOM.VALUE(3600,43200)), 'SECOND') AS end_date
, to_char(start_date,'WW') AS week_number
FROM ( -- Need subquery to generate end_date based on start_date.
SELECT e.employee_id, d.COLUMN_VALUE + NUMTODSINTERVAL(FLOOR(DBMS_RANDOM.VALUE(0,86399)), 'SECOND') AS start_date
FROM employees e
CROSS JOIN TABLE( generate_dates_pipelined(p_start_date, p_end_date) ) d
) ed
;
END;
/
EXEC create_emp_attendance(SYSDATE, SYSDATE);
-- Having problem with this update
update emp_attendance
set end_date=start_date + NUMTODSINTERVAL
CONVERT_TO_SECONDS('08:08:08'),'SECOND';
-- Once the update is working the query below should show 8hrs 8mins 8sec for each row.
select e.employee_id,
e.first_name,
e.last_name,
trunc(sum(a.end_date - a.start_date) * 24) hours,
trunc(mod(sum(a.end_date - a.start_date) * 24 * 60,60)) minutes,
round(mod(sum(a.end_date - a.start_date) * 24 * 60 * 60,60)) seconds
from employees e,
emp_attendance a
where a.employee_id = e.employee_id
AND start_date BETWEEN TRUNC(SYSDATE)
AND
TRUNC(SYSDATE)+ (1-1/24/60/60)
group by e.employee_id, e.first_name, e.last_name
order by e.employee_id, e.first_name,
e.last_name;
You can simplify your function to:
CREATE OR REPLACE FUNCTION CONVERT_TO_SECONDS(
i_date_string IN VARCHAR2
)
RETURN INTEGER DETERMINISTIC
AS
BEGIN
RETURN ( TO_DATE(i_date_string, 'HH24:MI:SS')
- TO_DATE('00:00:00', 'HH24:MI:SS')
) * 86400;
END;
/
Then you need brackets around the NUMTODSINTERVAL function arguments:
UPDATE emp_attendance
SET end_date = start_date + NUMTODSINTERVAL( CONVERT_TO_SECONDS('08:08:08'),'SECOND' );
db<>fiddle here
NUMTODSINTERVAL is a function. So it needs to be used like this:
update emp_attendance
set end_date=start_date + NUMTODSINTERVAL(CONVERT_TO_SECONDS('08:08:08'), 'SECOND')
;
There's no need to create custom function, because Oracle already has it: to_dsinterval. But you need to provide also a number of days, so add a zero before your time:
select
sysdate,
sysdate + to_dsinterval('0 ' || '08:08:08') as sysdate_shifted
from dual
SYSDATE | SYSDATE_SHIFTED
:------------------ | :------------------
2021-08-15 18:31:49 | 2021-08-16 02:39:57
db<>fiddle here
Then your update will be:
update emp_attendance
set end_date = start_date + to_dsinterval('0 ' || '08:08:08')

How to unite two columns of date + time for datediff - Oracle sql

I have two int columns:
thedate - for example 20210512
thetime - for example 142342
So, i need to unite them to one column to check if the time difference is lower the 5.
I tried this:
TO_DATE(sysdate) - TO_DATE(thedate, 'YYYY-MM-DD') < 5
But it only for the date not for the time so i will be glad to know how to unite the two int columns + and convert it to date type for time difference.
You can use:
SELECT *
FROM table_name
WHERE TO_DATE( thedate * 1000000 + thetime, 'YYYYMMDDHH24MISS' ) > SYSDATE - 5;
Which, for the sample data:
CREATE TABLE table_name ( thedate INT, thetime INT );
INSERT INTO TABLE_NAME ( thedate, thetime ) VALUES (
TO_NUMBER( TO_CHAR( SYSDATE, 'YYYYMMDD' ) ),
TO_NUMBER( TO_CHAR( SYSDATE, 'HH24MISS' ) )
);
INSERT INTO TABLE_NAME ( thedate, thetime ) VALUES (
TO_NUMBER( TO_CHAR( SYSDATE - INTERVAL '4 23' DAY TO HOUR, 'YYYYMMDD' ) ),
TO_NUMBER( TO_CHAR( SYSDATE - INTERVAL '4 23' DAY TO HOUR, 'HH24MISS' ) )
);
INSERT INTO TABLE_NAME ( thedate, thetime ) VALUES (
TO_NUMBER( TO_CHAR( SYSDATE - INTERVAL '5 1' DAY TO HOUR, 'YYYYMMDD' ) ),
TO_NUMBER( TO_CHAR( SYSDATE - INTERVAL '5 1' DAY TO HOUR, 'HH24MISS' ) )
);
Outputs:
THEDATE
THETIME
20210512
131832
20210507
141832
Or, if you want to use indexes on the thedate and thetime columns (the query above would not use indexes on thedate and thetime columns but would require a function-based index) then:
SELECT *
FROM table_name
WHERE thedate > TO_NUMBER( TO_CHAR( SYSDATE - 5, 'YYYYMMDD' ) )
OR ( thedate = TO_NUMBER( TO_CHAR( SYSDATE - 5, 'YYYYMMDD' ) )
AND thetime >= TO_NUMBER( TO_CHAR( SYSDATE - 5, 'HH24MISS' ) )
)
However, the better solution is to use appropriate data-types for your data; in this case, you should store date values in a DATE data-type (which, in Oracle, contains year-second components) rather than as two INT values for date and time.
db<>fiddle here
You want to convert to a timestamp, not a date. So:
select to_timestamp(cast(20210512 * 1000000 + 142342 as varchar2(255)), 'YYYYMMDDHH24MISS')
from dual;
Here is a db<>fiddle showing that this works.
Or in a where clause:
to_timestamp(cast(thedate * 1000000 + thetime as varchar2(255)), 'YYYYMMDDHH24MISS') > sysdate - interval '5' day
(or whatever you mean by "5").
Note: You may want to add a computed column to the table that has the full timestamp:
alter table t add column timestamp generated always as
( to_timestamp(cast(thedate * 10000 + thetime as varchar2(255)), 'YYYYMMDDHH24MISS') );

How to sum column which stores times in hh24:mi:ss in Oracle Sql?

I have a PROCESS_TIME with data type VARCHAR2(32),
in which I store the times as hh24:mi:ss,
and I intend to add these times on some grouping logic.
A minified version of the table
CREATE TABLE "SCHEMA"."MY_TABLE"
( "MY_TABLE_ID" VARCHAR2(32 BYTE) NOT NULL ENABLE,
"START_TIME" DATE NOT NULL ENABLE,
"END_TIME" DATE NOT NULL ENABLE,
"LOAD_PROCESS_TIME" VARCHAR2(32 BYTE) DEFAULT '00:00:00',
)
and a minified version of Insert
Insert into MY_TABLE (MY_TABLE_ID,START_TIME,END_TIME,LOAD_PROCESS_TIME)
values ('8880508C9AC4441DB8E16E023F206C2F',to_date('05/11/2018 07:22','MM/DD/YYYY HH:MI'),to_date('05/11/2018 08:22','MM/DD/YYYY HH:MI'),'01:00:14');
Insert into MY_TABLE (MY_TABLE_ID,START_TIME,END_TIME,LOAD_PROCESS_TIME) values ('C858EB646A794D04B5C77779C50EBFCF',
to_date('05/12/2018 10:20','MM/DD/YYYY HH:MI'),
to_date('05/12/2018 11:20','MM/DD/YYYY HH:MI'),
'02:30:10');
Intended Query
SELECT TO_CHAR(TRUNC(END_TIME, 'DD'), 'DD-MON-YY'),
sum(LOAD_PROCESS_TIME)
FROM MY_TABLE
WHERE END_TIME > SYSDATE -14
GROUP BY TRUNC(END_TIME,'DD')
ORDER BY TRUNC(END_TIME,'DD');
Can you please help me achieve this using Oracle SQL?
You can calculate the time interval as a fractional number of days and sum that:
SQL Fiddle
Oracle 11g R2 Schema Setup:
CREATE TABLE table_name ( PROCESS_TIME ) AS
SELECT '01:23:04' FROM DUAL UNION ALL
SELECT '23:00:00' FROM DUAL UNION ALL
SELECT '11:36:56' FROM DUAL;
Query 1:
SELECT SUM(
TO_DATE( PROCESS_TIME, 'HH24:MI:SS' )
- TO_DATE( '00:00:00', 'HH24:MI:SS' )
) AS num_days
FROM table_name
Results:
| NUM_DAYS |
|----------|
| 1.5 |
You could also create a custom object to aggregate INTERVAL DAY TO SECOND data types:
CREATE TYPE IntervalSumType AS OBJECT(
total INTERVAL DAY(9) TO SECOND(9),
STATIC FUNCTION ODCIAggregateInitialize(
ctx IN OUT IntervalSumType
) RETURN NUMBER,
MEMBER FUNCTION ODCIAggregateIterate(
self IN OUT IntervalSumType,
value IN INTERVAL DAY TO SECOND
) RETURN NUMBER,
MEMBER FUNCTION ODCIAggregateTerminate(
self IN OUT IntervalSumType,
returnValue OUT INTERVAL DAY TO SECOND,
flags IN NUMBER
) RETURN NUMBER,
MEMBER FUNCTION ODCIAggregateMerge(
self IN OUT IntervalSumType,
ctx IN OUT IntervalSumType
) RETURN NUMBER
);
/
CREATE OR REPLACE TYPE BODY IntervalSumType
IS
STATIC FUNCTION ODCIAggregateInitialize(
ctx IN OUT IntervalSumType
) RETURN NUMBER
IS
BEGIN
ctx := IntervalSumType( INTERVAL '0' DAY );
RETURN ODCIConst.SUCCESS;
END;
MEMBER FUNCTION ODCIAggregateIterate(
self IN OUT IntervalSumType,
value IN INTERVAL DAY TO SECOND
) RETURN NUMBER
IS
BEGIN
IF value IS NOT NULL THEN
self.total := self.total + value;
END IF;
RETURN ODCIConst.SUCCESS;
END;
MEMBER FUNCTION ODCIAggregateTerminate(
self IN OUT IntervalSumType,
returnValue OUT INTERVAL DAY TO SECOND,
flags IN NUMBER
) RETURN NUMBER
IS
BEGIN
returnValue := self.total;
RETURN ODCIConst.SUCCESS;
END;
MEMBER FUNCTION ODCIAggregateMerge(
self IN OUT IntervalSumType,
ctx IN OUT IntervalSumType
) RETURN NUMBER
IS
BEGIN
self.total := self.total + ctx.total;
RETURN ODCIConst.SUCCESS;
END;
END;
/
And then a custom aggregation function:
CREATE FUNCTION SUM_INTERVALS( value INTERVAL DAY TO SECOND )
RETURN INTERVAL DAY TO SECOND
PARALLEL_ENABLE AGGREGATE USING IntervalSumType;
/
Query 2:
SELECT SUM_INTERVALS( TO_DSINTERVAL( '+0 '||PROCESS_TIME ) )
AS total_difference
FROM table_name
Results:
| TOTAL_DIFFERENCE |
|------------------|
| 1 12:0:0.0 |
Update - Query 3:
SELECT TO_CHAR( num_days * 24, 'FM99990' )
|| ':' || TO_CHAR( MOD( num_days * 24*60, 60 ), 'FM00' )
|| ':' || TO_CHAR( MOD( num_days * 24*60*60, 60 ), 'FM00' )
AS total_time
FROM (
SELECT SUM(
TO_DATE( PROCESS_TIME, 'HH24:MI:SS' )
- TO_DATE( '00:00:00', 'HH24:MI:SS' )
) AS num_days
FROM table_name
)
Results:
| TOTAL_TIME |
|------------|
| 36:00:00 |
------------------------------answer----------------------------------------
As a completion to the discussion , the query which worked was
SELECT TO_CHAR(TRUNC(END_TIME, 'DD'), 'DD-MON-YY'),
to_char(sum(TO_DATE(LOAD_PROCESS_TIME,'HH24:MI:SS')- TO_DATE( '00:00:00', 'HH24:MI:SS' ))*24,'FM00')
|| ':'|| TO_CHAR(MOD(sum(TO_DATE(LOAD_PROCESS_TIME,'HH24:MI:SS')- TO_DATE( '00:00:00', 'HH24:MI:SS' ))*24*60,60),'FM00')
||':'|| TO_CHAR(MOD(sum(TO_DATE(LOAD_PROCESS_TIME,'HH24:MI:SS')- TO_DATE( '00:00:00', 'HH24:MI:SS' ))*24*60*60,60),'FM00')
as Days
FROM MY_TABLE
WHERE END_TIME > SYSDATE -30
GROUP BY TRUNC(END_TIME,'DD')
ORDER BY TRUNC(END_TIME,'DD');

Using the Oracle "at time zone" function in sql - problem and solution

I have a table with a date column that I know is stored in GMT. I have a procedure that accepts a date input and an account ID. The procedure:
1) gets the account ID timezone (stored in table account)
2) determines the start and end range in GMT as follows:
v_start_time := cast( from_tz( cast( i_date as timestamp ), v_tz ) at time zone c_gmt as date ); -- where i_date is input, v_tz is 'US/Eastern' or any other tzname from v$timezone_names, and c_gmt is the string 'GMT'
v_end_time := v_start_time + 1; -- Add exactly one day to start date
3) return sys_refcursor to caller as:
open o_cur for
select gmt_col, some_value
from my_table
where account_id = i_account_id
and gmt_col between v_start_time and v_end_time;
However, the developer would like both the gmt_date and the local time in the cursor. First, I attempted to use the exact same conversion method as I had to determine v_start_time, that is:
open o_cur for
select gmt_col,
cast( from_tz( cast( gmt_col as timestamp ), c_gmt ) at time zone v_tz as date ) as local_time, some_value
from my_table
where account_id = i_account_id
and gmt_col between v_start_time and v_end_time;
However, when compiled, this results in ORA-00905: missing keyword. I attempted to add the single quotes around the "v_tz" like: chr( 39 ) || v_tz || chr( 39 ), but that doesn't work - the proc compiles, but when I open the cursor, I get ORA-01882: timezone region not found. After a bit of experimentation, here are two solutions that allow "at time zone" to work smoothly in sql:
SOLUTION 1:
open o_cur for
select gmt_col,
cast( from_tz( cast( gmt_col as timestamp ), c_gmt ) at time zone ( select v_tz from dual ) as date ) as local_time, some_value
from my_table
where account_id = i_account_id
and gmt_col between v_start_time and v_end_time;
SOLUTION 2:
in package spec:
function echo( i_sound in varchar2 ) return varchar2;
pragma restrict_references( echo, wnps, rnps, wnds, rnds );
in package body:
function echo( i_sound in varchar2 ) return varchar2 is begin return i_sound; end;
in procedure:
open o_cur for
select gmt_col,
cast( from_tz( cast( gmt_col as timestamp ), c_gmt ) at time zone echo( v_tz ) as date ) as local_time, some_value
from my_table
where account_id = i_account_id
and gmt_col between v_start_time and v_end_time;
Performance appears to be comparable for each. The second solution hints at something I've started to do recently, which is to use functions to return "constants" with pragma restrict_references, so I can use the constant values flexibly between pl/sql and sql. For example:
function c_gmt return varchar2;
pragma restrict_references( c_gmt, wnds, rnds, wnps, rnps );
select * from v$timezone_names where tzabbrev = c_gmt;
select c_gmt from dual;
v_start_time := blah blah blah || c_gmt;
etc...
You shouldn't need the extra select from dual. Just putting the variable in parenthesis should do the trick (don't ask me why though):
open o_cur for
select gmt_col,
cast( from_tz( cast( gmt_col as timestamp ), c_gmt ) at time zone (v_tz) as date ) as local_time, some_value
from my_table
where account_id = i_account_id
and gmt_col between v_start_time and v_end_time;