I am new to SQL and I'm working on a project that requires a trigger. Here is my code:
CREATE OR REPLACE FUNCTION insert_detailed_sale_entry()
RETURNS TRIGGER
LANGUAGE plpgsql
AS $$
BEGIN
DELETE FROM summary_customer_sales;
INSERT INTO summary_customer_sales
SELECT customer_id,
last_name,
first_name,
year,
month,
SUM(amount) AS total_sales
FROM detailed_customer_sales
GROUP BY year, month, last_name, first_name, customer_id
ORDER BY year, month, total_sales DESC;
RETURN NEW;
END;
$$;
CREATE TRIGGER new_customer_sale
AFTER INSERT
ON detailed_customer_sales
FOR EACH STATEMENT
EXECUTE PROCEDURE insert_detailed_sale_entry();
When I try to test this trigger by inserting a row in the detailed_customer_sales table, the trigger doesn't seem to be firing. Does anyone see any errors in my code or can point me in the right direction to troubleshoot? Thank you in advance!
I compared an aggregate function in both tables after performing an insert on the detailed customer table. The aggregate function in the summary_customer_sales table is supposed to match after the trigger fires, but it does not.
Related
I have two identical tables: table1 and table2. I need to replicate all updates from one table to another using an after trigger. But I don't want to list all columns names in the update statement(except the PK). Is it possible to do something like this?
CREATE FUNCTION replicate_changes()
RETURNS trigger
LANGUAGE 'plpgsql' AS
$BODY$
BEGIN
UPDATE table2
SET (*) = NEW.*;
WHERE table2.id = NEW.id;
RETURN NULL;
END;
$BODY$;
CREATE TRIGGER trigger_replicate_changes
AFTER UPDATE
ON table1
FOR EACH ROW
EXECUTE FUNCTION replicate_changes();
You cannot do this directly, but you can delete the existing row and insert/select the new row. (see demo).
create or replace function replicate_changes()
returns trigger
language 'plpgsql' as
$body$
begin
delete from table2 where id = old.id;
insert into table2
select * from table1
where id = new.id;
return null;
end;
$body$;
But be careful, this is a dangerous process. What happens when you insert or delete from table1. As it stand those actions will not be reflected in table2. And what happens when DML is issued directly against table2, or only 1 of the table definitions gets updated (ddl) bu not the other.
Description:
I am running postgresql 13
I have two tables under different schemas, t1 and t2.
t2 is derivative of t1 in the sense that they share all the same
columns and data, but t2 is always downstream of t1 as far as
validity.
The rows in both tables share the same primary key, which is what I assume would be used as the link between them.
The ask:
I would like to create a trigger that reflects any changes in t1 and syncs t2 to be the same.
I started with INSERT or UPDATE, but if DELETE is easily added, I would like to implement that as well.
Trigger Code:
-- Trigger for t1 to t2 --
CREATE OR REPLACE FUNCTION t1_schema.sync_trigger()
RETURNS TRIGGER AS
$$
BEGIN
INSERT INTO t2_schema.t2 (col1, col2, col3)
VALUES (NEW.col1, NEW.col2, NEW.col3);
RETURN NEW;
END
$$ LANGUAGE plpgsql;
CREATE TRIGGER t1t2_test_sync
AFTER INSERT OR UPDATE ON t1_schema.t1
FOR EACH ROW
EXECUTE PROCEDURE t1_schema.sync_trigger()
When I execute this code and do a test UPDATE on t1, the same row on t2 does not reflect the changes or give me any errors.
I have tried:
Discretely labeling all rows as updated with NEW. format, but run into the problem of primary key column not being editable in t2.
Adding a WHERE clause after the VALUES clause, something like WHERE primary_key=NEW.primary_key, but I get an error.
Another option I have seen is adding an IF statement before the
INSERT, or adding a WHEN clause in the trigger, but neither have
worked.
Your best approach is to not create t2 as a table. Instead create it as a VIEW on t1. This totally eliminates triggers to keep them synchronized because the actual source is the same. Follows the concept to store a single data point in only 1 place. Keep in mind that if you store a single piece in 2 places, 1 on them will be wrong at some point. (see demo).
create view soq2.t2 as
select *
from soq1.t1;
Also if you need column names to change then use an alias during the create view;
create view soq2.t2a as
select t1_id as t2_id
, name as t2_name
, status as t2_status
from soq1.t1;
(A) Solution based on triggers
You maybe get an error when updating a row in t1 because your trigger function tries to insert a new row in t2 which has alreday been inserted in t2 by the same trigger function when it has been inserted in t1. You need to duplicate and specialize your trigger functions, one for insert, one for update, one for delete because the treatment to be triggered on t2 is different :
CREATE OR REPLACE FUNCTION t1_schema.sync_trigger_insert()
RETURNS TRIGGER AS
$$
BEGIN
INSERT INTO t2_schema.t2 (col1, col2, col3)
VALUES (NEW.col1, NEW.col2, NEW.col3);
RETURN NEW;
END
$$ LANGUAGE plpgsql;
CREATE TRIGGER t1t2_test_sync_insert
AFTER INSERT ON t1_schema.t1
FOR EACH ROW EXECUTE PROCEDURE t1_schema.sync_trigger_insert() ;
CREATE OR REPLACE FUNCTION t1_schema.sync_trigger_update()
RETURNS TRIGGER AS
$$
BEGIN
UPDATE t2
SET col1 = NEW.col1
, col2 = NEW.col2
, col3 = NEW.col3
WHERE primary_key_t2 = NEW. primary_key_t1 ; -- primary_key_t2 must be replaced by the set of columns which are in the primary key of t2 with AND operators, the same for NEW.primary_key_t1
RETURN NEW;
END
$$ LANGUAGE plpgsql;
CREATE TRIGGER t1t2_test_sync_update
AFTER UPDATE ON t1_schema.t1
FOR EACH ROW EXECUTE PROCEDURE t1_schema.sync_trigger_update() ;
CREATE OR REPLACE FUNCTION t1_schema.sync_trigger_delete()
RETURNS TRIGGER AS
$$
BEGIN
DELETE FROM t2
WHERE primary_key_t2 = NEW. primary_key_t1 ; -- primary_key_t2 must be replaced by the set of columns which are in the primary key of t2 with AND operators, the same for NEW.primary_key_t1
RETURN OLD; -- NEW is not available for triggers ON DELETE
END
$$ LANGUAGE plpgsql;
CREATE TRIGGER t1t2_test_sync_delete
AFTER DELETE ON t1_schema.t1
FOR EACH ROW EXECUTE PROCEDURE t1_schema.sync_trigger_delete() ;
(B) Solution based on foreign key
It is possible that a foreign key on t2 (col1,col2,col3) referencing t1 (col1, col2, col3) with the options ON UPDATE CASCADE ON DELETE CASCADE may deliver your expected result in a much more simple and efficient way, see the manual. In this case, you don't need the triggers ON UPDATE and ON DELETE anymore, but you still need the trigger ON INSERT.
I have a big trouble.
In my function, I need get new id from table then insert into the table with the new id +1.
The table just like below:
create table bill(
flinkid bigint not null,
fsystem text not null,
fnaviid bigint not null,
fbillid bigint not null,
fdata jsonb,
constraint bill_pkey primary key (flinkid, fsystem, fnaviid, fbillid)
);
In my function, I need to get the max old fbillid from the table, then insert a new row with the new id +1, just as below:
do $$ declare
vnewid bigint;
vdata jsonb;
begin
...
select coalesce(max(fbillid),0)+1 into vnewid from bill where flinkid=123 and fsystem='test' and fnaviid=123;
...
insert into bill(flinkid,fsystem,fnaviid,fbillid,fdata)
values(123,'test',123,vnewid,vdata);
...
update bill set fdata=jsonb_set_lax(fdata,'{head,fbillno}',"XXX"::jsonb)
where flinkid=123 and fsystem='test' and fnaviid=123 and fbillid=vnewid;
...
end;
$$ language plpgsql;
When meets concurrent transactions, that is two or more reqeusts call the function almost at the same time, because the function's transaction need some time to execute, the first transation hasn't commit yet, and the second transaction visit the table, the field fbillid is the same one. this will cause two row becomes one row, and the row data is the second, the first row's data is lost.
How to solve this problem?
Please give me some advice, thanks a lot!
You can use SELECT FOR UPDATE command. FOR UPDATE causes the rows retrieved by the SELECT statement to be locked as though for update. This prevents them from being locked, modified or deleted by other transactions until the current transaction ends. That is, other transactions that attempt UPDATE, DELETE, SELECT FOR UPDATE, SELECT FOR NO KEY UPDATE, SELECT FOR SHARE or SELECT FOR KEY SHARE of these rows will be blocked until the current transaction ends; conversely, SELECT FOR UPDATE will wait for a concurrent transaction that has run any of those commands on the same row, and will then lock and return the updated row.
For example:
select fbillid into vnewid
from bill where fbillid = (select coalesce(max(fbillid),0) from bill)
for update;
------------ OR --------------
select fbillid into vnewid
from bill
order by fbillid desc
limit 1
for update;
But, after finishing your operations, you must update this locked record for unlocking:
update bill
set
updated_date = now()
where fbillid = vnewid;
I'd like to get an opinion on a trigger I've written for a PostGreSQL Database in PL/pgSQL. I haven't done it previously and would like to get suggestions by more experienced users.
Task is simple enough:
Reduce the number of entries in a table to a set amount.
What should happen:
An INSERT into to the table device_position occurs,
If the amount of entries with a specific column (deviceid) value exceeds 50 delete the oldest.
Repeat
Please let me know if you see any obvious flaws:
CREATE OR REPLACE FUNCTION trim_device_positions() RETURNS trigger AS $trim_device_positions$
DECLARE
devicePositionCount int;
maxDevicePos CONSTANT int=50;
aDeviceId device_position.id%TYPE;
BEGIN
SELECT count(*) INTO devicePositionCount FROM device_position WHERE device_position.deviceid=NEW.deviceid;
IF devicePositionCount>maxDevicePos THEN
FOR aDeviceId IN SELECT id FROM device_position WHERE device_position.deviceid=NEW.deviceid ORDER BY device_position.id ASC LIMIT devicePositionCount-maxDevicePos LOOP
DELETE FROM device_position WHERE device_position.id=aDeviceId;
END LOOP;
END IF;
RETURN NULL;
END;
$trim_device_positions$ LANGUAGE plpgsql;
DROP TRIGGER trim_device_positions_trigger ON device_position;
CREATE TRIGGER trim_device_positions_trigger AFTER INSERT ON device_position FOR EACH ROW EXECUTE PROCEDURE trim_device_positions();
Thanks for any wisdom coming my way :)
I tried create a trigger:
CREATE TRIGGER DataTrigger ON Data AFTER INSERT , DELETE
AS
BEGIN
IF EXISTS(SELECT * FROM Inserted)
INSERT INTO [dbo].[AuditTrail]
([ActionType]
,[TableName]
,[Name]
,[Time])
('INSERT' //Incorrect syntax near 'INSERT'
,'Data'
,SELECT col1 FROM Inserted
,CURRENT_TIMESTAMP //Incorrect syntax near the keyword 'CURRENT_TIMESTAMP')
END
but it keep saying that I have thoes errors, can somebody show me where I did wrong?
P/S: what is the best way to detect an update?
Thank you
CREATE TRIGGER DataTrigger ON Data AFTER INSERT , DELETE
AS
BEGIN
INSERT INTO [dbo].[AuditTrail]
([ActionType]
,[TableName]
,[Name]
,[Time])
SELECT 'INSERT'
,'Data'
,col1
,CURRENT_TIMESTAMP
FROM Inserted
END
Just for clarification.
Your query, if syntatically correct would have failed if more than one row was inserted, the version above allows for multiple inserts.
The IF EXISTS was redundant, which is why it was removed, if there are no rows there will be no insert into your audit table.
If you want to audit DELETE you'll need a similar statement again, but using the Deleted table rather than Inserted
To audit UPDATE, create a new trigger, for each updated row you get an entry in Inserted with the new updates and an entry in Deleted with the old data, you can join these if you want to track old and new.
CREATE TRIGGER DataTrigger ON Data AFTER INSERT , DELETE
AS
BEGIN
INSERT INTO [dbo].[AuditTrail]
([ActionType]
,[TableName]
,[Name]
,[Time])
SELECT 'INSERT', 'Data', col1, CURRENT_TIMESTAMP FROM Inserted
END
I am not entirely sure what you are trying to accomplish here so I have just corrected the syntax to get it to work, but it should help. I have also tried to handle the deleted case as well
CREATE TRIGGER DataTrigger
ON Data
AFTER INSERT , DELETE
AS
BEGIN
INSERT INTO [dbo].[AuditTrail]([ActionType],
[TableName],
[Name],
[Time])
SELECT 'INSERT','Data', col1, CURRENT_TIMESTAMP
FROM Inserted
INSERT INTO [dbo].[AuditTrail]([ActionType],
[TableName],
[Name],
[Time])
SELECT 'DELETE','Data', col1, CURRENT_TIMESTAMP
FROM Deleted
END