Function (trigger) to insert rows based on interval and duration - postgresql

I have a schedule table, like so:
ScheduleId::uuid | Start::timestamptz(now()) | SlotSize::int(minutes) | Interval::int(days)
and a slot table like so:
SlotId::uuid | ScheduleId::uuid | Start::timestamptz | End::timestamptz
I want to automatically insert slots, based on a trigger on the schedule table.
So far I have:
create
trigger create_slots after insert
on
schedule for each row execute procedure create_new_slots();
create or replace function create_new_slots()
returns trigger
language plpgsql
as $function$
begin
-- in a loop determine how many slots there are, then insert each one
insert into slot
select
uuid_generate_v4(),
new."ScheduleId",
start, -- need to determine the start time of each instance of slot
end -- need to determine the end time of each instance of slot
-- end loop
end return new;
end $function$
I need to somehow put this into a cursor and calculate the number of slots and the start and end times each slot.
I am using PostgreSQL 10
Any help is appreciated!

OK so what I needed, it seems, was to generate a series based on the [Start] time (rounded to the hour), the end time (which is the [Start] time + the [PlanningHorizon] in days) and the [SlotSize]. Then loop through this series and insert each time slot into my [Slot] table:
CREATE OR REPLACE FUNCTION create_new_slots()
RETURNS trigger
LANGUAGE plpgsql
AS $function$
declare
slot timestamptz;
begin
for slot in select generate_series(
date_trunc('hour', new."Start")::timestamptz,
(new."Start" + interval '1' day * new."PlanningHorizon")::timestamptz,
new."SlotSize" * '1 minutes'::interval)
loop
insert into slot select uuid_generate_v4(), new."ScheduleId", slot, slot + new."SlotSize" * '1 minutes'::interval;
end loop;
return NEW;
end
$function$

Related

Postgres: timing the runtime of a function

I want to update an audit table that stores the duration of a function/stored proc,
so far I have
drop table if exists tmp_interval_test;
create table tmp_interval_test (
id serial primary key,
duration interval
);
drop function if exists tmp_interval;
create or replace function tmp_interval()
returns void as
$body$
declare
sleep int;
start_time timestamp;
end_time timestamp;
diff interval;
begin
start_time := now();
sleep := floor(random() * 10 + 1)::int;
-- actual code goes here
perform pg_sleep(sleep);
end_time := now();
diff := age(end_time, start_time);
insert into tmp_interval_test (duration) values (diff);
end;
$body$
language 'plpgsql' volatile;
However, when I test this function, the duration shows
id|duration|
--|--------|
1|00:00:00|
How do I correctly insert the duration into my table?
The now() functions returns transaction time - it is same inside one transaction. So 0 is correct result. You should to use different functions, that returns real time - Use clock_timestamp() function instead.
On second hand, if you want to collect times of functions, you can use a buildin functionality in Postgres (if has superuser rights). Activate tracking functions. Then you can see what you need in system table pg_stat_user_function.
See SO regarding now()
Updated function and used clock_timestamp() instead of now(), e.g.,
start_time := clock_timestamp();

Race condition in partitioning with dynamic table creation

I'm trying to implement table partitioning with dynamic table creation using BEFORE INSERT trigger to create new tables and indexes when necesarry using following solution:
create table mylog (
mylog_id serial not null primary key,
ts timestamp(0) not null default now(),
data text not null
);
CREATE OR REPLACE FUNCTION mylog_insert() RETURNS trigger AS
$BODY$
DECLARE
_name text;
_from timestamp(0);
_to timestamp(0);
BEGIN
SELECT into _name 'mylog_'||replace(substring(date_trunc('day', new.ts)::text from 0 for 11), '-', '');
IF NOT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name=_name) then
SELECT into _from date_trunc('day', new.ts)::timestamp(0);
SELECT into _to _from + INTERVAL '1 day';
EXECUTE 'CREATE TABLE '||_name||' () INHERITS (mylog)';
EXECUTE 'ALTER TABLE '||_name||' ADD CONSTRAINT ts_check CHECK (ts >= '||quote_literal(_from)||' AND ts < '||quote_literal(_to)||')';
EXECUTE 'CREATE INDEX '||_name||'_ts_idx on '||_name||'(ts)';
END IF;
EXECUTE 'INSERT INTO '||_name||' (ts, data) VALUES ($1, $2)' USING
new.ts, new.data;
RETURN null;
END;
$BODY$
LANGUAGE plpgsql;
CREATE TRIGGER mylog_insert
BEFORE INSERT
ON mylog
FOR EACH ROW
EXECUTE PROCEDURE mylog_insert();
Everything works as expected but each day when concurrent INSERT statements are being fired for the first time that day, one of them fails trying to "create table that already exists". I suspect that this is caused by the triggers being fired concurrently and both trying to create new table and only one can succeed.
I could be using CREATE TABLE IF NOT EXIST but I cannot detect the outcome so I cannot reliably create constraints and indexes.
What can I do to avoid such problem? Is there any way to signal the fact that the table has been already created to other concurrent triggers? Or maybe there is a way of knowing if CREATE TABLE IF NOT EXISTS created new table or not?
What I do is create a pgAgent job to run every day and create 3 months of tables ahead of time.
CREATE OR REPLACE FUNCTION avl_db.create_alltables()
RETURNS numeric AS
$BODY$
DECLARE
rec record;
BEGIN
FOR rec IN
SELECT date_trunc('day', i::timestamp without time zone) as table_day
FROM generate_series(now()::date,
now()::date + '3 MONTH'::interval,
'1 DAY'::interval) as i
LOOP
PERFORM avl_db.create_table (rec.table_day);
END LOOP;
PERFORM avl_db.avl_partition(now()::date,
now()::date + '3 MONTH'::interval);
RETURN 0;
END;
$BODY$
LANGUAGE plpgsql VOLATILE
COST 100;
ALTER FUNCTION avl_db.create_alltables()
OWNER TO postgres;
create_table is very similar to your CREATE TABLE code
avl_partition update the BEFORE INSERT TRIGGER but I saw you do that part with dynamic query. Will have to check again that.
Also I see you are doing inherit, but you are missing a very important CONSTRAINT
CONSTRAINT route_sources_20170601_event_time_check CHECK (
event_time >= '2017-06-01 00:00:00'::timestamp without time zone
AND event_time < '2017-06-02 00:00:00'::timestamp without time zone
)
This improve the query a lot when doing a search for event_time because doesn't have to check every table.
See how doesn't check all tables for the month:
Eventually I wrapped CREATE TABLE in BEGIN...EXCEPTION block that catches duplicate_table exception - this did the trick, but creating the tables upfront in a cronjob is much better approach performance-wise.
CREATE OR REPLACE FUNCTION mylog_insert() RETURNS trigger AS
$BODY$
DECLARE
_name text;
_from timestamp(0);
_to timestamp(0);
BEGIN
SELECT into _name 'mylog_'||replace(substring(date_trunc('day', new.ts)::text from 0 for 11), '-', '');
IF NOT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name=_name) then
SELECT into _from date_trunc('day', new.ts)::timestamp(0);
SELECT into _to _from + INTERVAL '1 day';
BEGIN
EXECUTE 'CREATE TABLE '||_name||' () INHERITS (mylog)';
EXECUTE 'ALTER TABLE '||_name||' ADD CONSTRAINT ts_check CHECK (ts >= '||quote_literal(_from)||' AND ts < '||quote_literal(_to)||')';
EXECUTE 'CREATE INDEX '||_name||'_ts_idx on '||_name||'(ts)';
EXCEPTION WHEN duplicate_table THEN
RAISE NOTICE 'table exists -- ignoring';
END;
END IF;
EXECUTE 'INSERT INTO '||_name||' (ts, data) VALUES ($1, $2)' USING
new.ts, new.data;
RETURN null;
END;
$BODY$
LANGUAGE plpgsql;

Transpose generate series date postgresql

Questions about transpose are asked many times before, but I cannot find any good answer when using generate_series and dates, because the columns may vary.
WITH range AS
(SELECT to_char(generate_series('2015-01-01','2015-01-05', interval '1 day'),'YYYY-MM-DD'))
SELECT * FROM range;
The normal output from generate series is:
2015-12-01
2015-12-02
2015-12-03
... and so on
http://sqlfiddle.com/#!15/9eecb7db59d16c80417c72d1e1f4fbf1/5478
But I want it to be columns instead
2015-12-01 2015-12-02 2015-12-03 ...and so on
It seems that crosstab maybe should do the trick, but I only get errors:
select * from crosstab('(SELECT to_char(generate_series('2015-01-01','2015-01-05', interval '1 day'),'YYYY-MM-DD'))')
as ct (dynamic columns?)
How do I get crosstab to work with generate_series(date-date) and different intervals dynamically?
TIA
Taking Reference from link PostgreSQL query with generated columns.
you can generate columns dynamically:
create or replace function sp_test()
returns void as
$$
declare cases character varying;
declare sql_statement text;
begin
drop table if exists temp_series;
create temporary table temp_series as
SELECT to_char(generate_series('2015-01-01','2015-01-02', interval '1 day'),'YYYY-MM-DD') as series;
select string_agg(concat('max(case when t1.series=','''',series,'''',' then t1.series else ''0000-00-00'' end) as ','"', series,'"'),',') into cases from temp_series;
drop table if exists temp_data;
sql_statement=concat('create temporary table temp_data as select ',cases ,'
from temp_series t1');
raise notice '%',sql_statement;
execute sql_statement;
end;
$$
language 'plpgsql';
Call function in following way to get output:
select sp_test(); select * from temp_data;
Updated Function which takes two date paramaeters:
create or replace function sp_test(start_date timestamp without time zone,end_date timestamp without time zone)
returns void as
$$
declare cases character varying;
declare sql_statement text;
begin
drop table if exists temp_series;
create temporary table temp_series as
SELECT to_char(generate_series(start_date,end_date, interval '1 day'),'YYYY-MM-DD') as series;
select string_agg(concat('max(case when t1.series=','''',series,'''',' then t1.series else ''0000-00-00'' end) as ','"', series,'"'),',') into cases from temp_series;
drop table if exists temp_data;
sql_statement=concat('create temporary table temp_data as select ',cases ,'
from temp_series t1');
raise notice '%',sql_statement;
execute sql_statement;
end;
$$
language 'plpgsql';
Function call:
select sp_test('2015-01-01','2015-01-10'); select * from temp_data;

Oracle sql trigger on insert/update to calculate 2 values and update a third value

I'd like to make a trigger that will update the row by calculating 2 values and update the third one:
CREATE TABLE Customers (
Cust_ID INTEGER PRIMARY KEY,
Dates TIMESTAMP WITH LOCAL TIME ZONE,
Quantity dec(7,2) NOT NULL,
Price_per_item dec(7,2) NOT NULL,
Total_price dec(7,2)
);
I have done this:
CREATE OR REPLACE TRIGGER cust_after_insert AFTER INSERT
ON Customers
FOR EACH ROW
BEGIN
UPDATE Customers
SET
DATES = CURRENT_TIMESTAMP,
Total_price = Quantity * Price_per_item;
END;
I get some kind of errors that I try to view and edit table at the same time.
I've also tried this:
CREATE SEQUENCE cust_Seq START WITH 1 INCREMENT BY 1;
CREATE OR REPLACE TRIGGER cust_trig BEFORE INSERT ON Customers
FOR EACH ROW
BEGIN
SELECT cust_Seq.nextval INTO :new.Fuel_ID FROM dual;
SELECT CURRENT_TIMESTAMP INTO :new.DATES FROM dual;
SELECT Quantity * Price_per_item INTO :new.Total_price FROM dual;
END;
/
I would prefer the second option and I will need something if an UPDATE will occur as well.
The second approach is in the right direction, but it needs some tweaking to handle the Total_price calculation and to handle updates too:
CREATE SEQUENCE cust_Seq START WITH 1 INCREMENT BY 1;
CREATE OR REPLACE TRIGGER cust_trig BEFORE INSERT OR UPDATE ON Customers
FOR EACH ROW
BEGIN
IF INSERTING THEN
SELECT cust_Seq.nextval INTO :new.Fuel_ID FROM dual;
SELECT CURRENT_TIMESTAMP INTO :new.DATES FROM dual;
END IF;
:new.Total_price := :new.Quantity * :new.Price_per_item;
END;
/

Plpgsql - Iterate over a recordset multiple times

I have a table with series of months with cumulative activity e.g.
month | activity
Jan-15 | 20
Feb-15 | 22
I also have a series of thresholds in another table e.g. 50, 100, 200. I need to get the date when the threshold is reached i.e. activity >= threshold.
The way I thought of doing this is to have a pgsql function that reads in the thresholds table, iterates over that cursor and reads in the months table to a cursor, then iterating over those rows working out the month where the threshold is reached. For performance reasons, rather than selecting all rows in the months table each time, I would then go back to the first row in the cursor and re-iterate over with the new value from the thresholds table.
Is this a sensible way to approach the problem? This is what I have so far - I am getting a
ERROR: cursor "curs" already in use error.
CREATE OR REPLACE FUNCTION schema.function()
RETURNS SETOF schema.row_type AS
$BODY$
DECLARE
rec RECORD;
rectimeline RECORD;
notification_threshold int;
notification_text text;
notification_date date;
output_rec schema.row_type;
curs SCROLL CURSOR FOR select * from schema.another_function_returning_set(); -- this is months table
curs2 CURSOR FOR select * from schema.notifications_table;
BEGIN
OPEN curs;
FOR rec IN curs2 LOOP
notification_threshold := rec.threshold;
LOOP
FETCH curs INTO rectimeline; -- this line seems to be the problem - not sure why cursor is closing
IF notification_threshold >= rectimeline.activity_total THEN
notification_text := rec.housing_notification_text;
notification_date := rectimeline.active_date;
SELECT notification_text, notification_date INTO output_rec.notification_text, output_rec.notification_date;
MOVE FIRST from curs;
RETURN NEXT output_rec;
END IF;
END LOOP;
END LOOP;
RETURN;
END;
$BODY$
LANGUAGE plpgsql VOLATILE
select distinct on (t.threshold) *
from
thresholds t
inner join
months m on t.threshold < m.activity
order by t.threshold desc, m.month