PostgreSQL date difference - sql

I have a PostgreSQL function which calculates date difference:
CREATE OR REPLACE FUNCTION testDateDiff () RETURNS int AS $BODY$
DECLARE startDate TIMESTAMP;
DECLARE endDate TIMESTAMP;
DECLARE diffDatePart int ;
BEGIN
Select evt_start_date From events Where evt_id = 5 INTO startDate ;
Select evt_start_date From events Where evt_id = 6 INTO endDate ;
SELECT EXTRACT(day FROM TIMESTAMP startDate - endDate) INTO diffDatePart;
RETURN diffDatePart;
END;
$BODY$
LANGUAGE plpgsql
COST 100
If dates are subtracted directly then difference is calculated. But in my case dates are present in variables as startDate and endDate, which causes the problem.
How can I subtract dates contained in variables?

Debug
What your function is doing could be done much simpler. The actual cause for the syntax error is here:
SELECT EXTRACT(day FROM TIMESTAMP startDate - endDate) INTO diffDatePart;
It looks like you are trying to cast startDate to timestamp, which is nonsense to begin with, because your parameter startDate is declared as timestamp already.
It also does not work. I quote the manual here:
To avoid syntactic ambiguity, the type 'string' syntax can only be
used to specify the type of a simple literal constant.
It would work like this:
SELECT EXTRACT(day FROM startDate - endDate)::int INTO diffDatePart;
But that still wouldn't make a lot of sense. You are talking about "dates", but still define your parameters as timestamp. You could sanitize what you have like this:
CREATE OR REPLACE FUNCTION f_date_diff()
RETURNS int AS
$BODY$
DECLARE
start_date date;
end_date date;
date_diff int;
BEGIN
SELECT evt_start_date FROM events WHERE evt_id = 5 INTO start_date;
SELECT evt_start_date FROM events WHERE evt_id = 6 INTO end_date;
date_diff := (endDate - startDate);
RETURN date_diff;
END
$BODY$ LANGUAGE plpgsql;
DECLARE only needed once.
date columns declared as proper type date.
Don't use mixed case identifiers, unless you know exactly what you are doing.
Subtract the start from the end to get a positive number or apply the absolute value operator #.
Since subtracting dates (as opposed to subtracting timestamps, which yields an interval) already yields integer, simplify to:
SELECT (startDate - endDate) INTO diffDatePart;
Or even simpler as plpgsql assignment:
diffDatePart := (startDate - endDate);
Simple query
You can solve the simple task with a simple query - using a subquery:
SELECT (SELECT evt_start_date
FROM events
WHERE evt_id = 6)
- evt_start_date AS date_diff
FROM events
WHERE evt_id = 5;
Or you could CROSS JOIN the base table to itself (1 row from each instance, so that's ok):
SELECT e.evt_start_date - s.evt_start_date AS date_diff
FROM events e
,events s
WHERE e.evt_id = 6
AND s.evt_id = 5;
SQL function
If you insist on a function for the purpose, use a simple sql function:
CREATE OR REPLACE FUNCTION f_date_diff(_start_id int, _end_id int)
RETURNS int LANGUAGE sql AS
$func$
SELECT e.evt_start_date - s.evt_start_date
FROM events s, events e
WHERE s.evt_id = $1
AND e.evt_id = $2
$func$;
Call:
SELECT f_date_diff(5, 6);
PL/pgSQL function
If you insist on plpgsql ...
CREATE OR REPLACE FUNCTION f_date_diff(_start_id int, _end_id int)
RETURNS int LANGUAGE plpgsql AS
$func$
BEGIN
RETURN (SELECT evt_start_date
- (SELECT evt_start_date FROM events WHERE evt_id = _start_id)
FROM events WHERE evt_id = _end_id);
END
$func$;
Same call.

I would write the query like this:
create function testDateDiff()
returns integer as $$
declare
startDate timestamp;
endDate timestamp;
begin
startDate := (select evt_start_date From events Where evt_id = 5);
endDate := (select evt_start_date From events Where evt_id = 6);
return (select extract(day from startDate - endDate));
end;
$$ language 'plpgsql';
The difference between using := and into in the context above is that using := your query must return a single value. If you use into your query can return a single row (i.e. more than one column).
For a full explanation of using select with into and plpgsql you should read http://www.postgresql.org/docs/9.1/static/plpgsql-statements.html. Specifically, section 39.5.3 of the PostgreSQL documentation.

Do you really need a function for this?
This query would work as well:
SELECT (SELECT evt_start_date::date FROM events WHERE evt_id = 5)
- evt_start_date::date
FROM events WHERE evt_id = 6;

Related

how to create a snowflake udf that makes use of time_slice to return a table

Is there a way to create a snowflake udf or stored procedure that returns a table grouped by time_slice where the week starts on a day other than Monday? The SQL is simple, however to use time_slice by week with a start day other than Monday, one must first call ALTER SESSION SET WEEK_START. I can put the ALTER SESSION call within a stored procedure, and put the time_slice sql within a user defined function to return a table, but I haven't been able to make all calls within a SINGLE function or stored procedure.
Here's sample sql.
ALTER SESSION SET WEEK_START = 3;
select
time_slice(s_startdateutc, 1, 'WEEK', 'START') as BLOCKSTART,
time_slice(s_startdateutc, 1, 'WEEK', 'END') as BLOCKEND,
count(*) as ACTIONCOUNT
from mytable
where s_startdateutc between $REPORTSTARTDATE and $REPORTENDDATE
group by BLOCKSTART, BLOCKEND;
The following code works, but if you uncomment out the ALTER SESSION line, it fails.
create or replace function nr_sessacts_application_overview(reportStart date, reportEnd date)
returns table (BLOCKSTART date, BLOCKEND date, ACTIONCOUNT number)
language sql
as
$$
--ALTER SESSION SET WEEK_START = 3;
select
to_date(time_slice(s_startdateutc, 1, 'WEEK', 'START')) as BLOCKSTART,
to_date(time_slice(s_startdateutc, 1, 'WEEK', 'END')) as BLOCKEND,
count(*) as ACTIONCOUNT
from mytable
where s_startdateutc between reportStart and reportEnd
group by BLOCKSTART, BLOCKEND
order by BLOCKSTART
$$
;
What if you looked for the nearest Tuesday in the block?
select next_day(s_startdateutc::date-7, 'Tue') as blockstart,
next_day(s_startdateutc::date, 'Tue') as blockend,
count(*) as actioncount
from mytable
where s_startdateutc between $REPORTSTARTDATE and $REPORTENDDATE
group by blockstart, blockend;
Solved. The key is to include EXECUTE AS CALLER.
create or replace function nr_sessacts_application_overview(reportStart date, reportEnd date)
returns table (BLOCKSTART date, BLOCKEND date, ACTIONCOUNT number)
language sql
EXECUTE AS CALLER
as
$$
ALTER SESSION SET WEEK_START = 3;
select
to_date(time_slice(s_startdateutc, 1, 'WEEK', 'START')) as BLOCKSTART,
to_date(time_slice(s_startdateutc, 1, 'WEEK', 'END')) as BLOCKEND,
count(*) as ACTIONCOUNT
from mytable
where s_startdateutc between reportStart and reportEnd
group by BLOCKSTART, BLOCKEND
order by BLOCKSTART
$$
;

How do declare date variables in PL/SQL?

I am new to PL/SQL and don't understand how to declare date variables. How would I write this T-SQL script in PL/SQL?
declare #date1 as date
declare #date2 as date
set #date1 = '2022-06-01'
set #date2 = '2022-06-30'
select *
from example_table
where example_date between #date1 and #date2
Declaring variables is easy.
Problem comes when you want to do something with the select statement as PL/SQL requires you to select INTO something (a local variable, set of local variables, ref cursor, collection, ...). You didn't say what you'd want to do afterwards, so the following example
selects number of rows that satisfy condition
loops through the table and does nothing (null;) for each loop iteration.
You'd do something else, I presume.
declare
date1 date := date '2022-06-01';
date2 date := date '2022-06-30';
l_cnt number;
begin
select count(*)
into l_cnt
from example_table
where example_date between date1 and date2;
for cur_r in (select *
from example_table
where example_date between date1 and date2
)
loop
null;
end loop;
end;
/

How to turn this script into a UDF in BigQuery

This should be easy to do but I'm struggling.
I have the below script that calculates a number of business dates before or after a date. I need to change to UDF as I'll be using it in multiple views. It is for Bigquery:
DECLARE Date DATE;
DECLARE DAYS_EXTEND INT64;
DECLARE COUNTER INT64;
SET Date = '2021-12-23';
SET DAYS_EXTEND = 3;
SET COUNTER = DAYS_EXTEND;
BEGIN
WHILE COUNTER > 0 Do
SET COUNTER = COUNTER -1;
SET Date = Date +1;
IF Extract( DAYOFWEEK from Date) in (1,7) or Date in ('2021-01-01','2021-04-02','2021-04-05','2021-05-03','2021-05-31','2021-08-30','2021-12-27','2021-12-28','2022-01-03','2022-04-15','2022-04-18','2022-05-02','2022-06-02','2022-06-03','2022-08-29','2022-12-26','2022-12-27')
Then
BEGIN
SET DAYS_EXTEND = DAYS_EXTEND +1;
SET COUNTER = COUNTER +1;
END;
END IF;
END WHILE;
WHILE COUNTER <0 DO
SET COUNTER = COUNTER +1;
SET Date = Date -1;
IF Extract( DAYOFWEEK from Date) in (1,7) or Date in ('2021-01-01','2021-04-02','2021-04-05','2021-05-03','2021-05-31','2021-08-30','2021-12-27','2021-12-28','2022-01-03','2022-04-15','2022-04-18','2022-05-02','2022-06-02','2022-06-03','2022-08-29','2022-12-26','2022-12-27')
Then
BEGIN
SET DAYS_EXTEND = DAYS_EXTEND - 1;
SET COUNTER = COUNTER - 1;
END;
END IF;
END WHILE;
END;
I'm just not sure how to turn it into a Select statement or if it is possible to do UDF without a Select statement with the loops remaining.
I'd appreciate any help.
For what I understand from your question, you are trying to create a function/procedure that make uses of selects and loops to identify business days. Personally I would avoid complex scripting (usually as last resort). I think It can be achieve by just using selects.
Here is a sample code, that can assist you identifying business days:
DECLARE MyDate DATE;
DECLARE DAYS_RANGE INT64;
SET MyDate = '2021-11-22';
SET DAYS_RANGE = 10;
/*if it fits you, create a table for special dates and holidays*/
create temp table offdays (
nonworkingdays date
);
insert into offdays values('2021-11-23');
insert into offdays values('2021-11-19');
with working_dates as(
select dafter,
case when EXTRACT(DAYOFWEEK FROM dafter) not in (1,7) then 1 else 0 end as isweekdays_after,
case when dafter in (select nonworkingdays from offdays) then 1 else 0 end as isoffdays_after,
dbefore,
case when EXTRACT(DAYOFWEEK FROM dbefore) not in (1,7) then 1 else 0 end as isweekdays_before,
case when dbefore in (select nonworkingdays from offdays) then 1 else 0 end as isoffdays_before
from (
select date_add(MyDate, INTERVAL days_to_count DAY) as dafter,
date_add(MyDate, INTERVAL -days_to_count DAY) as dbefore
from (SELECT days_to_count FROM UNNEST(GENERATE_ARRAY(1, DAYS_RANGE)) AS days_to_count)
)
)
select * from working_dates
If you run it, you will get a main table working_dates with rows equals to the range given and columns that will help you identify weekdays and offdays.
So, You can use this code to create a function or procedure where you can pass parameters and calculate if you either want days after or before and return the days or the count of days filtered by the columns weekdays and offdays.
Take this as a sample function derived from above script:
CREATE TEMP FUNCTION GetBusinessDaysAfter(fnDate Date, days_range INT64)
RETURNS INT64
AS ((
select count(d.dafter) from(
select dafter,
case when EXTRACT(DAYOFWEEK FROM dafter) not in (1,7) then 1 else 0 end as isweekdays_after,
case when dafter in ('2021-11-23') then 1 else 0 end as isoffdays_after,
from (
select date_add(fnDate, INTERVAL days_to_count DAY) as dafter
from (SELECT days_to_count FROM UNNEST(GENERATE_ARRAY(1, days_range)) AS days_to_count)
)) as d
where d.isweekdays_after=1 and d.isoffdays_after=0
));
select GetBusinessDaysAfter('2021-11-22',3)
I can use it to retrieve how many business days will pop up in the next 3 days. Turns out to be just 2 (for the sake of the sample, I put a fixed value in offdays, you can replace it making a reference to your holidays table in your dataset).
For more information about scripting, functions and procedures, here are some useful links:
Data definition language
Working with arrays
Working with dates functions

Get first and last day of month into variables - Oracle

I would like to declare some variables that contain the first and last date of the current month in Oracle. I know how to get these values, but evidently not store or use them; I am more of a T-SQL guy.
In T-SQL, I could write:
DECLARE #startDate DATE = GETDATE();
DECLARE #endDate DATE = EOMONTH(GETDATE());
SELECT *
FROM SomeTable
WHERE SomeDate BETWEEN #startDate AND #endDate;
I cannot, for the life of me, work out how to do this in Oracle. I have tried several variations, like:
DECLARE END_DT DATE := TRUNC(LAST_DAY(SYSDATE))
END_DT DATE := TRUNC(LAST_DAY(SYSDATE))
DECLARE END_DT DATE;
SELECT TRUNC(LAST_DAY(SYSDATE)) INTO END_DT FROM DUAL;
I am mostly using 11g, but I would like to be able to use the same script on a 9i/10g server also.
You can use such a query to detect first and last days of the current month :
SELECT TRUNC(LAST_DAY(ADD_MONTHS(SYSDATE,-1)))+1,
TRUNC(LAST_DAY(SYSDATE))
INTO :startDate,:endDate
FROM DUAL;
with the contribution of ADD_MONTHS() function
Update : Alternatively use this PL/SQL code block :
DECLARE
startDate date;
endDate date;
BEGIN
startDate := TRUNC(LAST_DAY(ADD_MONTHS(SYSDATE,-1)))+1;
endDate := TRUNC(LAST_DAY(SYSDATE));
DBMS_OUTPUT.PUT_LINE ('startDate : '||startDate);
DBMS_OUTPUT.PUT_LINE ('endDate : '||endDate);
END;
/
Demo
You can use the LAST_DAY and TRUNC combination as following:
DECLARE
START_DT DATE := TRUNC(SYSDATE, 'MM');
END_DT DATE := TRUNC(LAST_DAY(SYSDATE));
BEGIN
--DBMS_OUTPUT.PUT_LINE(START_DT || ' ' || END_DT);
SELECT
*
INTO <...>
FROM
SOMETABLE
WHERE
SOMEDATE BETWEEN START_DT AND END_DT;
END;
/
Cheers!!

time interval in function not working

I have a function:
CREATE OR REPLACE FUNCTION a(b integer)
RETURNS integer AS
$BODY$
declare
c integer;
timevalue text;
begin
timevalue = b::text || ' days'; -- build string like '7 days'
select sum(value)
into c
from tablx
where createdate between current_date - interval timevalue and current_date;
return c;
end;
$BODY$
LANGUAGE plpgsql VOLATILE
function is simple... give summary of records which fits the date criteria.
for some reason it does not accept current_date - interval timevalue
what can I do?
Unfortunately you can't specify a "dynamic" interval value. But as you always use the same unit, you can use:
select sum(value)
into c
from tablx
where createdate between current_date - interval '1' day * b and current_date;
You can make that simpler, because you can subtract b directly from current_date
select sum(value)
into c
from tablx
where createdate between current_date - b and current_date;
In an expression date - integer the integer value is the number of days.
For more details see the manual: http://www.postgresql.org/docs/current/static/functions-datetime.html