Postgres trigger function to update aggregated result in another table - postgresql

I have two tables. Table x and table y. Table x gets updated everyday. I wish to update the table y as soon as new data is inserted in Table x. Table y contains the aggregated value of all the update in Table x each day.
Date is in Date type and the rest of the two column are of real type.
Table_x can be updated daily and table_y should be updated automatically.
Table x:
Date Sales product
12/12/2017 4000 2
12/12/2017 3000 1
12/12/2017 2000 1
12/12/2017 5000 3
11/12/2017 1000 3
11/12/2017 2000 4
Table y (to be as updated as shown below):
Date Sales product
12/12/2017 14000 7
11/12/2017 3000 7
I wrote the trigger function as shown below but it updates each item rather than aggregated value.
CREATE OR REPLACE FUNCTION public.rec_insert_table_y()
RETURNS trigger AS
$BODY$
BEGIN
INSERT INTO table_y ("Date","Sales","product")
SELECT NEW."Date",(sum(NEW."Sales")),(sum(NEW."product"))
GROUP BY NEW."Date";
RETURN NEW;
Trigger Function :
CREATE TRIGGER insert_into_table_y
AFTER INSERT
ON public.table_x
FOR EACH ROW
EXECUTE PROCEDURE public.rec_insert_table_y();

You can write a trigger that updates the aggregate value if exists or inserts if not.
Also you must be aware updates and deletes in the 'x' table:
create function y_x_trg() returns trigger
language plpgsql
as
$body$
declare
row_exists boolean;
begin
if tg_op<>'INSERT' then
update y
set sales = y.sales - old.sales,
product = y.product - old.product
where y.date = old.date
returning true into row_exists;
end if;
if tg_op<>'DELETE' then
update y
set sales = y.sales + new.sales,
product = y.product + new.product
where y.date = new.date
returning true into row_exists;
if row_exists is not true then
insert into y values (new.date, new.sales, new.product);
end if;
return new;
else
return null;
end if;
end;
$body$;
create trigger y_x_trg AFTER INSERT OR UPDATE OR DELETE ON x
FOR EACH ROW EXECUTE PROCEDURE y_x_trg();
You can see a running example at http://rextester.com/FVR79644

Related

How do I cap number of rows in based on category using postgres policies?

I have a database of orders and each order has a time_slot (of type TIME).
select id, time_slot from orders limit 5;
10 | 13:00:00
11 | 12:00:00
13 | 11:00:00
14 | 12:30:00
15 | 11:30:00
I want to make sure that only a certain number of orders can be placed in a time slot; for example, let's say I only want 8 orders per time slot.
I am using Supabase, so I would like to implement using RLS policies.
I've been through several iterations, but none of them have worked. Some complain of infinite recursion. My current approach is the following: I have created a view of time slot load.
create or replace view time_slot_load as
select time_slot, count(*)
from orders
group by time_slot;
select * from time_slot_load limit 5;
11:00:00 | 1
12:30:00 | 1
11:30:00 | 1
13:00:00 | 1
12:00:00 | 7
I can then create a policy that checks against this view.
ALTER POLICY "Only 8 orders per time slot"
ON public.orders
WITH CHECK (
(SELECT (load.count <= 8)
FROM time_slot_load load
WHERE (load.time_slot = orders.time_slot))
);
But this is not working.
Is there some way I can do this using constraints or RLS policies?
Any help is appreciated.
demo
table structure:
begin;
create table orders_count(
time_slot tstzrange ,
order_count integer default 0,
constraint order_ct_max1000 check(order_count <=4));
create table
orders(orderid int primary key, realtimestamp timestamptz , order_info text);
with a as(
SELECT x as begin_,x + interval '1 hour' as end_
FROM generate_series(
timestamp '2022-01-01',
timestamp '2022-01-02',
interval '60 min') t(x))
insert into orders_count(time_slot)
select tstzrange(a.begin_, a.end_,'[]') from a;
commit;
two table, one table for all the orders info, another table is track the timeslot's count, also make one constraint, within one timeslot no more than 4 orderid.
Then create trigger for delete/update/insert action on table orders.
for table orders_count, 20 years only 175200 rows (one hour one row).
main trigger function:
begin;
create or replace function f_trigger_orders_in()
returns trigger as
$$
begin
update orders_count set order_count = order_count + 1
where time_slot #> NEW.realtimestamp;
return null;
end;
$$ language plpgsql;
create or replace function f_trigger_orders_up()
returns trigger as
$$
begin
if OLD.realtimestamp <> NEW.realtimestamp THEN
update orders_count
set order_count = order_count -1
where time_slot #> OLD.realtimestamp;
update orders_count
set order_count = order_count + 1
where time_slot #> NEW.realtimestamp;
end if;
return null;
end
$$ language plpgsql;
create or replace function f_trigger_order_delaft()
returns trigger as
$$
BEGIN
update orders_count set order_count = order_count -1 where time_slot #> OLD.realtimestamp;
return null;
end;
$$
language plpgsql;
triggers action:
create trigger trigger_in_orders
AFTER INSERT ON public.orders FOR EACH ROW execute procedure f_trigger_orders_in();
create trigger trigger_up_orders
after update on public.orders for each row execute procedure f_trigger_orders_up();
create trigger trigger_del_orders
AFTER DELETE ON public.orders FOR EACH ROW execute procedure f_trigger_order_delaft();
I want to make sure that only a certain number of orders can be placed
in a time slot; for example, let's say I only want 8 orders per time
slot.
You cannot do that in PostgreSQL.
https://www.postgresql.org/docs/current/sql-createpolicy.html
check_expression:
Any SQL conditional expression (returning boolean). The conditional
expression cannot contain any aggregate or window functions. This
expression will be used in INSERT and UPDATE queries against the table
if row-level security is enabled. Only rows for which the expression
evaluates to true will be allowed. An error will be thrown if the
expression evaluates to false or null for any of the records inserted
or any of the records that result from the update. Note that the
check_expression is evaluated against the proposed new contents of the
row, not the original contents.
Why view won't work:
https://www.postgresql.org/docs/current/sql-createview.html
see Updatable Views section:
A view is automatically updatable if it satisfies all of the following
conditions:
The view's select list must not contain any aggregates, window functions or set-returning functions.

How can I write a trigger that gets the last inserted row into the table?

I was to populate a field is_continued_post if some conditions are true about the previously inserted row into the table (it's the same user, and it's inserted_at is less than N mins from the new rows inserted_at).
When a new comment is inserted into the database. I want to get the last comment (with the same post_id) that was inserted, then check that the old rows user_id are the same as the new rows user_id, and that the old row was inserted less than 2 mins before the new row. If this is true, I want to flip a boolean on the new row to true before inserting it.
Is this possible with Postgresql triggers? Or is there a better way to do this?
This is what I've come up with so far:
CREATE OR REPLACE FUNCTION update_message_cont()
RETURNS trigger AS $$
BEGIN
old := (SELECT m0.user_id, m0.inserted_at FROM messages AS m0 WHERE (m0.post_id = NEW.post_id) ORDER BY m0.inserted_at DESC LIMIT 1);
NEW.is_continued := CASE
WHEN old is NULL THEN FALSE
WHEN old.user_id = NEW.user_id AND ((NEW.inserted_at - old.inserted_at) < 120) THEN TRUE
END;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
Yes, that is possible, but only if you have a column in the table that allows you to identify the last inserted row. The order of insertion is not reflected in the table as such.
So introduce a column
inserted_at timestamp with time zone DEFAULT clock_timestamp() NOT NULL
An index on (post_id, inserted_at) will make the query fast.
The whole trigger could look like:
CREATE FUNCTION update_message_cont() RETURNS trigger AS
$$BEGIN
SELECT user_id IS NOT DISTINCT FROM NEW.user_id INTO NEW.is_continued
FROM messages
WHERE post_id = NEW.post_id
AND inserted_at > NEW.inserted_at - INTERVAL '120 seconds'
ORDER BY inserted_at DESC
LIMIT 1;
-- if no previous row was found:
IF NEW.is_continued IS NULL THEN
NEW.is_continued = FALSE;
END IF;
RETURN NEW;
END;$$ LANGUAGE plpgsql;
CREATE TRIGGER update_message_cont BEFORE INSERT ON messages
FOR EACH ROW EXECUTE PROCEDURE update_message_cont();

How to implement a constraint in postgresql

In a Details table (Product ID, Receipt ID) only products with a stock greater than 0 can be added.
Warehouse (Product ID, Stock). How can I implement this constraint in Postgresql?
-- CREATE THE FUNCTION
CREATE FUNCTION trg_product_stock_check()
RETURNS trigger AS
$func$
BEGIN
if exists (select * from warehouse w where w.product_id = new.product_id and stock <= 0) then
raise NOTICE 'Product must have a stock of greater than 0';
return null;
end if;
return new;
END
$func$ LANGUAGE plpgsql;
-- CREATE THE TRIGGER
CREATE TRIGGER product_stock_check
BEFORE INSERT ON "orders"
FOR EACH ROW EXECUTE PROCEDURE trg_product_stock_check();
The solution is to create a trigger function that's set up on insert of the order's table. First create your function. You want to check your warehouse table for the product's stock quantity in the function. If the product's stock quantity is less than or equal to 0, return null with a log message. Otherwise, return the newly created row (eg: the new keyword in the function is the row being inserted). After you create the function, you can set the trigger up to run on the orders table before insert.
You can read more about creating a trigger/function here and here.

Postgresql same date record insert

I am using a Postgresql 9.5 database. A third party software application is also using this database. I have a Features table. I created an Events table to record Features events.
Features
------------
id name lon lat
1 x 0 10
2 y 15 20
When I create a record in the Features table, my trigger inserts a record into the Events table.
Events
id name date feature_id
1 insert 09.04.2018 14:22:23.065125 1
When I update Features name, lon and lat and save it, the software execution results in 3 update records at same time.
Events
id name date feature_id
1 insert 09.04.2018 14:22:23.065125 1
2 update 09.04.2018 18:15:41.099689 1
3 update 09.04.2018 18:15:41.099689 1
4 update 09.04.2018 18:15:41.099689 1
But this is 3 update is same values.
How can I restrict this in my trigger?
My trigger function:
CREATE FUNCTION event_fn() AS $BODY$ BEGIN
IF TG_OP = 'INSERT' THEN
INSERT INTO events (event_name, event_date, feature_id) VALUES ('insert', now(), NEW.id);
RETURN NEW;
END IF;
IF TG_OP = 'UPDATE' THEN
INSERT INTO events (event_name, event_date, feature_id) VALUES ('update', now(), NEW.id);
RETURN NEW;
END IF;
IF TG_OP = 'DELETE' THEN
INSERT INTO events (event_name, event_date, feature_id) VALUES ('delete', now(), OLD.id);
RETURN OLD;
END IF;
END;
The best solution would be to opt out of the software that performs several updates instead of actually a single one. However, if you can not do this, you can add a trigger for the events table, e.g.:
create or replace function before_insert_on_events()
returns trigger language plpgsql as $$
begin
if exists (
select 1
from events e
where e.name = new.name
and e.date = new.date
and e.feature_id = new.feature_id)
then new = null;
end if;
return new;
end $$;
create trigger before_insert_on_events
before insert on events
for each row
execute procedure before_insert_on_events();

postgres count from table efficient way

In my application we are using postgresql,now it has one million records in summary table.
When I run the following query it takes 80,927 ms
SELECT COUNT(*) AS count
FROM summary_views
GROUP BY question_id,category_type_id
Is there any efficient way to do this?
COUNT(*) in PostgreSQL tends to be slow. It's a feature of MVCC. One of the workarounds of the problem is a row counting trigger with a helper table:
create table table_count(
table_count_id text primary key,
rows int default 0
);
CREATE OR REPLACE FUNCTION table_count_update()
RETURNS trigger AS
$BODY$
begin
if tg_op = 'INSERT' then
update table_count set rows = rows + 1
where table_count_id = TG_TABLE_NAME;
elsif tg_op = 'DELETE' then
update table_count set rows = rows - 1
where table_count_id = TG_TABLE_NAME;
end if;
return null;
end;
$BODY$
LANGUAGE 'plpgsql' VOLATILE;
Next step is to add proper trigger declaration for each table you'd like to use it with. For example for table tab_name:
begin;
insert into table_count values
('tab_name',(select count(*) from tab_name));
create trigger tab_name_table_count after insert or delete
on tab_name for each row execute procedure table_count_update();
commit;
It is important to run in a transaction block to keep actual count and helper table in sync in case of delete or insert between initial count and trigger creation. Transaction guarantees this. From now on to get current count instantly, just invoke:
select rows from table_count where table_count_id = 'tab_name';
Edit: In case of your group by clause, you'll need more sophisticated trigger function and count table.