Oracle equivalent to SQL Server/Sybase DateDiff - sql

We are now using NHibernate to connect to different database base on where our software is installed. So I am porting many SQL Procedures to Oracle.
SQL Server has a nice function called DateDiff which takes a date part, startdate and enddate.
Date parts examples are day, week, month, year, etc. . .
What is the Oracle equivalent?
I have not found one do I have to create my own version of it?
(update by Mark Harrison) there are several nice answers that explain Oracle date arithmetic. If you need an Oracle datediff() see Einstein's answer. (I need this to keep spme SQL scripts compatible between Sybase and Oracle.) Note that this question applies equally to Sybase.

I stole most of this from an old tom article a few years ago, fixed some bugs from the article and cleaned it up. The demarcation lines for datediff are calculated differently between oracle and MSSQL so you have to be careful with some examples floating around out there that don't properly account for MSSQL/Sybase style boundaries which do not provide fractional results.
With the following you should be able to use MSSQL syntax and get the same results as MSSQL such as SELECT DATEDIFF(dd,getdate(),DATEADD(dd,5,getdate())) FROM DUAL;
I claim only that it works - not that its effecient or the best way to do it. I'm not an Oracle person :) And you might want to think twice about using my function macros to workaround needing quotes around dd,mm,hh,mi..etc.
(update by Mark Harrison) added dy function as alias for dd.
CREATE OR REPLACE FUNCTION GetDate
RETURN date IS today date;
BEGIN
RETURN(sysdate);
END;
/
CREATE OR REPLACE FUNCTION mm RETURN VARCHAR2 IS BEGIN RETURN('mm'); END;
/
CREATE OR REPLACE FUNCTION yy RETURN VARCHAR2 IS BEGIN RETURN('yyyy'); END;
/
CREATE OR REPLACE FUNCTION dd RETURN VARCHAR2 IS BEGIN RETURN('dd'); END;
/
CREATE OR REPLACE FUNCTION dy RETURN VARCHAR2 IS BEGIN RETURN('dd'); END;
/
CREATE OR REPLACE FUNCTION hh RETURN VARCHAR2 IS BEGIN RETURN('hh'); END;
/
CREATE OR REPLACE FUNCTION mi RETURN VARCHAR2 IS BEGIN RETURN('mi'); END;
/
CREATE OR REPLACE FUNCTION ss RETURN VARCHAR2 IS BEGIN RETURN('ss'); END;
/
CREATE OR REPLACE Function DateAdd(date_type IN varchar2, offset IN integer, date_in IN date )
RETURN date IS date_returned date;
BEGIN
date_returned := CASE date_type
WHEN 'mm' THEN add_months(date_in,TRUNC(offset))
WHEN 'yyyy' THEN add_months(date_in,TRUNC(offset) * 12)
WHEN 'dd' THEN date_in + TRUNC(offset)
WHEN 'hh' THEN date_in + (TRUNC(offset) / 24)
WHEN 'mi' THEN date_in + (TRUNC(offset) /24/60)
WHEN 'ss' THEN date_in + (TRUNC(offset) /24/60/60)
END;
RETURN(date_returned);
END;
/
CREATE OR REPLACE Function DateDiff( return_type IN varchar2, date_1 IN date, date_2 IN date)
RETURN integer IS number_return integer;
BEGIN
number_return := CASE return_type
WHEN 'mm' THEN ROUND(MONTHS_BETWEEN(TRUNC(date_2,'MM'),TRUNC(date_1, 'MM')))
WHEN 'yyyy' THEN ROUND(MONTHS_BETWEEN(TRUNC(date_2,'YYYY'), TRUNC(date_1, 'YYYY')))/12
WHEN 'dd' THEN ROUND((TRUNC(date_2,'DD') - TRUNC(date_1, 'DD')))
WHEN 'hh' THEN (TRUNC(date_2,'HH') - TRUNC(date_1,'HH')) * 24
WHEN 'mi' THEN (TRUNC(date_2,'MI') - TRUNC(date_1,'MI')) * 24 * 60
WHEN 'ss' THEN (date_2 - date_1) * 24 * 60 * 60
END;
RETURN(number_return);
END;
/

JohnLavoie - you don't need that. DATE in Oracle is actually a date and time data type. The only difference between DATE and TIMESTAMP is that DATE resolves down to the second but TIMESTAMP resolves down to the micro second. Therefore the Ask Tom article is perfectly valid for TIMESTAMP columns as well.

Tom's article is very old. It only discusses the DATE type. If you use TIMESTAMP types then date arithmetic is built into PL/SQL.
http://www.akadia.com/services/ora_date_time.html
DECLARE
ts_a timestamp;
ts_b timestamp;
diff interval day to second;
BEGIN
ts_a := systimestamp;
ts_b := systimestamp-1/24;
diff := ts_a - ts_b;
dbms_output.put_line(diff);
END;
+00 01:00:00.462000
or
DECLARE
ts_b timestamp;
ts_a timestamp;
date_part interval day to second;
BEGIN
ts_a := systimestamp;
date_part := to_dsinterval('0 01:23:45.678');
ts_b := ts_a + date_part;
dbms_output.put_line(ts_b);
END;
04-SEP-08 05.00.38.108000 PM

YOU Could write a function in oracle for this
function datediff( p_what in varchar2, p_d1 in date, p_d2 in date) return number as l_result number;
BEGIN
select (p_d2-p_d1) *
decode( upper(p_what), 'SS', 24*60*60, 'MI', 24*60, 'HH', 24, NULL )
into l_result from dual;
return l_result;
END;
and use it like :
DATEDIFF('YYYY-MM-DD', SYSTIMESTAMP, SYSTIMESTAMP)

Related

PL/SQL function with date parameter returns wrong output

In order to make my main code more readable, I have wrapped an operation to convert a date to epoch format in a function. However, the function returns an incorrect value. What am I doing wrong?
What I get
Function
create or replace function date_to_epoch(
date_in in date)
return number
is
epoch_out number;
begin
epoch_out := round((to_date(date_in, 'yyyy-mm-dd hh24:mi:ss') - to_date('1970-01-01', 'yyyy-mm-dd'))*24*60*60);
return epoch_out;
end;
I call the function with:
declare
date_out number;
begin
select date_to_epoch(to_date('2020-01-01 00:00:00', 'yyyy-mm-dd hh24:mi:ss')) into date_out from dual;
DBMS_OUTPUT.PUT_LINE('Output:' || date_out);
end;
returns: -62134128000
What I expect
I get the expected output when I run the same operation in a simple SQL statement:
select round((to_date('2020-01-01', 'yyyy-mm-dd hh24:mi:ss') - to_date('1970-01-01', 'yyyy-mm-dd'))*24*60*60) from dual;
returns: 1577836800
Your input parameter is a date already, so you should not use to_date() on it. Bad things happen when you do that.
I would write your code as:
create or replace function date_to_epoch(date_in in date)
return number
is
epoch_out number;
begin
epoch_out := round((date_in - date '1970-01-01')*24*60*60);
return epoch_out;
end;
Note that you don't actually need an intermediate variable to store the return value of the function. You can directly do:
create or replace function date_to_epoch(date_in in date)
return number
is
begin
return round((date_in - date '1970-01-01')*24*60*60);
end;
In this demo on DB Fiddle, the code yields:
Output:1577836800
What am I doing wrong?
The TO_DATE function has the signature:
TO_DATE( date_string, format_model, nls_param )
Where all the arguments are expected to have a string data-type.
You are not passing a string as the first argument; you are passing a DATE data type. Oracle will try to be helpful and convert the DATE you are passing to a string by implicitly calling TO_CHAR using the current session's NLS date format session parameter.
So, your function is effectively:
create or replace function date_to_epoch(
date_in in date
) return number
is
epoch_out number;
begin
epoch_out := round(
( to_date(
TO_CHAR(
date_in,
( SELECT value
FROM NLS_SESSION_PARAMETERS
WHERE parameter = 'NLS_DATE_FORMAT' )
),
'yyyy-mm-dd hh24:mi:ss'
)
-
to_date(
'1970-01-01',
'yyyy-mm-dd'
)
)*24*60*60
);
return epoch_out;
end;
To fix it, you need to just use the date input as a date rather than trying to convert something that is already a date to a date:
CREATE FUNCTION date_to_epoch(
date_in IN DATE
) RETURN NUMBER DETERMINISTIC
IS
BEGIN
RETURN ROUND( ( date_in - DATE '1970-01-01' )*24*60*60 );
END date_to_epoch;
However, you also need to make sure that your dates are in the UTC time zone and if they aren't convert them to UTC before calculating the epoch:
CREATE FUNCTION date_to_epoch(
date_in IN DATE,
timezone IN VARCHAR2 DEFAULT 'UTC'
) RETURN NUMBER DETERMINISTIC
IS
BEGIN
RETURN ROUND(
( CAST(
FROM_TZ( date_in, timezone ) -- Convert to a timestamp in the correct TZ
AT TIME ZONE 'UTC' -- Change to the UTC TZ
AS DATE -- Cast back to a date
)
- DATE '1970-01-01'
)*24*60*60
);
END date_to_epoch;
/
So:
SELECT date_to_epoch( DATE '1970-01-01' ) AS utc_epoch,
date_to_epoch( DATE '1970-01-01', 'PST' ) AS pst_epoch
FROM DUAL;
outputs:
UTC_EPOCH | PST_EPOCH
--------: | --------:
0 | 28800
db<>fiddle here

Migration of PIPELINED from Oracle to Postgres

Creation of TYPE is as follows:
create or replace Type T_dates as OBJECT(
p_st_date VARCHAR2(32767),
p_en_date VARCHAR2(32767)
);
create or replace TYPE T_dates_tab AS table of T_dates;
Sample Code which needs to convert is as follows.
Oracle Code:
Function f_get_dates(p_st_date varchar2,
p_end_date varchar2,
p_st_time varchar2,
p_end_time varchar2)
return T_dates_tab PIPELINED
IS
V_DAY_COUNT NUMBER;
P_TEMP_ST_DATE VARCHAR2(100 ):=p_st_date;
BEGIN
SELECT TO_DATE(p_end_date,'MM-DD-YYYY ') - TO_DATE(p_temp_st_date,'MM-DD-YYYY ') INTO V_DAY_COUNT FROM DUAL ;
WHILE V_DAY_COUNT >= 0
LOOP
DBMS_OUTPUT.PUT_LINE(p_temp_st_date||' '||p_st_time||','||p_temp_st_date||' '||p_end_time);
PIPE ROW (T_dates((p_temp_st_date||' '||p_st_time),(p_temp_st_date||' '||p_end_time)));
SELECT TO_CHAR(TO_DATE(p_temp_st_date,'MM-DD-YYYY')+1,'MM/DD/YYYY') INTO p_temp_st_date FROM DUAL;
SELECT TO_DATE(p_end_date,'MM-DD-YYYY ') - TO_DATE(p_temp_st_date,'MM-DD-YYYY ') INTO V_DAY_COUNT FROM DUAL ;
END LOOP;
DBMS_OUTPUT.PUT_LINE('f_get_dates');
RETURN;
END f_get_dates;
Need Postgres Inputs for the above Oracle code.
This function can be massively simplified in Postgres. In fact you don't really need your own function for that:
select p_st_date::date + time '00:00:00' as p_st_date,
p_st_date::date + time '18:00:00' as p_en_date
from generate_series(date '2018-01-01', date '2018-01-30', interval '1' day) as t(p_st_date);
In contrast to your function this returns real timestamp values rather than character values. If you indeed want to treat timestamp values as character data, use to_char() to convert that.
You can put the above into a SQL function:
create function f_get_dates(p_st_date date, p_en_date date, p_st_time time, p_end_time time)
returns table (p_st_date timestamp, p_en_date timestamp)
as
$$
select p_st_date::date + p_st_time as p_st_date,
p_st_date::date + p_end_time as p_en_date
from generate_series(p_st_date, p_en_date, interval '1' day) as t(p_st_date);
$$
language sql;
If you really want to return the timestamps as character values, just format them using to_char():
create function f_get_dates(p_st_date date, p_en_date date, p_st_time time, p_end_time time)
returns table (p_st_date varchar, p_en_date varchar)
as
$$
select to_char(p_st_date::date + p_st_time, 'mm-dd-yyyy hh24:mi:ss') as p_st_date,
to_char(p_st_date::date + p_end_time, 'mm-dd-yyyy hh24:mi:ss') as p_en_date
from generate_series(p_st_date, p_en_date, interval '1' day) as t(p_st_date);
$$
language sql;

PL/SQL newbie: Functions with SELECT and SYSDATE

I'm new to SQL and was hoping for some help turning this into a function:
SELECT
SYSDATE "Today's Date",
NEXT_DAY(trunc(SYSDATE, 'MONTH')-1, 'Tuesday')"First Tuesday this Month",
NEXT_DAY (LAST_DAY(SYSDATE)+1,'TUESDAY') "First Tuesday of Next Month"
FROM DUAL;
Here's my attempt at turning the above into a function (as you can see it didn't go so hot)...
CREATE OR REPLACE FUNCTION first_tuesday
RETURN DATE
AS
date1 DATE;
date2 DATE;
date3 DATE;
BEGIN
date1 := SELECT SYSDATE;
date2 := SELECT NEXT_DAY(trunc(SYSDATE, 'MONTH')-1, 'Tuesday');
date3 := SELECT NEXT_DAY (LAST_DAY(SYSDATE)+1,'TUESDAY');
RETURN 'todays date:' || date1,
'first tuesday this month:' || date2,
'first tuesday next month:' || date3;
END;
/
SELECT first_tuesday FROM DUAL;
What am I doing wrong?
You're really trying to return three values there. I'd suggest breaking it out into separate functions, but leave "today's date" just as a call to sysdate.
You don't need to issue a SELECT query to populate a variable, and in this case you can just return an expression.
CREATE OR REPLACE FUNCTION first_tuesday
RETURN DATE
AS
BEGIN
RETURN NEXT_DAY(trunc(SYSDATE, 'MONTH')-1, 'Tuesday')
END;
/
Something to look out for here is that within the execution of a SQL statement, SYSDATE always has the same value, but that's not the case in PL/SQL. So when sysdate is evaluated in PL/SQL that is called from a SQL statement, you can get a different value in each context. So if you call:
select sysdate col1,
some_function col2
from dual;
... and during it's lengthy execution some_function calculates sysdate, it could be using a slightly different (later) value than the parent SQL statement.
The way to avoid this is to pass sysdate into the function:
CREATE OR REPLACE FUNCTION first_tuesday (sysdate_in date)
RETURN DATE
AS
BEGIN
RETURN NEXT_DAY(trunc(sysdate_in, 'MONTH')-1, 'Tuesday')
END;
/
So then you might ...
Select ...
From invoices
Where invoice_date < first_tuesday(sysdate)
That's a trivial example just to demonstrate the principle, and it really only matters for functions called from SQL.
To be honest, most practitioners in your situation would probably skip the function and just:
Select ...
From invoices
Where invoice_date < NEXT_DAY(trunc(sysdate, 'MONTH')-1, 'Tuesday')
Edit: by the way, you probably want to use:
NEXT_DAY (LAST_DAY(SYSDATE),'TUESDAY')
... not ...
NEXT_DAY (LAST_DAY(SYSDATE)+1,'TUESDAY')

Select doesn't return expected result with Date type in Oracle DB

Table my_table is:
CREATE TABLE MY_TABLE(
ID NUMBER NOT NULL,
MY_DATE DATE NOT NULL);
Typing the following query:
select sysdate from dual
the result is:
10-MAG-12 21:22:32
note that mag = may.
Now, if I type this query:
select *
from my_table
where my_date <= sysdate
the result is:
9918 10-MAG-12 20:00:00
9915 10-MAG-12 21:00:00
9952 10-MAG-12 22:00:00
9951 10-MAG-12 23:00:00
Note that in my_table I have only these 4 records. Why I see all the records and not the first and second record only? Thanks.
I use Oracle SQL Developer.
Edit: please note that when I insert a record with PL/SQL I type something like:
nCount NUMBER;
myDate DATE;
stringDate VARCHAR2(255);
BEGIN
nCount := 0;
stringDate := substr(to_char(trunc(sysdate)),0,9);
myDate := to_date(stringDate || ' 20:00:00','dd-mm-yyyy hh24:mi:ss');
for t in (a cursor) loop
insert into MY_TABLE(ID,MY_DATE)
values (9918,myDate+(nCount/1440));
nCount := nCount + 60;
end loop;
END;
I suspect that the data being stored in your table does not have a year of 2012. It probably has a year of 0012 (two thousand years ago).
What do you see when you run the query
SELECT id, to_char( my_date, 'dd-mm-yyyy hh24:mi:ss' )
FROM my_table
I expect that the year will be 0012 rather than 2012. The reason for that is that the code you're using to insert the data is incorrectly converting a date to a string without using an explicit format mask then converts the string back to a date using an explicit format mask that happens not to match the session's NLS_DATE_FORMAT. In general, if you ever find yourself converting a date to a string and back to a date, you're probably doing something wrong. If you change your code to simply do date manipulation, it will be more efficient, more robust, and less error-prone.
DECLARE
nCount NUMBER;
myDate DATE;
BEGIN
nCount := 0;
myDate := trunc(sysdate) + interval '20' hour;
for t in (a cursor) loop
insert into MY_TABLE(ID,MY_DATE)
values (9918,myDate+(nCount/1440));
nCount := nCount + 60;
end loop;
END;
Walking through why the original code goes wrong
stringDate := substr(to_char(trunc(sysdate)),0,9);
This takes SYSDATE and truncates it to midnight on the current day. So far, so good. Then, it calls TO_CHAR without an explicit format mask. This causes Oracle to use the session's NLS_DATE_FORMAT, meaning that different users with different settings will get different results. If your session's NLS_DATE_FORMAT happens to be 'dd-mon-rr hh24:mi:ss', which I'm guessing based on the query results you posted, that will mean that the string has a 2-digit year. Your SUBSTR appears to assume that the output has just a two-digit year (if you have a different NLS_DATE_FORMAT, your SUBSTR will generate different bugs such as potentially cutting off the 12 from a year of 2012 leaving a year of just 20).
myDate := to_date(stringDate || ' 20:00:00','dd-mm-yyyy hh24:mi:ss');
Assuming stringDate is something like 10-MAG-12, this next line generates a string 10-MAG-12 20:00:00 and then tries to convert it to a date using the format mask dd-mm-yyyy hh24:mi:ss. This assumes that the string has a 4-digit year so when it only finds 2-digits, it assumes that you meant the year 12, not the year 2012.

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..