how to divide actual months to 5 days periods using plpgsql function - postgresql

I need to get first day of each five days of month using posgrees. For example if no is 2.03 then I would get 1.03. If today would be 19.03 then I would get 15.03. I tried to use following if statements but for today;s day I got null and I'm not sure why. Also there surely is some less complicated way to do this operation. Any ideas?
create or replace function getFirstDayOfFive()
returns timestamp with time zone as $$
declare
firstDay timestamp;
begin
if (now()::date>date_trunc('month', now()::date) and now()::date < date_trunc('month', now()::date) + interval '5 day') then
return firstDay = date_trunc('month', now()::date);
elsif (now()::date>date_trunc('month', now()::date) + interval '5 day' and now()::date < date_trunc('month', now()::date) + interval '10 day') then
return firstDay = (date_trunc('month', now()::date) + interval '5 day')::date;
elsif (now()::date>date_trunc('month', now()::date) + interval '10 day' and now()::date < date_trunc('month', now()::date) + interval '15 day') then
return firstDay = (date_trunc('month', now()::date) + interval '10 day')::date;
elsif (now()::date>date_trunc('month', now()::date) + interval '15 day' and now()::date < date_trunc('month', now()::date) + interval '20 day') then
return firstDay = (date_trunc('month', now()::date) + interval '15 day')::date;
elsif (now()::date>date_trunc('month', now()::date) + interval '20 day' and now()::date < date_trunc('month', now()::date) + interval '25 day') then
return firstDay = (date_trunc('month', now()::date) + interval '20 day')::date;
end if;
end;
$$
language plpgsql;

demo:db<>fiddle
SELECT
make_date( -- 4
date_part('year', my_date)::int, -- 3
date_part('month', my_date)::int,
greatest( -- 2
floor(date_part('day', my_date) / 5) * 5, -- 1
1
)::int
)
Get day of the current date using date_part(). After it round it to full multiple 5 with floor(day / 5) * 5
The provided algorithm always gives the previous multiple of 5 (for 19 it gives 15, for 6 it gives 5, ...). However, for days 1 to 4 it gives 0. So this is an exception which needs to be handled. This is done here
Get year and month part of the current date
Create the expected date.
This query can be embedded into a function, of course:
create or replace function getFirstDayOfFive()
returns timestamp with time zone as $$
declare
firstDay timestamp;
begin
SELECT
make_date(
date_part('year', now())::int,
date_part('month', now())::int,
greatest(
floor(date_part('day', now()) / 5) * 5,
1
)::int
)
INTO firstDay;
RETURN firstDay;
end;
$$
language plpgsql;
Edit: From the comments: Same for Last Day:
You have to change:
floor() to ceil()
the greatest(..., 1) into least(..., last day of current month)
To get the last date of the current month, you have to find the first day using date_trunc('month', ...), add one month to get the first day of the next month and subtract one day from it:
demo:db<>fiddle
SELECT
least(
ceil(date_part('day', now()) / 5) * 5,
date_part('day', date_trunc('month', now()) + interval '1 month - 1 day')
)

Related

Recursive queries with two aggregate columns

I want to compute certain statistics montly in a postgres database
WITH RECURSIVE totals(start, t_end, null_count, not_null_count) AS (
VALUES (date_trunc('month', current_date + interval '1 month'),
date_trunc('month', current_date + interval '2 months'),
0::bigint)
UNION
SELECT start - interval '1 month' as start, start as t_end,
(SELECT count(*) filter (WHERE flag IS NULL) FROM tbl
WHERE created_at >= start
and created_at < t_end
and deleted_at < current_timestamp
) as null_count,
(SELECT count(*) filter (WHERE flag IS NOT NULL) FROM tbl
WHERE created_at >= start
and created_at < t_end
and deleted_at < current_timestamp
) as not_null_count
FROM totals
WHERE start > current_date - interval '1 year'
)
select * from totals
This would give me what I want, but would scan tbl twice.
Is there a way to to this scanning only once, like one would do in a plain query
SELECT count(*) filter (WHERE flag IS NULL) null_count,
count(*) filter (WHERE flag IS NOT NULL) not_null_count, FROM tbl
WHERE created_at >= start
AND created_at < t_end
AND deleted_at < current_timestamp
I know I could group by date_trunk('month', created_at) but doing that causes the query to sort the rows, and this is very costly in this case.
Yes. You can update your query to -
WITH RECURSIVE totals(start, t_end, null_count, not_null_count) AS (
VALUES (date_trunc('month', current_date + interval '1 month'),
date_trunc('month', current_date + interval '2 months'),
0::bigint)
UNION
SELECT start - interval '1 month' as start, start as t_end,
count(*) filter (WHERE flag IS NULL) as null_count,
count(*) filter (WHERE flag IS NOT NULL) as not_null_count
FROM tbl
JOIN totals ON created_at >= start
and created_at < t_end
WHERE start > current_date - interval '1 year'
AND deleted_at < current_timestamp
)
select * from totals

PostgreSQL Time Dimension (By Hours and Days) Error

I am am building a Time Dimension table in PostgreSQL with DATE_ID and DATE_DESC.
My T-SQL (works perfectly) script is:
set DATEFIRST 1
;WITH DATES AS (
SELECT CAST('2019-01-01 00:00:00.000' AS datetime) AS [DATE]
UNION ALL
SELECT DATEADD(HH,1,[DATE])
FROM DATES
WHERE DATEADD(HH,1,[DATE]) <= CAST('2019-12-31' AS datetime)
)
SELECT
DATE_ID, DATE_DESC
from
(
SELECT
CONVERT(int, CONVERT(char(8), DATE, 112)) AS DATE_ID,
DATE AS DATE_DESC
FROM
DATES)a
order by 1
OPTION (MAXRECURSION 0)
At the moment Im trying to convert this code to PostgreSQL readable one and it does not work..
Here is mine at the moment:
set EXTRACT(DOW FROM TIMESTAMP '2019-01-01 00:00:00.000')+1
;WITH DATES AS (
SELECT CAST('2019-01-01 00:00:00.000' AS timestamp) AS DATE
UNION ALL
SELECT CURRENT_DATE + INTERVAL '1 hour'
FROM DATES
WHERE CURRENT_DATE + INTERVAL '1 hour' <= CAST('2019-12-31' AS timestamp)
)
SELECT DATE_ID, DATE_DESC from
(SELECT cast(to_char((DATE)::TIMESTAMP,'yyyymmddhhmiss') as BIGInt) AS DATE_ID,
DATE AS DATE_DESC
FROM
DATES)a
order by 1
OPTION (MAXRECURSION 0)
I need all the hours (24h) between 2019-01-01 and 2019-12-31 . At the moment I think OPTION (MAXRECURSION 0) and set EXTRACT(DOW FROM TIMESTAMP '2019-01-01 00:00:00.000')+1 is not working properly.
Its a problem of Recursive CTE, In Postgresql, your desired query will be like below
WITH recursive DATES AS (
SELECT CAST('2019-01-01 00:00:00.000' AS timestamp) AS date_
UNION ALL
SELECT date_ + INTERVAL '1 hour'
FROM DATES
WHERE date_ + INTERVAL '1 hour' <= CAST('2019-12-31' AS timestamp)
)
SELECT DATE_ID, DATE_DESC from
(SELECT cast(to_char((date_)::TIMESTAMP,'yyyymmddhhmiss') as BIGInt) AS DATE_ID,
date_ AS DATE_DESC
FROM
DATES)a
order by 1
DEMO

return query from psql function based on function arguments

I have a function in postgresql which returns query based on the input supplied:
create or replace function func(last_month date, arg1 varchar default 'any')
returns table(id bigint, start_date date, end_date date) as $$
begin
if arg1 = 'multiple' then
SELECT DISTINCT id, (last_month - interval '1 year' + interval '1 month')::date as start_date,
last_month as end_date
FROM table
WHERE month BETWEEN (last_month - interval '1 year' + interval '1 month')::date
AND last_month
and month >= '2017-09-01'
AND activity >= 5;
else
SELECT DISTINCT id, (last_month - interval '1 year' + interval '1 month')::date as start_date,
last_month as end_date
FROM table
WHERE month BETWEEN (last_month - interval '1 year' + interval '1 month')::date
AND last_month
and month >= '2017-09-01'
end if;
end; $$ language sql;
However when I run it, it gives me the following error :
syntax error at or near "if"
I have checked returning only integer for testing purposes and it works and syntax appears to be fine. How can I return different query results from a function based on the given input.
I was able to resolve in he following way:
create or replace function func(last_month date, arg1 varchar default 'any')
returns table(id bigint, start_date date, end_date date) as $BODY$
begin
case arg1
when arg1 = 'multiple' then
return query
SELECT DISTINCT id, (last_month - interval '1 year' + interval '1 month')::date as start_date,
last_month as end_date
FROM table
WHERE month BETWEEN (last_month - interval '1 year' + interval '1 month')::date
AND last_month
and month >= '2017-09-01'
AND activity >= 5;
else
return query
SELECT DISTINCT id, (last_month - interval '1 year' + interval '1 month')::date as start_date,
last_month as end_date
FROM table
WHERE month BETWEEN (last_month - interval '1 year' + interval '1 month')::date
AND last_month
and month >= '2017-09-01';
end case;
end;
$BODY$ language plpgsql;

Convert to Postgres function

I am converting my SQL stored procedure to Postgres function but it is resulting error in one of the statement as
ERROR: syntax error at or near ","
LINE 7: CAST(time(0), '00:00' + (h.hour * interval '1Hour...
^
Below is my Postgres function and SQL Store procedure which i am converting.Please tell me where i am getting wrong.
CREATE OR REPLACE FUNCTION shiftwisedata_sp(INOut shift_id bigint,InOut userdate date,OUT shift_name character varying (50),OUT from_time character varying(50),OUT to_time character varying(50),OUT cal bigint)
RETURNS SETOF record AS
$BODY$
BEGIN
return query
SELECT userdate, s.shift_name,
CAST(time(0), '00:00' + (h.hour * interval '1Hour')) AS from_time,
CAST(time(0), '00:00' + ((h.hour + 1) * interval '1Hour')) AS to_time,
COALESCE(r.Readings, 0) AS readings
FROM shift_wise s
CROSS JOIN (VALUES(0), (1), (2), (3), (4), (5), (6), (7), (8), (9),
(10), (11), (12), (13), (14), (15), (16), (17), (18), (19),
(20), (21), (22), (23)) AS h(hour)
OUTER APPLY (SELECT SUM(r.param_value) AS Readings
FROM table_1 r
WHERE r.timestamp_col >= CAST(userdate as timestamp without time zone) + (h.hour * interval '1Hour')
AND r.timestamp_col < CAST(userdate as timestamp without time zone) + ((h.hour + 1) * interval '1Hour')) AS r
WHERE s.shift_id = shift_id
AND (s.to_time > s.from_time AND
h.hour >= date_part(HOUR, s.from_time) AND
h.hour < date_part(HOUR, s.to_time)
OR
s.to_time < s.from_time AND
(h.hour >= date_part(HOUR, s.from_time) OR
h.hour < date_part(HOUR, s.to_time))
)
ORDER BY s.to_time;
END;
$BODY$
LANGUAGE plpgsql VOLATILE
CREATE PROCEDURE Shiftdata #date date, #shiftid int AS
SELECT #date, s.Shift_Name,
convert(time(0), dateadd(HOUR, h.hour, '00:00')) AS from_time,
convert(time(0), dateadd(HOUR, h.hour + 1, '00:00')) AS to_time,
coalesce(r.Readings, 0) AS readings
FROM Shift_Wise s
CROSS JOIN (VALUES(0), (1), (2), (3), (4), (5), (6), (7), (8), (9),
(10), (11), (12), (13), (14), (15), (16), (17), (18), (19),
(20), (21), (22), (23)) AS h(hour)
OUTER APPLY (SELECT SUM(r.Reading_Col) AS Readings
FROM Reading r
WHERE r.Timestamp_Col >= dateadd(HOUR, h.hour, convert(datetime, #date))
AND r.Timestamp_Col < dateadd(HOUR, h.hour + 1, convert(datetime, #date))) AS r
WHERE s.Shift_ID = #shiftid
AND (s.to_time > s.from_time AND
h.hour >= datepart(HOUR, s.from_time) AND
h.hour < datepart(HOUR, s.to_time)
OR
s.to_time < s.from_time AND
(h.hour >= datepart(HOUR, s.from_time) OR
h.hour < datepart(HOUR, s.to_time))
)
ORDER BY s.to_time
That isn't how the CAST works in Postgresql. See http://www.postgresql.org/docs/9.2/static/sql-createcast.html
('00:00' + (h.hour * interval '1Hour'))::time
is easiest to read (IMHO) if you don't mind using the non-standard :: notation.
Finally,
Converted SQL SP into Postgres Function and got the desired result
CREATE OR REPLACE FUNCTION shiftwisedata_sp(IN shiftid bigint, INOUT userdate date, OUT shift_name character varying, OUT from_time time without time zone, OUT to_time time without time zone, OUT readings bigint)
RETURNS SETOF record AS
$BODY$
BEGIN
return query
SELECT userdate, s.shift_name,
('00:00' + (h.hour * interval '1Hour'):: time) AS from_time,
('00:00' + ((h.hour + 1) * interval '1Hour'):: time) AS to_time,
COALESCE(r.Readings, 0) AS readings
FROM shift_wise s
CROSS JOIN (VALUES(0), (1), (2), (3), (4), (5), (6), (7), (8), (9),
(10), (11), (12), (13), (14), (15), (16), (17), (18), (19),
(20), (21), (22), (23)) AS h(hour)
LEFT JOIN LATERAL (SELECT CAST(SUM(r.param_value) as bigint) AS Readings
FROM table_1 r
WHERE r.timestamp_col >= (CASt(userdate As timestamp without time zone) + h.hour * interval '1Hour')
AND r.timestamp_col < (CASt(userdate As timestamp without time zone) + (h.hour + 1) * interval '1Hour')
) AS r ON TRUE
WHERE s.shift_id = shiftid
AND (s.to_time > s.from_time AND
h.hour >= Extract(HOUR from CAST(s.from_time as time)) AND
h.hour < Extract(HOUR from CAST(s.to_time as time))
OR
s.to_time < s.from_time AND
(h.hour >= Extract(HOUR from CAST(s.from_time as time)) OR
h.hour < Extract(HOUR from CAST(s.to_time as time))
))
ORDER BY s.to_time;
END;
$BODY$
LANGUAGE plpgsql VOLATILE

Split datetime SQL Server 2008

I have a table with 3 columns StartDate, EndDate, ElapsedTimeInSec.
I use an AFTER INSERT trigger to calculate the ElapsedTimeInSec.
I would like to do this:
If my start date is 2011-11-18 07:30:00 and my end date 2011-11-18 9:30:00 which give me a ElapsedtimeInSec of 7200 I would like to be able to split it this way.
Row 1 : 2011-11-18 07:30:00 / 2011-11-18 08:00:00 / 1800
Row 2 : 2011-11-18 08:00:00 / 2011-11-18 09:00:00 / 3600
Row 3 : 2011-11-18 09:00:00 / 2011-11-18 09:30:00 / 1800
How can I achieve this result ?
I dont think I made my explaination clear enough.
I have an actual table with data in it which as 2 field one with a StratDowntime and one with a EndDowntime and I would like to create a view of hours per hour base on a production shift of 12 hours (07:00:00 to 19:00:00) of the downtime.
So If I have a downtime from 2011-11-19 06:00:00 to 2011-11-19 08:00:00 I want in my report to see from 07:00:00 so the new rocrd should look like 2011-11-19 07:00:00 to 2011-11-19 08:00:00.
Another example if I do have downtime from 2011-11-19 10:30:00 to 2011-11-19 13:33:00 I should get in my report this
- 2011-11-19 10:30:00 to 2011-11-19 11:00:00
- 2011-11-19 11:00:00 to 2011-11-19 12:00:00
- 2011-11-19 12:00:00 to 2011-11-19 13:00:00
- 2011-11-19 13:00:00 to 2011-11-19 13:33:00
I hope this will clarify the question because none of the solution down there is actually doing this it is close but not on it.
thanks
You could try something like:
DECLARE #StartDate DATETIME = '11/18/2011 07:30:00',
#EndDate DATETIME = '11/18/2011 09:30:00',
#Runner DATETIME
IF DATEDIFF (mi, #StartDate, #EndDate) < 60
BEGIN
SELECT #StartDate,
#EndDate,
DATEDIFF (s, #StartDate, #EndDate)
RETURN
END
SET #Runner = CONVERT (VARCHAR (10), #StartDate, 101) + ' ' + CAST (DATEPART(hh, #StartDate) + 1 AS VARCHAR) + ':00:00'
WHILE #Runner <= #EndDate
BEGIN
SELECT #StartDate,
#Runner,
DATEDIFF (s, #StartDate, #Runner)
SET #StartDate = #Runner
SET #Runner = DATEADD(hh, 1, #Runner)
END
SET #Runner = CONVERT (VARCHAR (10), #EndDate, 101) + ' ' + CAST (DATEPART(hh, #EndDate) AS VARCHAR) + ':00:00'
SELECT #Runner,
#EndDate,
DATEDIFF (s, #Runner, #EndDate)
CTE:
DECLARE #beginDate DATETIME,
#endDate DATETIME
SELECT #beginDate = '2011-11-18 07:30:00',
#endDate = '2011-11-18 09:33:10'
DECLARE #mytable TABLE
(
StartDowntime DATETIME,
EndDowntime DATETIME,
ElapsedDowntimesec INT
)
-- Recursive CTE
;WITH Hours
(
BeginTime,
EndTime,
Seconds
)
AS
(
-- Base case
SELECT #beginDate,
DATEADD(MINUTE, ( DATEPART(MINUTE, #beginDate) * -1 ) + 60, #beginDate),
DATEDIFF
(
SECOND,
#beginDate,
CASE
WHEN #endDate < DATEADD(MINUTE, ( DATEPART(MINUTE, #beginDate) * -1 ) + 60, #beginDate) THEN #endDate
ELSE DATEADD(MINUTE, ( DATEPART(MINUTE, #beginDate) * -1 ) + 60, #beginDate)
END
)
UNION ALL
-- Recursive
SELECT Hours.EndTime,
CASE
WHEN #endDate < DATEADD(MINUTE, ( DATEPART(MINUTE, Hours.BeginTime) * -1 ) + 120, Hours.BeginTime) THEN #endDate
ELSE DATEADD(minute, ( DATEPART(MINUTE, Hours.BeginTime) * -1 ) + 120, Hours.BeginTime)
END,
DATEDIFF
(
SECOND,
Hours.EndTime,
CASE
WHEN #endDate < DATEADD(MINUTE, ( DATEPART(MINUTE, Hours.BeginTime) * -1 ) + 120, Hours.BeginTime) THEN #endDate
ELSE DATEADD(MINUTE, ( DATEPART(MINUTE, Hours.BeginTime) * -1 ) + 120, Hours.BeginTime)
END
)
FROM Hours
WHERE Hours.BeginTime < #endDate
)
INSERT INTO #myTable
SELECT *
FROM Hours
WHERE BeginTime < #endDate
SELECT * FROM #myTable
Results
BeginTime EndTime Seconds
2011-11-18 07:30:00.000 2011-11-18 08:00:00.000 1800
2011-11-18 08:00:00.000 2011-11-18 09:00:00.000 3600
2011-11-18 09:00:00.000 2011-11-18 09:33:10.000 1990
You can use a table valued function applied like SELECT * FROM [dbo].split('2011-11-02 12:55:00','2011-11-02 13:05:00')
Function defintion:
CREATE FUNCTION [dbo].[split] (#d1 DATETIME, #d2 DATETIME)
RETURNS #result TABLE (
StartDate DATETIME,
EndDate DATETIME,
ElapsedTimeSeconds INT
)
AS
BEGIN
-- Store intermediate values in #tmp, using ix as driver for start times.
DECLARE #tmp TABLE (ix INT NOT NULL IDENTITY(0,1) PRIMARY KEY
, d1 DATETIME, d2 DATETIME)
-- Insert first hole hour lower than start time
INSERT INTO #tmp (d1) SELECT DATEADD(HOUR, DATEDIFF(HOUR, -1, #d1), -1)
-- Calculate expected number of intervals
DECLARE #intervals INT = DATEDIFF(HOUR, #d1, #d2) - 1
-- insert all intervals
WHILE #intervals > 0
BEGIN
INSERT INTO #tmp (d1, d2) select top 1 d1, d2 FROM #tmp
SET #intervals = #intervals - 1
END
-- Set start and end time for all whole hour intervals
UPDATE #tmp SET d1 = DATEADD(hour, ix, d1)
, d2 = DATEADD(hour, ix + 1, d1)
-- Set correct start time for first interval
UPDATE #tmp SET d1 = #d1 WHERE d1 <= #d1
-- Insert end interval
INSERT INTO #tmp (d1, d2)
SELECT MAX(d2), #d2 FROM #tmp
-- Delete non-empty last interval
DELETE FROM #tmp WHERE d1 = d2
-- Insert #tmp to #result
INSERT INTO #result (StartDate, EndDate)
SELECT d1, d2 FROM #tmp
-- Set interval lengths
UPDATE #result SET ElapsedTimeSeconds = DATEDIFF(second, StartDate, EndDate)
return
END
GO
To get a result from an existing table, you can use CROSS APPLY. Assuming a table YourTable with StartTime and EndTime you can do something like
SELECT s.*, y.* FROM YourTable y
cross apply dbo.split(y.StartTime, y.EndTime) s
WHERE y.EndTime < '2011-09-11'
to get a result with a kind of join between input data and output table.