How to average time intervals? - sql

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;

Related

How do I join a string and an int in PostgreSQL?

I have a procedure with an int parameter.
CREATE OR REPLACE PROCEDURE update_retention_policy(id int, days int)
language plpgsql
AS
$$
BEGIN
PERFORM add_retention_policy(('schema_' + id + '.my_hypertable'), days * INTERVAL '1 day', true);
END
$$;
The syntax for the add_retention_policy function is add_retention_policy('hypertable', INTERVAL 'x days', true). I want to prefix the hypertable with the schema which is always 'schema_' and then followed by the id parameter, how do I do that?
You just need to rewrite the INTERVAL part in your function call as days * INTERVAL '1 day'.
Instead of concatenating strings, you multiply the '1 day' interval by the days param.
EDIT: for the id part, you can just use the || operator, which is the string concatenation operator in Postgres, instead of +. You shouldn't even need to explicitly cast id to character varying

ORA-01873: the leading precision of the interval is too smalll

I am getting an ORA-01873 "leading precision of the interval is too small" error from this statement and can't figure out why:
The v_not_auto_bl_num is declared as VARCHAR2(1000).
What is causing the error?
In the code you originally posted you are doing:
ABS( EXTRACT(DAY FROM (TO_TIMESTAMP(DHS.ASSIGNMENT_IODT,'YYYYMMDDHH24MISS.FF')
- TO_TIMESTAMP(DHS.COMPLETED_IODT,'YYYYMMDDHH24MISS.FF')) *86400*1000) / 1000)
The relevant part is this:
(TO_TIMESTAMP(DHS.ASSIGNMENT_IODT,'YYYYMMDDHH24MISS.FF')
- TO_TIMESTAMP(DHS.COMPLETED_IODT,'YYYYMMDDHH24MISS.FF')) *86400*1000
If you subtract two timestamps you get an interval data type, not a number; e.g. if your table columns were, say, '20170419065416' and '20170419000000' then subtracting them would generate:
(TO_TIMESTAMP(DHS.A
-------------------
+00 06:54:16.000000
If you multiply that by 86400*1000 you exceed the precision of the interval data type. I chose that value because one second less is OK:
with dhs (assignment_iodt, completed_iodt) as (
select '20170419065415', '20170419000000' from dual
)
select (TO_TIMESTAMP(DHS.ASSIGNMENT_IODT,'YYYYMMDDHH24MISS.FF')
- TO_TIMESTAMP(DHS.COMPLETED_IODT,'YYYYMMDDHH24MISS.FF')) as original,
(TO_TIMESTAMP(DHS.ASSIGNMENT_IODT,'YYYYMMDDHH24MISS.FF')
- TO_TIMESTAMP(DHS.COMPLETED_IODT,'YYYYMMDDHH24MISS.FF')) *86400*1000 as multiplied
from dhs;
ORIGINAL MULTIPLIED
------------------- -------------------------
+00 06:54:15.000000 +24855000 00:00:00.000000
Once second more (or, in fact, anything beyond 20170419065415.134814814, or any pair of values with the actual interval above 06:54:15.134814814) will error as the multiplied interval is out of range for the data type.
What's actually happening under the hood is unclear; using a smaller multiplier also causes the issues once you cross that raw interval size limit.
Anyway, you seem to be trying to get the number of while seconds, which you can do by extracting each time element and multiplying them individually:
select abs(
(extract(day from diff) * 86400)
+ (extract (hour from diff) * 3600)
+ (extract (minute from diff) * 60)
+ trunc(extract (second from diff))
) as c_f_previous_time
from (
select to_timestamp(dhs.assignment_iodt,'YYYYMMDDHH24MISS.FF')
- to_timestamp(dhs.completed_iodt,'YYYYMMDDHH24MISS.FF') as diff
from dhs
);
I've put the timestamp subtraction in an inline view just so it doesn't have to be repeated within each extract call. You can put the rest of your original query inside that inline view (or a CTE) too.
Incidentally, the abs() implies you can have rows in your table where the completed date is earlier than the assignment; or just that you didn't notice you're doing the subtraction the wrong way round. If you data cannot have completed before assigned then you can swap the terms over and lose the abs(); I'd probably swap the terms anyway just to make it look more logical.
first try this:
create table test_table as
SELECT ACT_BL.BL_NUM,
ABS( EXTRACT(DAY FROM (TO_TIMESTAMP(DHS.ASSIGNMENT_IODT,'YYYYMMDDHH24MISS.FF') - TO_TIMESTAMP(DHS.COMPLETED_IODT,'YYYYMMDDHH24MISS.FF')) *86400*1000) / 1000) AS C_F_PREVIOUS_TIME
FROM DOCI_ACTIVITY ACT ,
DOCI_ACTIVITY_RELATED_BL ACT_BL ,
DSH_ACTIVITY DHS
WHERE ACTIVITY_TYPE IN ('BlCodingAndFormatting','BlCreationFromESI')
AND ACT.ACTIVITY_ID =ACT_BL.ACTIVITY_ID
AND ACT_BL.ACTIVITY_ID = DHS.ACTIVITY_ID
AND ACT_BL.BL_NUM = v_not_auto_bl_num;
then check the test_table columns type(BL_NUM and C_F_PREVIOUS_TIME)
after that you apply that column types to your table
In your case, the exception is raised when you multiply an interval by 86400.
As I've posted here you could use the following shorter method to convert interval to milliseconds.
SELECT ROUND((EXTRACT(DAY FROM (
TO_TIMESTAMP(DHS.ASSIGNMENT_IODT,'YYYYMMDDHH24MISS.FF') - TO_TIMESTAMP(DHS.COMPLETED_IODT ,'YYYYMMDDHH24MISS.FF')
) * 24 * 60) * 60 + EXTRACT(SECOND FROM (
TO_TIMESTAMP(DHS.ASSIGNMENT_IODT,'YYYYMMDDHH24MISS.FF') - TO_TIMESTAMP(DHS.COMPLETED_IODT ,'YYYYMMDDHH24MISS.FF')
))) * 1000) AS MILLIS FROM DUAL;
Your numeric number appears to be too large for the ABS function to handle. The biggest value you can pass to ABS() as the number is 2^31-1:

Oracle. Select and function

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.

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
/

Oracle sql - date subtraction within a function

(moved from previous post) - sorry if this is seen as a repeat!
Hi everyone,
Just having some issues with date calculations and subtracting from a date within a function.
I am confused with the data types I should be using as I am having to convert from date to_char for example. I'm not sure whether to have the return type as varchar2 or date?
Here is the function that receives a car_id, looks in the table for that car, pulls out the cars arrive date, stores it in a date variable
does the same with the departure date.
Then converts both dates to_char, does the subtraction and returns it in a varchar2.
When I call the function it will inputting the car_id as a parameter,
then storing the returned result in a varchar variable, for a dbms output.
eg.
v_result := get_duration('0001')
here is the function:
DROP FUNCTION get_duration;
CREATE FUNCTION get_duration (p_car_id number)
RETURN varchar2 is
v_arrive date;
v_depart date
v_duration varchar2(25); --not too sure about this variable choice
begin
select arrival, departure
into v_arrive, v_depart
from car_info
where car_id = p_car_id;
v_duration := to_char(v_depart, 'dd-mon-yyyy hh24:mi:ss') - to_char(v_arrive, 'dd-mon-yyyy hh24:mi:ss')
return v_duration;
END;
/
As you can see, I am trying to put the start and end times from the table in to the variables, then minus the end date from the start date.
The function compiles, but with a warning, and when I got to call the function, the error: get_duration is invalid
Any input on this would be greatly received,
Sorry if the post is relatively big!
regards,
Darren
Trivial problems are that you're missing a ; when you define v_depart, and at the end of the line you assign the value to v_duration; and you're mixing up your variable names. (You're also inconsistent about the type of car_info.id; you've created it as a varchar when it probably ought to be a number, but that's more of a comment on your previous question).
The main problem is that you can't perform a minus on two strings, as that doesn't really mean anything. You need to do the manipulation of the original dates, and then figure out how you want to return the result to the caller.
Subtracting one date from another gives a number value, which is the number of days; partial days are fractions, so 0.25 is 6 hours. With the dates from your previous quesiton, this query:
select arrival, departure, departure - arrival as duration
from car_info
where car_id = 1;
... shows duration of 2.125, which is 2 days and 3 hours.
This isn't the best way to do this, but to show you the process of what's going on I'll use that duration number and convert it into a string in quite a long-winded way:
CREATE OR REPLACE FUNCTION get_duration (p_car_id number)
RETURN varchar2 is
v_arrive date;
v_depart date;
v_duration number;
v_days number;
v_hours number;
v_minutes number;
v_seconds number;
BEGIN
select arrival, departure, departure - arrival
into v_arrive, v_depart, v_duration
from car_info
where car_id = p_car_id;
-- Days is the whole-number part, which you can get with trunc
v_days := trunc(v_duration);
-- Hours, minutes and seconds are extracted from the remainder
v_hours := trunc(24 * (v_duration - v_days));
v_minutes := trunc(60 * (v_duration - v_days - (v_hours/24)));
v_seconds := trunc(60 * (v_duration - v_days - (v_hours/24)
- (v_minutes/(24*60))));
return v_days || ' days '
|| to_char(v_hours, '00') || ' hours '
|| to_char(v_minutes, '00') || ' minutes '
|| to_char(v_seconds, '00') || ' seconds';
END;
/
Function created.
show errors
No errors.
select get_duration(1) from dual;
GET_DURATION(1)
--------------------------------------------------------------------------------
2 days 03 hours 00 minutes 00 seconds
You can play with the number format masks etc. to get the output you want.
DATE arithmetic works on DATEs not on VARCHAR2s:
CREATE FUNCTION get_duration (p_car_id number)
RETURN varchar2 is
v_arrive date;
v_depart date
v_duration number; --<<<
begin
select arrival, departure
into v_arrive, v_depart
from car_info
where car_id = p_car_id;
v_duration := v_arrive - v_depart;
return v_duration;
END;
That gives a result in days.
You cannot subtract dates once you cast them into varchar2 (to_char function does that!). Instead use v_duration := v_stop - v_start; which will give you the result 'v_duration' in days. Then you can convert it to hours, minutes, etc..