Oracle. Select and function - sql

I have function like this:
CREATE OR REPLACE FUNCTION IntervalToSec(Run_Duration interval day to second) RETURN NUMBER IS
vSeconds NUMBER ;
BEGIN
SELECT EXTRACT( DAY FROM Run_Duration ) * 86400
+ EXTRACT( HOUR FROM Run_Duration ) * 3600
+ EXTRACT( MINUTE FROM Run_Duration ) * 60
+ EXTRACT( SECOND FROM Run_Duration )
INTO
vSeconds
FROM DUAL ;
RETURN vSeconds ;
END;
That converts interval data to total seconds number
Then i have select query:
select RUN_DURATION from SYS.USER_SCHEDULER_JOB_RUN_DETAILS WHERE JOB_NAME = 'WASTE_MESSAGES_JOB' and LOG_DATE > (systimestamp - INTERVAL '0 00:10:00.0'DAY TO SECOND(1) )
order by LOG_DATE desc;
Output like:+00 00:00:01.000000
Question is how can i pipe query result into function?
Thank you!

A user defined function doesn't behave any differently to built-in functions, so you call it as you would any other function - to_char, to_date, trunc, round etc.
You can write user-defined functions in PL/SQL, Java, or C to provide functionality that is not available in SQL or SQL built-in functions. User-defined functions can appear in a SQL statement wherever an expression can occur.
For example, user-defined functions can be used in the following:
The select list of a SELECT statement
...
So just call the function as part of the query:
select IntervalToSec(RUN_DURATION)
from SYS.USER_SCHEDULER_JOB_RUN_DETAILS
WHERE JOB_NAME = 'WASTE_MESSAGES_JOB'
and LOG_DATE > (systimestamp - INTERVAL '0 00:10:00.0'DAY TO SECOND(1) )
order by LOG_DATE desc;
As long as the queried column is the same data type the function expects (or can be implicitly converted to that type) it can be passed as the function's argument.

https://docs.oracle.com/cd/B13789_01/appdev.101/b10807/13_elems045.htm
I believe a select Into statement will work for this? If you just want the query results to go into the function statement. Parameters for the into say it will take a function_name. Of course I could be way off as I've never used it for getting information into functions.

Related

How to pass the number of days to a Postgres function?

Argument days in function getAvgByDay() doesn't work, I guess because it is inside quotes:
CREATE OR REPLACE FUNCTION getAvgByDay(days int)
RETURNS TABLE ( average text,
date timestamp with time zone
) AS
$func$
BEGIN
RETURN QUERY
SELECT to_char( AVG(measure), '99999D22') AS average, ( now() - interval '$1 day') AS date
FROM (
SELECT mes.date, mes.measure
FROM measures mes
WHERE mes.date < ( now() - interval '$1 day')
) AS mydata;
END
$func$
LANGUAGE plpgsql;
Assuming the column measures.date is actually data type timestamptz and not a date:
CREATE OR REPLACE FUNCTION get_avg_by_day(_days int)
RETURNS TABLE (average text, ts timestamptz) AS -- not using "date" for a timestamp
$func$
SELECT to_char(avg(measure), '99999D22') -- AS average
, now() - interval '1 day' * $1 -- AS ts
FROM measures m
WHERE m.date < now() - interval '1 day' * $1
$func$ LANGUAGE sql;
No need for PLpgSQL, can be a simper SQL function.
Difference between language sql and language plpgsql in PostgreSQL functions
No need for a subquery. Only adds complexity and cost for no gain.
No need for column aliases in the outer query level. Those are not used, as visible column names are defined in the RETURNS clause.
No need for extra parentheses. Operator precedence works as desired anyway. (No harm in this case, either.)
Don't use CaMeL case identifier in Postgres if you can avoid it.
Are PostgreSQL column names case-sensitive?
Don't call a timestamptz column "date". That's misleading. Using "ts" instead.
Most importantly: You suspected as much, and "sticky bit" already explained: no interpolation inside strings. But just multiply the time unit with your integer input to subtract the given number of days:
interval '1 day' * $1
That's faster and cleaner than string concatenation.
There's no interpolation in strings. But you can concatenate strings and cast them to an interval. Try:
... concat(days, ' day')::interval ...
Or you could use format(), that's probably a little closer to what you originally had:
... format('%s day', days)::interval ...

PostgreSQL: SELECT integers as DATE or TIMESTAMP

I have a table where I have multiple integer columns: year, month and day. Unfortunately, while the three should have been grouped into one DATE column from the beginning, I am now stuck and now need to view it as such. Is there a function that can do something along the lines of:
SELECT makedate(year, month, day), othercolumn FROM tablename;
or
SELECT maketimestamp(year, month, day, 0, 0), othercolumn FROM tablename;
You can
SELECT format('%s-%s-%s', "year", "month", "day")::date
FROM ...
or use date maths:
SELECT DATE '0001-01-01'
+ ("year"-1) * INTERVAL '1' YEAR
+ ("month"-1) * INTERVAL '1' MONTH
+ ("day"-1) * INTERVAL '1' DAY
FROM ...
Frankly, it's surprising that PostgreSQL doesn't offer a date-constructor like you describe. It's something I should think about writing a patch for.
In fact, a quick look at the sources shows that there's an int date2j(int y, int m, int d) function at the C level already, in src/backend/utils/adt/datetime.c. It just needs to be exposed at the SQL level with a wrapper to convert to a Datum.
OK, now here's a simple makedate extension that adds a single function implemented in C, named makedate. A pure-SQL version is also provided if you don't want to compile and install an extension. I'll submit the C function for the 9.4 commitfest; meanwhile that extension can be installed to provide a fast and simple date constructor:
regress=# SELECT makedate(2012,01,01);
makedate
------------
2012-01-01
(1 row)
PostgreSQL 9.4+
In PostgreSQL 9.4, a function was added to do just this
make_date(year int, month int, day int)
There may be a more elegant method, but this will give you a date.
select to_date(to_char(year * 10000 + month * 100 + day,'00000000'), 'yyyymmdd')
from tablename;
Try something like:
SELECT year * interval '1 year' +
month * interval '1 month' +
day * interval '1 day'
FROM tablename;

Date difference get different results from function than from select statement

I need to get the difference of 2 date fields, if the greater date is null then I'll use SYSDATE instead. Having this requirement, I created a function to solve this issues (note: this code follows the standard of the organization, not my personal taste)
CREATE FUNCTION F_GET_DIFFERENCE (P_WORKFLOWID NUMBER)
RETURN NUMBER --result in minutes
IS
TIME NUMBER;
BEGIN
TIME := 0
SELECT
F_WORKTIME_DIFF(NVL(X.ENDDATE, SYSDATE), X.STARTDATE)
INTO
TIME
FROM
TABLEX X
WHERE
X.WORKFLOWID = P_WORKFLOWID;
RETURN TIME;
EXCEPTION
WHEN OTHERS THEN
RETURN 0;
END;
The F_WORKTIME_DIFF function already exists and calculates the worktime of the day (assumming nobody works at 12 a.m. and things like that). The problem is when calling this function, the result contains an additional amount of time. That's very strange, because when executing the query in the function, it returns the expected output.
Example (important: date format in Peru is DD/MM/YYYY HH24:MI:SS)
TABLEX
WORKFLOWID STARTDATE ENDDATE
1 '01/12/2012 10:00:00' null
Assumming that the server day is the same day (01/12/2012) but greater time (10:01:00), we execute the function:
SELECT F_GET_DIFFERENCE(1)
FROM DUAL;
The result is: 14.
Now, executing the query in the function and having the server time at 10:02:00, the result is 2 (exact output).
I even tried executing this
SELECT
F_WORKTIME_DIFF(NVL(X.ENDDATE, SYSDATE), X.STARTDATE) SELECT_WAY,
F_GET_DIFFERENCE(1) FUNCTION_WAY
FROM
TABLEX X
WHERE
X.WORKFLOWID = 1
And the result is (having the server time at 10:10:00)
SELECT_WAY FUNCTION_WAY
10 24
Is maybe any consideration that I must take into account when working with Oracle dates in inner functions or anything that could explain this odd behavior?
It is difficult to tell anything without seeing the function F_WORKTIME_DIFF.
Whatever is the datatype returned from F_WORKTIME_DIFF, it is casted to number when assigned to the variable time. This may be a clue.
This may not be exactly what are you looking for but the first example gives you hours diff between two dates:
Select EXTRACT(HOUR FROM (SYSDATE - trunc(SYSDATE )) DAY TO SECOND ) From dual
/
Select
EXTRACT(hour From Cast(SYSDATE as timestamp)) hh,
EXTRACT(minute From Cast(SYSDATE as timestamp)) mi,
EXTRACT(second From Cast(SYSDATE as timestamp)) ss
From dual
/

Optimizing SQL Query and Dynamically using current date

I am trying to optimize a simple SQL query and was wondering if anyone has any suggestions. I am developing using Oracle SQL Developer (which I don't like) on an Oracle 11g database. The query I am using is:
SELECT count(*)
FROM my_table
WHERE my_date
BETWEEN TO_DATE('2012-5-09T05.00.00','YYYY-MM-DD"T"HH24:MI:SS')
AND TO_DATE('2012-5-10T04.59.59','YYYY-MM-DD"T"HH24:MI:SS')
AND my_code='33'
GROUP BY my_code;
Also, I want to be able to use this query dynamically by changing the part of the date to be whatever the current date is, but I want to be able to specify the hour. So I want to be comparing something like:
getdate() + 'T05.00.00'
I have no idea how to do this and the getdate() function doesn't seem to work in SQL Developer/I don't know how to use it correctly.
So what I'm looking for is optimization suggestions and pointers on how to just dynamically change the day-month-year part of the date I want to constrain my results to. Thanks!
To get current date, you can use SYSDATE. To add x number of hours to it, you can add x/24. So something like this:
Example: Get current date + 5 hours
SELECT SYSDATE + 5/24 FROM dual
So in your example:
SELECT count(*)
FROM my_table
WHERE my_date
BETWEEN sysdate
AND sysdate + 5/24 -- if you want 5 hours ahead, for example
AND my_code='33'
GROUP BY my_code;
If you want to be able to change the number of hours, you could make this code into a function, and pass in the hours and code as variables.
Something like this:
CREATE FUNCTION myfunc
(
p_num_hours INT
, p_my_code VARCHAR
) RETURN INT
AS
l_ret INT;
BEGIN
SELECT count(*)
INTO l_ret
FROM my_table
WHERE my_date
BETWEEN sysdate
AND sysdate + p_num_hours/24
AND my_code=p_my_code
RETURN l_ret;
END;
As an alternative to adding fractional days via expressions such as "5 / 24" you might want to use an INTERVAL constant. For example:
SELECT count(*)
FROM my_table
WHERE my_date BETWEEN (TRUNC(SYSDATE) + INTERVAL '5' HOUR)
AND (TRUNC(SYSDATE) + INTERVAL '1' DAY +
INTERVAL '5' HOUR - INTERVAL '1' SECOND) AND
my_code='33'
GROUP BY my_code
I like to use INTERVAL constants because it's quite clear what these constants represent. With the fractional-day constants I sometimes get confused ('course, I sometimes get confused, regardless... :-)
Share and enjoy.
If I understand correctly, something like
select count(*)
from my_table
where trunc(my_date) = trunc(sysdate)
and my_code = '33'
group by my_code;
or
select count(*)
from my_table
where my_date
between sysdate and sysdate + 5/24
and my_code = '33'
group by my_code;
HTH.
Alessandro

How to average time intervals?

In Oracle 10g I have a table that holds timestamps showing how long certain operations took. It has two timestamp fields: starttime and endtime. I want to find averages of the durations given by these timestamps. I try:
select avg(endtime-starttime) from timings;
But get:
SQL Error: ORA-00932: inconsistent
datatypes: expected NUMBER got
INTERVAL DAY TO SECOND
This works:
select
avg(extract( second from endtime - starttime) +
extract ( minute from endtime - starttime) * 60 +
extract ( hour from endtime - starttime) * 3600) from timings;
But is really slow.
Any better way to turn intervals into numbers of seconds, or some other way do this?
EDIT:
What was really slowing this down was the fact that I had some endtime's before the starttime's. For some reason that made this calculation incredibly slow. My underlying problem was solved by eliminating them from the query set. I also just defined a function to do this conversion easier:
FUNCTION fn_interval_to_sec ( i IN INTERVAL DAY TO SECOND )
RETURN NUMBER
IS
numSecs NUMBER;
BEGIN
numSecs := ((extract(day from i) * 24
+ extract(hour from i) )*60
+ extract(minute from i) )*60
+ extract(second from i);
RETURN numSecs;
END;
There is a shorter, faster and nicer way to get DATETIME difference in seconds in Oracle than that hairy formula with multiple extracts.
Just try this to get response time in seconds:
(sysdate + (endtime - starttime)*24*60*60 - sysdate)
It also preserves fractional part of seconds when subtracting TIMESTAMPs.
See http://kennethxu.blogspot.com/2009/04/converting-oracle-interval-data-type-to.html for some details.
Note that custom pl/sql functions have significant performace overhead that may be not suitable for heavy queries.
If your endtime and starttime aren't within a second of eachother, you can cast your timestamps as dates and do date arithmetic:
select avg(cast(endtime as date)-cast(starttime as date))*24*60*60
from timings;
It doesn't look like there is any function to do an explicit conversion of INTERVAL DAY TO SECOND to NUMBER in Oracle. See the table at the end of this document which implies there is no such conversion.
Other sources seem to indicate that the method you're using is the only way to get a number from the INTERVAL DAY TO SECOND datatype.
The only other thing you could try in this particular case would be to convert to number before subtracting them, but since that'll do twice as many extractions, it will likely be even slower:
select
avg(
(extract( second from endtime) +
extract ( minute from endtime) * 60 +
extract ( hour from endtime ) * 3600) -
(extract( second from starttime) +
extract ( minute from starttime) * 60 +
extract ( hour from starttime ) * 3600)
) from timings;
SQL Fiddle
Oracle 11g R2 Schema Setup:
Create a type to use when performing a custom aggregation:
CREATE TYPE IntervalAverageType AS OBJECT(
total INTERVAL DAY(9) TO SECOND(9),
ct INTEGER,
STATIC FUNCTION ODCIAggregateInitialize(
ctx IN OUT IntervalAverageType
) RETURN NUMBER,
MEMBER FUNCTION ODCIAggregateIterate(
self IN OUT IntervalAverageType,
value IN INTERVAL DAY TO SECOND
) RETURN NUMBER,
MEMBER FUNCTION ODCIAggregateTerminate(
self IN OUT IntervalAverageType,
returnValue OUT INTERVAL DAY TO SECOND,
flags IN NUMBER
) RETURN NUMBER,
MEMBER FUNCTION ODCIAggregateMerge(
self IN OUT IntervalAverageType,
ctx IN OUT IntervalAverageType
) RETURN NUMBER
);
/
CREATE OR REPLACE TYPE BODY IntervalAverageType
IS
STATIC FUNCTION ODCIAggregateInitialize(
ctx IN OUT IntervalAverageType
) RETURN NUMBER
IS
BEGIN
ctx := IntervalAverageType( INTERVAL '0' DAY, 0 );
RETURN ODCIConst.SUCCESS;
END;
MEMBER FUNCTION ODCIAggregateIterate(
self IN OUT IntervalAverageType,
value IN INTERVAL DAY TO SECOND
) RETURN NUMBER
IS
BEGIN
IF value IS NOT NULL THEN
self.total := self.total + value;
self.ct := self.ct + 1;
END IF;
RETURN ODCIConst.SUCCESS;
END;
MEMBER FUNCTION ODCIAggregateTerminate(
self IN OUT IntervalAverageType,
returnValue OUT INTERVAL DAY TO SECOND,
flags IN NUMBER
) RETURN NUMBER
IS
BEGIN
IF self.ct = 0 THEN
returnValue := NULL;
ELSE
returnValue := self.total / self.ct;
END IF;
RETURN ODCIConst.SUCCESS;
END;
MEMBER FUNCTION ODCIAggregateMerge(
self IN OUT IntervalAverageType,
ctx IN OUT IntervalAverageType
) RETURN NUMBER
IS
BEGIN
self.total := self.total + ctx.total;
self.ct := self.ct + ctx.ct;
RETURN ODCIConst.SUCCESS;
END;
END;
/
Then you can create a custom aggregation function:
CREATE FUNCTION AVERAGE( difference INTERVAL DAY TO SECOND )
RETURN INTERVAL DAY TO SECOND
PARALLEL_ENABLE AGGREGATE USING IntervalAverageType;
/
Query 1:
WITH INTERVALS( diff ) AS (
SELECT INTERVAL '0' DAY FROM DUAL UNION ALL
SELECT INTERVAL '1' DAY FROM DUAL UNION ALL
SELECT INTERVAL '-1' DAY FROM DUAL UNION ALL
SELECT INTERVAL '8' HOUR FROM DUAL UNION ALL
SELECT NULL FROM DUAL
)
SELECT AVERAGE( diff ) FROM intervals
Results:
| AVERAGE(DIFF) |
|---------------|
| 0 2:0:0.0 |
Well, this is a really quick and dirty method, but what about storing the seconds difference in a separate column (you'll need to use a trigger or manually update this if the record changes) and averaging over that column?
Unfortunately Oracle does not support most functions with intervals. There are a number of workarounds for this, but they all have some kind of drawback (and notably, none are ANSI-SQL compliant).
The best answer (as #justsalt later discovered) is to write a custom function to convert the intervals into numbers, average the numbers, then (optionally) convert back to intervals. Oracle 12.1 and later support doing this using a WITH block to declare a function:
with
function fn_interval_to_sec(i in dsinterval_unconstrained)
return number is
begin
return ((extract(day from i) * 24
+ extract(hour from i) )*60
+ extract(minute from i) )*60
+ extract(second from i);
end;
select numtodsinterval(avg(fn_interval_to_sec(endtime-starttime)), 'SECOND')
from timings;
If you are on 11.2 or earlier, or if you prefer not to include functions in your SQL statements, you can declare it as a stored function:
create or replace function fn_interval_to_sec(i in dsinterval_unconstrained)
return number is
begin
return ((extract(day from i) * 24
+ extract(hour from i) )*60
+ extract(minute from i) )*60
+ extract(second from i);
end;
You can then use it in SQL as expected:
select numtodsinterval(avg(fn_interval_to_sec(endtime-starttime)), 'SECOND')
from timings;
Using dsinterval_unconstrained
Using the PL/SQL type alias dsinterval_unconstrained for the function parameter ensures you have maximum precision/scale; INTERVAL DAY TO SECOND defaults DAY precision to 2 digits (meaning anything at or over ±100 days is an overflow and throws an exception) and SECOND scale to 6 digits.
Additionally, Oracle 12.1 will raise a PL/SQL error if you try to specify any precision/scale in your parameter:
with
function fn_interval_to_sec(i in interval day(9) to second(9))
return number is
...
ORA-06553: PLS-103: Encountered the symbol "(" when expecting one of the following: to
Alternatives
Custom aggregate function
Oracle supports custom aggregate functions written in PL/SQL, which would allow you to make minimal changes to the statement:
select ds_avg(endtime-starttime) from timings;
However, this approach has several major drawbacks:
You have to create the PL/SQL aggregate objects in your database, which may not be desired or allowed;
You cannot name it avg, as Oracle will always use the builtin avg function rather than your own. (Technically you can, but then you have to qualify it with schema, which defeats the purpose.)
As #vadzim noted, aggregate PL/SQL functions have significant performance overhead.
Date arithmetic
If your values are not significantly far apart, #vadzim's approach works as well:
select avg((sysdate + (endtime-starttime)*24*60*60*1000000 - sysdate)/1000000.0)
from timings;
Be aware, though, that if the interval is too great, the (endtime-starttime)*24*60*60*1000000 expression will overflow and throw ORA-01873: the leading precision of the interval is too small. At this precision (1μs) the difference cannot be greater than or equal to 00:16:40 in magnitude, so it is safe for small intervals, but not all.
Finally, if you are comfortable losing all subsecond precision, you can cast the TIMESTAMP columns to DATE; subtracting a DATE from a DATE will return the number of days with second precision (credit to #jimmyorr):
select avg(cast(endtime as date)-cast(starttime as date))*24*60*60
from timings;