Day Difference is not working properly due to timezone conversion - postgresql

I was working on a problem where we are creating a view. We have a task & the task has an estimated time put up by the creator, assigned_at, due_date. We need to distribute the estimated time b/w assigned_at to due_date. What I was doing is getting the days using the extract function and dividing the estimated time with days count. By default, we store timestamps as UTC. The days count not coming correctly.
Eg: A task has,
Due Date: 2022-03-24T18:30:00+00:00,
Assigned Date: 2022-03-23T20:09:57.028525+00:00,
Estimated Time: 10 //time to complete that task,
Per Day Estimated Time: 10(Estimated Time)/2 (No Of Days)=5
CREATE OR REPLACE FUNCTION public.get_task_estimated_time(start_date timestamp with time zone, end_date timestamp with time zone, total_estimate numeric)
RETURNS numeric
LANGUAGE plpgsql
AS $function$
declare
date_duration numeric;
begin
date_duration = extract(days from age(end_date, start_date));
-- If due date is less than assigned date
if (date_duration < 0) then
return 0;
end if;
case
-- For the same day
when date_duration = 0 then
date_duration = 1;
else
date_duration = date_duration + 1;
end case;
return total_estimate / date_duration;
end;
$function$
Here when we pass the UTC formatted date to the function named "get_task_estimated_time". We dont get the exact day difference (use the example that we given above)

Per the information in my comment:
CREATE OR REPLACE FUNCTION public.get_task_estimated_time(start_date timestamp with time zone, end_date timestamp with time zone, total_estimate numeric)
RETURNS numeric
LANGUAGE plpgsql
AS $function$
declare
date_duration numeric;
begin
date_duration = ceiling(extract(hours from age(end_date, start_date))/24.0);
RAISE NOTICE 'date duration = %', date_duration;
-- If due date is less than assigned date
if (date_duration < 0) then
return 0;
end if;
case
-- For the same day
when date_duration = 0 then
date_duration = 1;
else
date_duration = date_duration + 1;
end case;
return total_estimate / date_duration;
end;
$function$
select get_task_estimated_time('2022-03-23T20:09:57.028525+00:00', '2022-03-24T18:30:00+00:00', 10);
NOTICE: date duration = 1
get_task_estimated_time
-------------------------
5.0000000000000000
The primary change being:
ceiling(extract(hours from age(end_date, start_date))/24.0)
which converts the age into hours and then divides by 24.0 to get decimal division and then applies ceiling to move the value up to the next highest integer.
Not sure it is necessary to return full numeric. integer would work if you are only interested in full days or a constrained numeric, say numeric(5,2).

Related

PostgreSQL - Reference a variable value in select statement interval

I'm trying to calculate a value in a variable then use that value to drive a interval statement further on.
Running on PG 9.6.
We can get to PG 13 if there is something added since then we can use.
Example:
CREATE OR REPLACE FUNCTION public.demofunc(
int,
int,
date)
returns date
LANGUAGE plpgsql AS $$
DECLARE
diffvalue text;
returndate date;
BEGIN
diffvalue:= ($2 - $1);
returndate := (SELECT $3 - INTERVAL 'diffvalue days');
return returndate;
END$$;
This is a simplified version of what I want to achieve, the interval of the numbers of days to remove is based on a calculation that's done in the function. So its not a simple A - B but the end result is
I can't seem to get the function to resolve the "diffvalue" before running the select statement. I've tried using int and text and varchar and concat the string for anything.
Sorry for anything obvious i might be missing, only started this today.
If your diffvalue is in reality an integer, you can use the make_interval function to create an interval based on that number:
returndate := $3 + make_interval(days => diffvalue);
If diffvalue is a decimal that can represent fractional days, make_interval can't be used.
In that case you can multiply an interval of 1 day with that value:
returndate := $3 + interval '1 day' * diffvalue;

Measure the time it takes to execute a PostgreSQL query

Based on Measure the time it takes to execute a t-sql query, how would one time several trials of a query in PostgreSQL?
A general outline would be
-- set up number of trials (say 1000)
SELECT CURRENT_DATE ; -- save start time
BEGIN
LOOP
-- execute query to be tested
END LOOP;
END;
SELECT CURRENT_DATE ; -- save end time
I.E. I want a PostgreSQL equivalent of the following TSQL code, taken from an answer by HumbleWebDev from the linked TSQL question: see [reference for code]
declare #tTOTAL int = 0
declare #i integer = 0
declare #itrs integer = 100
while #i < #itrs
begin
declare #t0 datetime = GETDATE()
--your query here
declare #t1 datetime = GETDATE()
set #tTotal = #tTotal + DATEDIFF(MICROSECOND,#t0,#t1)
set #i = #i + 1
end
select #tTotal/#itrs
-- your query here: Standard SQL queries such as Select * from table1 inner -- join table2, or executing stored procedure, etc.
Coming from an MSSQL background myself and now more often working in Postgres I feel your pain =)
The "trouble" with Postgres is that it supports only 'basic' SQL commands (SELECT, INSERT, UPDATE, CREATE, ALTER, etc...) but the moment you want to add logic (IF THEN, WHILE, variables, etc.) you need to switch to pl/pgsql which you can only use inside functions (AFAIK). From a TSQL POV there are quite some limitations and in fact, some things suddenly don't work anymore (or need to be done differently.. e.g. SELECT * INTO TEMPORARY TABLE tempTable FROM someTable will not work but CREATE TABLE tempTable AS SELECT * FROM someTable will)
Something I learned the hard way too is that CURRENT_TIMESTAMP (or Now()) will return the same value within a transaction. And since everything inside a function runs inside a transaction this means you have to use clock_timstamp()
Anyway, to answer your question, I think this should get you going:
CREATE OR REPLACE FUNCTION fn_test ( nbrOfIterations int)
RETURNS TABLE (iterations int, totalTime interval, secondsPerIteration int)
AS $$
DECLARE
i int;
startTime TIMESTAMP;
endTime TIMESTAMP;
dummy text;
BEGIN
i := 1;
startTime := clock_timestamp();
WHILE ( i <= nbrOfIterations) LOOP
-- your query here
-- (note: make sure to not return anything or you'll get an error)
-- example:
SELECT pg_sleep INTO dummy FROM pg_sleep(1);
i := i + 1;
END LOOP;
endTime := clock_timestamp();
iterations := nbrOfIterations;
totalTime := (endTime - startTime);
secondsPerIteration := (EXTRACT(EPOCH FROM endTime) - EXTRACT(EPOCH FROM startTime)) / iterations;
RETURN NEXT;
END;
$$ language plpgsql;
SELECT * FROM fn_test(5);
While the accepted answer is correct, this tweaking of it worked better for me. Again, I want to emphasize this extra answer below is based on the above answer, and it would not be possible without it. It just works better in my own situation to use the tweak I made below.
The answer below is indeed almost entirely based on the accepted answer. However, I changed how the return is used and also seconds to milliseconds:
----------------------------------------------------------------------------------------------------
-- fn__myFunction_Q.sql
----------------------------------------------------------------------------------------------------
--------------------------------------------------------------------------------------------
-- DROP FUNCTION mySchema.fn__myFunction
--------------------------------------------------------------------------------------------
CREATE OR REPLACE FUNCTION mySchema.fn__myFunction ( nbrOfIterations int)
RETURNS TABLE (iterations int, totalTime interval, millisecondsPerIteration int) -- interval --
AS $$
declare
i int;
startTime TIMESTAMP;
endTime TIMESTAMP;
-- dummy text;
iterations int;
millisecondsPerIteration int;
totalTime interval;
BEGIN
i := 1;
startTime := clock_timestamp();
WHILE ( i <= nbrOfIterations) LOOP
PERFORM /* Put your query here, replacing SELECT with PERFORM */
--------------------------------------------------------------------------------------------
--SELECT
-- YOUR QUERY HERE
-- ...
--------------------------------------------------------------------------------------------
i := i + 1; -- very important to increment loop counter, else one gets an infinite loop!!!
END LOOP;
endTime := clock_timestamp();
iterations := nbrOfIterations;
totalTime := (endTime - startTime);
millisecondsPerIteration := 1000 * (EXTRACT(EPOCH FROM endTime) - EXTRACT(EPOCH FROM startTime)) / iterations;
RETURN QUERY select iterations, totalTime, millisecondsPerIteration;
-- RETURNS TABLE (iterations int, totalTime interval, secondsPerIteration int) -- interval --
-- RETURN NEXT;
END;
$$ language plpgsql;
--------------------------------------------------------------------------------------------
To call this function, just use:
SELECT * from mySchema.fn__myFunction(1000) as ourTableResult;

check leap year or not in PostgreSQL

How to check the given year is leap year or not in PostgreSQL.?
I tried below query.But it is showing error message.
select extract(year from hiredate)% 4 = 0 from emp
You must cast the extracted year to an integer.
select extract(year from hiredate)::integer % 4 = 0 from emp
However, that is the incorrect formula for a leap year. It is Every four years except every 100 except every 400. 1900 was not a leap year, but 2000 was.
create function is_leap_year(timestamp)
returns boolean as $$
declare y integer;
begin
y := extract(year from $1);
return (y % 4 = 0) and (y % 100 <> 0 or y % 400 = 0);
end
$$ language plpgsql;
Alternatively, you can check to see what day comes before March 1st in that year. This is safer as it will use Postgresql's internal logic.
create function is_leap_year(timestamp)
returns boolean as $$
begin
return date_part(
'day',
make_date(date_part('year', $1)::int, 3, 1) - '1 day'::interval
) = 29;
end
$$ language plpgsql;

how to get the next date for a day of month

thanks for reading, this is the situation
I have a current_date and a day of month, so i need to know what will be the next date for this day of month, having in mind that some month don't have 30 and 31.
Example:
current_date = '2018-09-24'
day_of_week = 31
Expected result: '2018-12-31'
Currently i have this:
create or replace function next_diff(vals int[], current_val int) returns int as
$$
declare v int;
declare o int := vals[1];
begin
foreach v in array vals loop
if current_val >= o and current_val < v then
return v - current_val;
end if;
o := v;
end loop;
return vals[1] - current_val;
end;
$$ language plpgsql;
and this:
create or replace function next_day_of_month(days_of_month int[], curr_date date) returns date as
$$
declare cur_dom int := extract(day from curr_date);
declare next_diff int := next_diff(days_of_month, cur_dom);
begin
if next_diff < 0 then
curr_date := curr_date + '1 months'::interval;
end if;
curr_date := curr_date + (next_diff || 'days')::interval;
return curr_date;
end;
$$ language plpgsql;
but for this calling:
select next_day_of_month(array[31], '2018-09-24');
i am getting:
"2018-10-01"
Extra example
If i have this value
current_date = '2018-02-01'
day_of_week = 31
i will need the next month with 31th but i can't get '2018-02-31' because February don't have 31th then i should get '2018-02-31' because March have 31th.
Conclusion
if the month don't have the specified day must ignore the month and jump to the next.
thanks for all
Final method
Using Carlos Gomez answer, i create this PostgreSQL function and work perfectly:
create or replace function next_day_date(curr_date date, day_of_month int) returns date as
$$
declare next_day date;
begin
SELECT next_day_date into next_day FROM (
SELECT make_date_nullable(EXTRACT(year from n.month)::int, EXTRACT(month from n.month)::int, day_of_month) AS next_day_date
FROM (
SELECT generate_series(curr_date, curr_date + '3 months'::interval, '1 month'::interval) as month
) n
) results
WHERE results.next_day_date IS NOT NULL and results.next_day_date > curr_date LIMIT 1;
return next_day;
end;
$$ language plpgsql;
just add other filter in where clause and results.next_day_date > curr_date to prevent get the same or previous values for specified date
Thanks everyone for helping
Thenks Carlos you are the best
Gracias carlos eres el mejor :)
Your examples don't really match up but I think I know what you are trying to solve for (your first example result should be '2018-10-31' since October has 31 days and your second example result should be '2018-03-31'). It seems that given a date and a day of month you want to find the next month that has that day of month. To do this, I would do the following:
This function just wraps make_date to let it return null since it throws an exception if a date given to it is out of bounds (like February 30).
CREATE OR REPLACE FUNCTION make_date_nullable(year int, month int, day int)
RETURNS date as $$
BEGIN
RETURN make_date(year, month, day);
EXCEPTION WHEN others THEN RETURN null;
END;
$$ language plpgsql;
This SELECT first generates the next three months starting with the current one, then makes date out of them with your provided day_of_month and finally gets the first one that isn't null (exists according to postgresql.
SELECT next_day_date FROM (
SELECT make_date_nullable(EXTRACT(year from n.month)::int, EXTRACT(month from n.month)::int, day_of_month) AS next_day_date
FROM (
SELECT generate_series(current_date, current_date + '3 months'::interval, '1 month'::interval) as month
) n
) results
WHERE results.next_day_date IS NOT NULL LIMIT 1;
Hope this helps!

Casted timeStamp does not give correct result in PL/pgsql

I'm new to pl/pgsql and I'm trying to execute following function and it gives 0 records as result.
CREATE OR REPLACE FUNCTION public.detectselect (i_channelID integer,te_startTime text,te_endTime text)
RETURNS SETOF detect_inst AS $BODY$
declare
r detect_inst%rowtype;
tstz_endTime timestamp without time zone;
tstz_startTime timestamp without time zone;
BEGIN
tstz_endTime = to_timestamp(te_endTime,'DD/MM/YYYY hh24:mi:ss')::timestamp without time zone;
tstz_startTime = to_timestamp(te_startTime,'DD/MM/YYYY hh24:mi:ss')::timestamp without time zone;
for r in SELECT * FROM detect_inst d WHERE d."ChannelID" = i_channelID AND d."EndTime" >= tstz_startTime AND d."EndTime" < tstz_endTime loop
return next r;
end loop;
RETURN;
END;
$BODY$ LANGUAGE plpgsql;
call as
select detectselect(1,'2016-01-21 0:0:0','2016-01-23 0:0:0');
but it give correct results when i give static time stamp values in this way
CREATE OR REPLACE FUNCTION public.detectselect (i_channelID integer,te_startTime text,te_endTime text)
RETURNS SETOF detect_inst AS $BODY$
declare
r detect_inst%rowtype;
tstz_endTime timestamp without time zone;
tstz_startTime timestamp without time zone;
BEGIN
tstz_endTime = to_timestamp(te_endTime,'DD/MM/YYYY hh24:mi:ss')::timestamp without time zone;
tstz_startTime = to_timestamp(te_startTime,'DD/MM/YYYY hh24:mi:ss')::timestamp without time zone;
for r in SELECT * FROM detect_inst d WHERE d."ChannelID" = i_channelID AND d."EndTime" >= '2016-01-21 0:0:0' AND d."EndTime" < '2016-01-23 0:0:0' loop
return next r;
end loop;
RETURN;
END;
$BODY$ LANGUAGE plpgsql;
Your function is overly complex. You don't need PL/pgSQL and you don't need a (slow) cursor to return the result.
It can be simplified to a plain SQL function which also will be a lot faster:
CREATE OR REPLACE FUNCTION public.detectselect (i_channelID integer, te_startTime timestamp, te_endTime timestamp)
RETURNS SETOF detect_inst AS
$BODY$
SELECT *
FROM detect_inst d
WHERE d."ChannelID" = i_channelID
AND d."EndTime" >= te_starttime
AND d."EndTime" < te_endtime
$BODY$
LANGUAGE sql;
You then call it:
select *
from detectselect(1, timestamp '2016-01-21 00:00:00', timestamp '2016-01-23 00:00:00' );
You should also avoid those dreaded quoted identifiers. They are much more trouble then they are worth it