Generuc function to generate date intervals - oracle

I gave the following code to several QA teams, which works fine.
with dt (dt, interv) as (
select TIMESTAMP '2022-11-01 02:37:11', numtodsinterval(5,'MINUTE') from dual
union all
select dt.dt + interv, interv from dt
where dt.dt + interv <
TIMESTAMP '2022-11-01 05:00:00')
select dt from dt;
The problem is VALUES are hardcoded and way too often when a person changes to put different values they make a mistake editing, which causes the code to fail.
Can this code be modified to a pipeline function or a procedure with an out parameter or a macro to prevent such problems.
I would need to pass in 2 dates(order of dates passed should not screw things up. Perhaps use least(), greatest()) an INTERVAL 'N' and a unit S=second M=MINUTE H=hour or D=Day.
Thanks to all who answer and your expertise.

As a macro:
CREATE OR REPLACE FUNCTION gen_dt(i_from_dat IN TIMESTAMP, i_to_dat IN TIMESTAMP, i_interval IN NUMBER, i_interval_type IN VARCHAR2)
RETURN VARCHAR2
SQL_MACRO
IS
BEGIN
RETURN q'~SELECT LEAST(i_from_dat,i_to_dat) + NUMTODSINTERVAL( (LEVEL-1)*i_interval, i_interval_type ) AS dt
FROM DUAL
CONNECT BY LEAST(i_from_dat,i_to_dat) + NUMTODSINTERVAL( (LEVEL-1)*i_interval, i_interval_type) < GREATEST(i_from_dat, i_to_dat)~';
END ;
(Note that you can't use WITH clause because there is a bug preventing substitution of variables inside the WITH() part)
SELECT * FROM gen_dt(SYSTIMESTAMP, SYSTIMESTAMP+1, 4, 'HOUR') ;
03/11/22 13:48:23,072872000
03/11/22 17:48:23,072872000
03/11/22 21:48:23,072872000
04/11/22 01:48:23,072872000
04/11/22 05:48:23,072872000
04/11/22 09:48:23,072872000

Related

Oracle. CAST COLLECT for date datatype

Consider the types:
CREATE OR REPLACE TYPE date_array AS TABLE OF DATE;
CREATE OR REPLACE TYPE number_array AS TABLE OF NUMBER;
CREATE OR REPLACE TYPE char_array AS TABLE OF VARCHAR2(80);
Queries:
WITH q AS
(SELECT LEVEL ID,
TRUNC(SYSDATE) + LEVEL MyDate,
to_char(LEVEL) STRING
FROM dual
CONNECT BY LEVEL < 5)
SELECT CAST(COLLECT(ID) AS number_array)
FROM q;
return collection of numbers
WITH q AS
(SELECT LEVEL ID,
TRUNC(SYSDATE) + LEVEL MyDate,
to_char(LEVEL) STRING
FROM dual
CONNECT BY LEVEL < 5)
SELECT CAST(COLLECT(STRING) AS char_array)
FROM q;
return collection of strings
WITH q AS
(SELECT LEVEL ID,
TRUNC(SYSDATE) + LEVEL MyDate,
to_char(LEVEL) STRING
FROM dual
CONNECT BY LEVEL < 5)
SELECT CAST(COLLECT(MyDate) AS date_array)
FROM q
return error invalid datatype.
Can anyone explain why the Date datatype behaves differently?
Here are my findings... it seems you are facing a bug caused by the fact that calculated dates seem to have a different internal representation from "database" dates. I did find a workaround, so keep on reading.
On my oracle dev installation (Oracle 11g Enterprise Edition Release 11.2.0.4.0 - 64bit Production) I am experiencing your same problem.
BUT... If I create a physical table containing your test data:
create table test_data as
SELECT LEVEL ID,
TRUNC(SYSDATE) + LEVEL MyDate,
to_char(LEVEL) STRING
FROM dual
CONNECT BY LEVEL < 5
and then run the "cast collect" operator on this physical table, it works as expected:
-- this one works perfectly
SELECT CAST(COLLECT(MyDate) AS date_array) from test_data
but all these examples still do NOT work:
-- here I just added 1 .. and it doesn't work
SELECT CAST(COLLECT(MyDate + 1) AS date_array)
from test_data
-- here I am extracting sysdate, instead of a physical column... and it doesn't work
SELECT CAST(COLLECT(sysdate) AS date_array)
from test_data
It feels like oracle doesn't think that calculated dates are the same thing of physical dates
So, I tried to "persuade" oracle that the data I am providing is actually a normal DATE value, using an explicit cast... and EUREKA! this does the job correctly:
WITH q AS
(SELECT LEVEL ID,
-- this apparently unnecessary cast does the trick
CAST( TRUNC(SYSDATE) + LEVEL AS DATE) MyDate,
to_char(LEVEL) STRING
FROM dual
CONNECT BY LEVEL < 5)
SELECT CAST(COLLECT(MyDate) AS date_array)
FROM q
Yes... but why??
It really seems that these two values are not exactly the same thing, even if the values we see are actually the same:
select sysdate, cast (sysdate as date) from dual
So I dug in the internal representation of the two values applying the "dump" function to both of them:
select dump(sysdate), dump(cast (sysdate as date)) from dual
and these are the results I got:
DUMP(SYSDATE ) -> Typ=13 Len=8: 226,7,11,9,19,20,47,0
DUMP(CAST(SYSDATEASDATE) as DUAL) -> Typ=12 Len=7: 120,118,11,9,20,21,48
Internally they look like two totally different data types! one is type 12 and the other is type 13... and they have different length and representation.
Anyway I discovered something more.. it seems someone else has noticed this: https://community.oracle.com/thread/4122627
The question has an answer pointing to this document: http://psoug.org/reference/datatypes.html
which contains a lengthy note about dates... an excerpt of it reads:
"What happened? Is the information above incorrect or does the DUMP()
function not handle DATE values? No, you have to look at the "Typ="
values to understand why we are seeing these results. ". The datatype
returned is 13 and not 12, the external DATE datatype. This occurs
because we rely on the TO_DATE function! External datatype 13 is an
internal c-structure whose length varies depending on how the
c-compiler represents the structure. Note that the "Len=" value is 8
and not 7. Type 13 is not a part of the published 3GL interfaces for
Oracle and is used for date calculations mainly within PL/SQL
operations. Note that the same result can be seen when DUMPing the
value SYSDATE."
Anyway, I repeat: I think this is a bug, but at least I found a workaround: use an explicit cast to DATE.

How to get monthly periods between a date range in Oracle PL/SQL

I have say, start and end date as "6/11/1996" and "3/1/2002" in "mm/dd/yyyy" format respectively.
I need to get all the monthly periods as given below.
Start Date End Date
6/11/1996 - 6/30/1996
7/01/1996 - 7/31/1996
8/01/1996 - 8/31/1996
.
.
.
Till
2/01/2002 - 2/28/2002
Any help would be highly appreciated.
Assuming the interval is given by way of two bind variables, :from_dt and :to_dt (strings in the indicated format):
with
inputs ( f_dt, t_dt ) as (
select to_date(:from_dt, 'mm/dd/yyyy'), to_date(:to_dt, 'mm/dd/yyyy') from dual
),
ld ( l_day, lvl ) as (
select add_months(last_day(f_dt), level - 1), level
from inputs
connect by level <= months_between(last_day(t_dt), last_day(f_dt)) + 1
)
select case when ld.lvl = 1 then i.f_dt else add_months(ld.l_day, -1) + 1 end
as start_date,
least(i.t_dt, ld.l_day) as end_date
from inputs i cross join ld
;
This assumes that in the original post you did, in fact, mean to have one more interval, from 3/1/2002 to 3/1/2002; and the query deals correctly with the case when the from-date and the to-date are in the same month: if the inputs are 6/11/1996 to 6/21/1996, then the output is exactly that interval.
Added: creating column aliases in the declaration of factored subqueries (in the WITH clause), as I have done, requires Oracle 11.2 or above. For earlier versions, it is necessary to write it a little differently, like so:
with
inputs as (
select to_date(:from_dt, 'mm/dd/yyyy') as f_dt,
to_date(:to_dt , 'mm/dd/yyyy') as t_dt
from dual
),
ld as (
select add_months(last_day(f_dt), level - 1) as l_day, level as lvl
from inputs ...............
You could use connect by query with functions add_months and months_between:
with p as ( select date '1996-06-11' d1, date '2002-03-01' d2 from dual )
select greatest(trunc(add_months(d1, level - 1), 'month'), d1) as d1,
trunc(add_months(d1, level), 'month') - 1 as d2
from p connect by level <= months_between(trunc(d2, 'month'), trunc(d1, 'month'))
The output is exactly as requested.
DECLARE
L_min_date DATE := '11-Jun-1996';
L_max_date DATE := '28-Feb-2002';
L_number_of_months NUMBER := 0;
L_new_date DATE := NULL;
BEGIN
L_number_of_months := MONTHS_BETWEEN(L_max_date, L_min_date);
DBMS_OUTPUT.PUT_LINE('Start_Date End_Date');
FOR i IN 1..L_number_of_months
LOOP
SELECT (ADD_MONTHS(L_min_date, 1) - 1) INTO L_new_date FROM dual;
DBMS_OUTPUT.PUT_LINE(L_min_date || ' - ' || L_new_date);
L_min_date := L_new_date + 1;
END LOOP;
END;
/
Something like above can help.

encountered an invalid number error calling the below oracle stored procedure

SELECT Ticket, ETRs, "Last ETR","Last ETR Time Change","STAR Restore Time","Restore Time - Last ETR"
FROM(
SELECT e.xsystemjob Ticket,
a.eventkey Event,
(select count(generatedtime) from obvwh.ops_ertchangelog_fact where eventkey = a.eventkey) ETRs,
to_char(a.ERT, 'MM/DD/YYYY HH24:MI:SS') "Last ETR", --GENERATEDTIME,
to_char(generatedtime, 'MM/DD/YYYY HH24:MI:SS') as "Last ETR Time Change",
to_char(e.restdate,'MM/DD/YYYY HH24:MI:SS') as "STAR Restore Time",
round(((e.restdate - a.generatedtime) * 1440),0) as "Restore Time - Last ETR"
FROM obvwh.ops_ertchangelog_fact a
join obvwh.ops_event_dim e
on a.eventkey = e.eventkey
where a.generatedtime = (select max(generatedtime) from obvwh.ops_ertchangelog_fact where eventkey = a.eventkey)
)
WHERE Substr(Ticket,0,1) = region
AND to_char("Last ETR", 'MM/DD/YYYY') between to_char(start_date,'MM/DD/YYYY') and to_char(end_date,'MM/DD/YYYY');
From you inner query, Last ETR is a string, representing the column value in format MM/DD/YYYY HH24:MI:SS. You're trying to convert that string to a string, passing a single format mask, and that is throwing the error.
You can see the same thing with a simpler demo:
select to_char('07/18/2016 12:13:14', 'MM/DD/YYYY') from dual;
Error report -
SQL Error: ORA-01722: invalid number
You could explicitly convert the string to a date and back again:
select to_char(to_date('07/18/2016 12:13:14', 'MM/DD/YYYY HH24:MI:SS'), 'MM/DD/YYYY') from dual;
TO_CHAR(TO
----------
07/18/2016
... but in the context of your comparison that doesn't really make sense anyway if the range you're comparing with can span a year end - the format mask you're using doesn't allow for simple comparison. Assuming start_date and end_date are dates (with their times set to midnight) you could do:
AND to_date("Last ETR", 'MM/DD/YYYY HH24:MI:SS') between start_date and end_date;
or even simpler, use the original raw ERT value (which is presumably already a date), and convert it to a string - if that is actually the right thing to do - in the outermost select list.
I'm not quite sure why you have an inline view here at all though, or why you're using a subquery to get the count since you're already querying ops_ertchangelog_fact - maybe you want to be using analytic functions here.

Sum of INTERVAL DAY in Oracle [duplicate]

I am trying to sum INTERVAL. E.g.
SELECT SUM(TIMESTAMP1 - TIMESTAMP2) FROM DUAL
Is it possible to write a query that would work both on Oracle and SQL Server? If so, how?
Edit: changed DATE to INTERVAL
I'm afraid you're going to be out of luck with a solution which works in both Oracle and MSSQL. Date arithmetic is something which is very different on the various flavours of DBMS.
Anyway, in Oracle we can use dates in straightforward arithmetic. And we have a function NUMTODSINTERVAL which turns a number into a DAY TO SECOND INTERVAL. So let's put them together.
Simple test data, two rows with pairs of dates rough twelve hours apart:
SQL> alter session set nls_date_format = 'dd-mon-yyyy hh24:mi:ss'
2 /
Session altered.
SQL> select * from t42
2 /
D1 D2
-------------------- --------------------
27-jul-2010 12:10:26 27-jul-2010 00:00:00
28-jul-2010 12:10:39 28-jul-2010 00:00:00
SQL>
Simple SQL query to find the sum of elapsed time:
SQL> select numtodsinterval(sum(d1-d2), 'DAY')
2 from t42
3 /
NUMTODSINTERVAL(SUM(D1-D2),'DAY')
-----------------------------------------------------
+000000001 00:21:04.999999999
SQL>
Just over a day, which is what we would expect.
"Edit: changed DATE to INTERVAL"
Working with TIMESTAMP columns is a little more labourious, but we can still work the same trick.
In the following sample. T42T is the same as T42 only the columns have TIMESTAMP rather than DATE for their datatype. The query extracts the various components of the DS INTERVAL and converts them into seconds, which are then summed and converted back into an INTERVAL:
SQL> select numtodsinterval(
2 sum(
3 extract (day from (t1-t2)) * 86400
4 + extract (hour from (t1-t2)) * 3600
5 + extract (minute from (t1-t2)) * 600
6 + extract (second from (t1-t2))
7 ), 'SECOND')
8 from t42t
9 /
NUMTODSINTERVAL(SUM(EXTRACT(DAYFROM(T1-T2))*86400+EXTRACT(HOURFROM(T1-T2))*
---------------------------------------------------------------------------
+000000001 03:21:05.000000000
SQL>
At least this result is in round seconds!
Ok, after a bit of hell, with the help of the stackoverflowers' answers I've found the solution that fits my needs.
SELECT
SUM(CAST((DATE1 + 0) - (DATE2 + 0) AS FLOAT) AS SUM_TURNAROUND
FROM MY_BEAUTIFUL_TABLE
GROUP BY YOUR_CHOSEN_COLUMN
This returns a float (which is totally fine for me) that represents days both on Oracle ant SQL Server.
The reason I added zero to both DATEs is because in my case date columns on Oracle DB are of TIMESTAMP type and on SQL Server are of DATETIME type (which is obviously weird). So adding zero to TIMESTAMP on Oracle works just like casting to date and it does not have any effect on SQL Server DATETIME type.
Thank you guys! You were really helpful.
You can't sum two datetimes. It wouldn't make sense - i.e. what does 15:00:00 plus 23:59:00 equal? Some time the next day? etc
But you can add a time increment by using a function like Dateadd() in SQL Server.
In SQL Server as long as your individual timespans are all less than 24 hours you can do something like
WITH TIMES AS
(
SELECT CAST('01:01:00' AS DATETIME) AS TimeSpan
UNION ALL
SELECT '00:02:00'
UNION ALL
SELECT '23:02:00'
UNION ALL
SELECT '17:02:00'
--UNION ALL SELECT '24:02:00' /*This line would fail!*/
),
SummedTimes As
(
SELECT cast(SUM(CAST(TimeSpan AS FLOAT)) as datetime) AS [Summed] FROM TIMES
)
SELECT
FLOOR(CAST(Summed AS FLOAT)) AS D,
DATEPART(HOUR,[Summed]) AS H,
DATEPART(MINUTE,[Summed]) AS M,
DATEPART(SECOND,[Summed]) AS S
FROM SummedTimes
Gives
D H M S
----------- ----------- ----------- -----------
1 17 7 0
If you wanted to handle timespans greater than 24 hours I think you'd need to look at CLR integration and the TimeSpan structure. Definitely not portable!
Edit: SQL Server 2008 has a DateTimeOffset datatype that might help but that doesn't allow either SUMming or being cast to float
I also do not think this is possible. Go with custom solutions that calculates the date value according to your preferences.
You can also use this:
select
EXTRACT (DAY FROM call_end_Date - call_start_Date)*86400 +
EXTRACT (HOUR FROM call_end_Date - call_start_Date)*3600 +
EXTRACT (MINUTE FROM call_end_Date - call_start_Date)*60 +
extract (second FROM call_end_Date - call_start_Date) as interval
from table;
You Can write you own aggregate function :-). Please read carefully http://docs.oracle.com/cd/B19306_01/appdev.102/b14289/dciaggfns.htm
You must create object type and its body by template, and next aggregate function what using this object:
create or replace type Sum_Interval_Obj as object
(
-- Object for creating and support custom aggregate function
duration interval day to second, -- In this property You sum all interval
-- Object Init
static function ODCIAggregateInitialize(
actx IN OUT Sum_Interval_Obj
) return number,
-- Iterate getting values from dataset
member function ODCIAggregateIterate(
self IN OUT Sum_Interval_Obj,
ad_interval IN interval day to second
) return number,
-- Merge parallel summed data
member function ODCIAggregateMerge(
self IN OUT Sum_Interval_Obj,
ctx2 IN Sum_Interval_Obj
) return number,
-- End of query, returning summary result
member function ODCIAggregateTerminate
(
self IN Sum_Interval_Obj,
returnValue OUT interval day to second,
flags IN number
) return number
)
/
create or replace type body Sum_Interval_Obj is
-- Object Init
static function ODCIAggregateInitialize(
actx IN OUT Sum_Interval_Obj
) return number
is
begin
actx := Sum_Interval_Obj(numtodsinterval(0,'SECOND'));
return ODCIConst.Success;
end ODCIAggregateInitialize;
-- Iterate getting values from dataset
member function ODCIAggregateIterate(
self IN OUT Sum_Interval_Obj,
ad_interval IN interval day to second
) return number
is
begin
self.duration := self.duration + ad_interval;
return ODCIConst.Success;
exception
when others then
return ODCIConst.Error;
end ODCIAggregateIterate;
-- Merge parallel calculated intervals
member function ODCIAggregateMerge(
self IN OUT Sum_Interval_Obj,
ctx2 IN Sum_Interval_Obj
) return number
is
begin
self.duration := self.duration + ctx2.duration; -- Add two intervals
-- return = All Ok!
return ODCIConst.Success;
exception
when others then
return ODCIConst.Error;
end ODCIAggregateMerge;
-- End of query, returning summary result
member function ODCIAggregateTerminate(
self IN Sum_Interval_Obj,
returnValue OUT interval day to second,
flags IN number
) return number
is
begin
-- return = All Ok, too!
returnValue := self.duration;
return ODCIConst.Success;
end ODCIAggregateTerminate;
end;
/
-- You own new aggregate function:
CREATE OR REPLACE FUNCTION Sum_Interval(
a_Interval interval day to second
) RETURN interval day to second
PARALLEL_ENABLE AGGREGATE USING Sum_Interval_Obj;
/
Last, check your function:
select sum_interval(duration)
from (select numtodsinterval(1,'SECOND') as duration from dual union all
select numtodsinterval(1,'MINUTE') as duration from dual union all
select numtodsinterval(1,'HOUR') as duration from dual union all
select numtodsinterval(1,'DAY') as duration from dual);
Finally You can create SUM function, if you want.

Oracle Recursive Query - Dates

I've got two sets of dates being passed into a query and I would like to find all the months/years between both sets of dates.
When I try this:
WITH CTE_Dates (cte_date) AS (
SELECT cast(date '2014-01-27' as date) from dual
UNION ALL
SELECT cast(ADD_MONTHS(TRUNC(cte_date, 'MONTH'),1) as date)
FROM CTE_Dates
WHERE ( TO_DATE(ADD_MONTHS(TRUNC(cte_date, 'MONTH'), 1)) BETWEEN TO_DATE ('27-01-2014','DD-MM-YYYY') AND TO_DATE ('27-04-2014','DD-MM-YYYY'))
OR
( TO_DATE(ADD_MONTHS(TRUNC(cte_date, 'MONTH'), 1)) BETWEEN TRUNC(TO_DATE('27-11-2014','DD-MM-YYYY'), 'MONTH') AND TO_DATE ('27-01-2015','DD-MM-YYYY'))
)
SELECT * from CTE_Dates
I get:
27-JAN-14
01-FEB-14
01-MAR-14
01-APR-14
I would also want to get:
01-NOV-14
01-DEC-14
01-JAN-15
It looks like the OR portion of the WHERE clause gets ignored.
Suggestions on how to create this query?
Thanks
Cory
The problem with what you have now (aside from extra cast() and to_date() calls) is that on the fourth iteration both the conditions are false so the recursion stops; there's nothing to make it skip a bit and pick up again, otherwise it would continue forever. I don't think you can achieve both ranges within the recursion.
You can put the latest date you want inside the recursive part, and then filter the two ranges you want afterwards:
WITH CTE_Dates (cte_date) AS (
SELECT date '2014-01-27' from dual
UNION ALL
SELECT ADD_MONTHS(TRUNC(cte_date, 'MONTH'), 1)
FROM CTE_Dates
WHERE ADD_MONTHS(TRUNC(cte_date, 'MONTH'), 1) <= date '2015-01-27'
)
SELECT * from CTE_Dates
WHERE cte_date BETWEEN date '2014-01-27' AND date '2014-04-27'
OR cte_date BETWEEN date '2014-11-27' AND date '2015-01-27';
CTE_DATE
---------
27-JAN-14
01-FEB-14
01-MAR-14
01-APR-14
01-DEC-14
01-JAN-15
6 rows selected
You can replace the hard-coded values with your pairs of start and end dates. If the ranges might overlap or the second range could be (or end) before the first one, you could pick the higher date:
WHERE ADD_MONTHS(TRUNC(cte_date, 'MONTH'), 1)
<= greatest(date '2015-01-27', date '2014-04-27')
... though that only makes sense with variables, not fixed values.

Resources