I have two tables called sale and customer. I want to create a trigger that updates the column last_purchase on customer table on each new insert in the sale table.
Table customer: customer_id, name, last_sale, ...
Table sale: sale_id, customer_id, date, ...
CREATE TRIGGER update_last_sale BEFORE INSERT ON sale FOR EACH ROW EXECUTE...
I have started writing but I don't know how to do it.
Could someone help me?
CREATE FUNCTION update_customer_last_sale() RETURNS TRIGGER AS $$
BEGIN
UPDATE customer SET last_sale=now() WHERE cutomer_id=NEW.customer_id;
RETURN NEW;
END; $$
LANGUAGE plpgsql;
then
CREATE TRIGGER update_last_sale
BEFORE INSERT ON sale
FOR EACH ROW EXECUTE update_customer_last_sale;
NEW is the row which is about to be inserted in the sale table. (For an update row, it would be NEW for how the row will look after the update, and OLD for how the row looks before the update).
Basically, I don't think it is a good idea to store redundant data. The last_sale column in customers is just an aggregate of max(sales.sale_date).
It even gets worse if we use now() to touch customers.last_date. What would happen if we would need to re-insert some historical records (eg to recompute last year's taxes). That's what you get when you store redundant data....
-- modelled after Erwin's version
SET search_path='tmp';
-- DROP TABLE customers CASCADE;
CREATE TABLE customers
( id INTEGER NOT NULL PRIMARY KEY
, name VARCHAR
, last_sale DATE
);
-- DROP TABLE sales CASCADE;
CREATE TABLE sales
( id INTEGER NOT NULL PRIMARY KEY
, customer_id INTEGER REFERENCES customers(id)
, saledate DATE NOT NULL
);
CREATE OR REPLACE FUNCTION update_customer_last_sale() RETURNS TRIGGER AS $meat$
BEGIN
UPDATE customers cu
-- SET last_sale = now() WHERE id=NEW.customer_id
SET last_sale = (
SELECT MAX(saledate) FROM sales sa
WHERE sa.customer_id=cu.id
)
WHERE cu.id=NEW.customer_id
;
RETURN NEW;
END; $meat$
LANGUAGE plpgsql;
CREATE TRIGGER update_last_sale
AFTER INSERT ON sales
FOR EACH ROW
EXECUTE PROCEDURE update_customer_last_sale();
INSERT INTO customers(id,name,last_sale) VALUES(1, 'Dick', NULL),(2, 'Sue', NULL),(3, 'Bill', NULL);
INSERT INTO sales(id,customer_id,saledate) VALUES (1,1,'1900-01-01'),(2,1,'1950-01-01'),(3,2,'2011-12-15');
SELECT * FROM customers;
SELECT * FROM sales;
The results:
id | name | last_sale
----+------+------------
3 | Bill |
1 | Dick | 1950-01-01
2 | Sue | 2011-12-15
(3 rows)
id | customer_id | saledate
----+-------------+------------
1 | 1 | 1900-01-01
2 | 1 | 1950-01-01
3 | 2 | 2011-12-15
(3 rows)
I think you want the rule here.
CREATE RULE therule AS ON INSERT TO sale DO ALSO
(UPDATE customer SET customer.last_sale = now()
WHERE customer.customer_id=NEW.customer_id);
EDIT: but see the discussion in comments.
Related
If I have a table like this:
CREATE TABLE mytable
(
id SERIAL,
content TEXT,
copyofid INTEGER
);
Is there a way to copy id into copyofid in a single insert statement?
I tried:
INSERT INTO mytable(content, copyofid) VALUES("test", id);
But that doesn't seem to work.
You can find the sequence behind your serial column using pg_get_serial_sequence() and access it using currval() to get what serial column just got as a result of your INSERT.
CREATE TABLE mytable
( id SERIAL,
content TEXT,
copyofid INTEGER
);
--this works for a single-record insert
INSERT INTO mytable
(content, copyofid)
VALUES
('test', currval(pg_get_serial_sequence('mytable','id')));
--inserting more, you'll have to handle both columns relying on the sequence
INSERT INTO mytable
( id,
content,
copyofid)
VALUES
( nextval(pg_get_serial_sequence('mytable','id')),
'test3',
currval(pg_get_serial_sequence('mytable','id'))),
( nextval(pg_get_serial_sequence('mytable','id')),
'test4',
currval(pg_get_serial_sequence('mytable','id')));
table mytable;
-- id | content | copyofid
------+---------+----------
-- 1 | test | 1
-- 2 | test3 | 2
-- 3 | test4 | 3
--(3 rows)
Fiddle
Edouard makes makes a fair point that if you can specify the conditions when you want this behaviour, you can add them to the definition:
CREATE TABLE mytable
( id SERIAL,
content TEXT,
copyofid integer
generated always as (
case when content ilike '%requires copying ID%' then id end)
stored
);
insert into mytable (content) values ('abc') returning *;
-- id | content | copyofid
------+---------+----------
-- 1 | abc |
--(1 row)
insert into mytable (content) values ('abc, but requires copying ID') returning *;
-- id | content | copyofid
------+------------------------------+----------
-- 2 | abc, but requires copying ID | 2
--(1 row)
If they vary between inserts
CREATE TABLE mytable
( id SERIAL,
content TEXT,
copyofid integer
generated always as (
case when should_copy_id then id end)
stored,
should_copy_id boolean default false
);
insert into mytable (content) values ('efg') returning *;
-- id | content | copyofid | should_copy_id
------+---------+----------+----------------
-- 1 | efg | | f
--(1 row)
insert into mytable (content,should_copy_id) values ('klm','today'::date<>'2022-10-28'::date) returning *;
-- id | content | copyofid | should_copy_id
------+---------+----------+----------------
-- 2 | klm | 2 | t
--(1 row)
The trigger will be better if
the check is fairly complex - generated columns are pretty limited in terms of the definition complexity. For example, you can't use mutable functions in them - not even STABLE are accepted
you want to save the logic and change it later without having to drop the column each time, then re-add it with a new definition (only way to alter a generated column definition)
as a part of the insert you'll want to do more than just copy the id column
The solution is to create a trigger function which is fired before inserting a new row in table mytable and which copy NEW.id into NEW.copyofid if a condition is true :
CREATE OR REPLACE FUNCTION before_insert_mytable() RETURN trigger LANGUAGE plpgsql AS $$
BEGIN
IF condition
THEN NEW.copyofid = NEW.id ;
END IF ;
RETURN NEW ;
END ; $$
CREATE OR REPLACE TRIGGER before_insert_mytable BEFORE INSERT ON mytable
FOR EACH ROW EXECUTE FUNCTION before_insert_mytable () ;
The condition can also be stated directly in the WHEN clause of the trigger instead of in the function :
CREATE OR REPLACE FUNCTION before_insert_mytable() RETURN trigger LANGUAGE plpgsql AS $$
BEGIN
NEW.copyofid = NEW.id ;
RETURN NEW ;
END ; $$
CREATE OR REPLACE TRIGGER before_insert_mytable BEFORE INSERT ON mytable
WHEN condition
FOR EACH ROW EXECUTE FUNCTION before_insert_mytable () ;
see the manual
I have a view I need to allow my users to update and insert on. Importantly, when they Insert, they need to be able to return the new value from the Inserted row, however right now they get NULL. It must be a view, as in the SELECT of the view, it needs to be able to return values that are the result of a join.
My underlying table definitions:
CREATE TABLE my_assets (
asset_id bigserial not null primary key,
asset_price NUMERIC(32,10) -- This will vary constantly via an independent process
);
CREATE TABLE my_transactions (
id bigserial not null primary key,
asset_id bigint not null REFERENCES my_assets(asset_id),
some_text varchar(100)
);
INSERT INTO my_assets(asset_price) SELECT 100 as asset_price;
My view that shows the result of the table:
CREATE VIEW my_transactions_view AS
SELECT tx.id, tx.asset_id, tx.some_text, a.asset_price
FROM my_transactions tx
JOIN my_assets a ON tx.asset_id = a.asset_id
My trigger that allows insertion into my_transactions_view:
CREATE OR REPLACE FUNCTION trigfx_insert_to_my_transactions_view()
RETURNS trigger AS
$BODY$
BEGIN
INSERT INTO my_transactions(asset_id, some_text)
SELECT NEW.asset_id, NEW.some_text;
RETURN NEW;
END
$BODY$
LANGUAGE 'plpgsql';
CREATE TRIGGER trig_my_transactions_view INSTEAD OF INSERT on my_transactions_view
FOR EACH ROW EXECUTE PROCEDURE trigfx_insert_to_my_transactions_view();
All good so far. However, the problem arises from trying to run the below SQL:
INSERT INTO my_transactions_view(asset_id, some_text)
SELECT 1 as asset_id, 'Hello World' as some_text
RETURNING id, asset_id, some_text;
The returned table returns NULL for ID, but I want to return the newly updated ID from the my_transactions table:
|---------------------|------------------|------------------|
| ID | asset_id | some_text |
|---------------------|------------------|------------------|
| null | 1 | Hello World |
|---------------------|------------------|------------------|
Running a subsequent SELECT * FROM my_transactions_view DOES produce the updated result:
|------------------|------------------|------------------|------------------|
| ID | asset_id | some_text | asset_price |
|------------------|------------------|------------------|------------------|
| 1 | 1 | Hello World | 100.0000000 |
|------------------|------------------|------------------|------------------|
but I need it produced during the RETURNING of the INSERT statement.
Thank you!!!
You can populate the new record with the generated ID:
CREATE OR REPLACE FUNCTION trigfx_insert_to_my_transactions_view()
RETURNS trigger AS
$BODY$
BEGIN
INSERT INTO my_transactions(asset_id, some_text)
values (NEW.asset_id, NEW.some_text);
new.id := lastval(); --<< this gets the generated id from the transactions table
RETURN NEW;
END
$BODY$
LANGUAGE plpgsql;
Online example
Alternatively you could use currval(pg_get_serial_sequence('my_transactions','id')) instead of lastval()
Turns out we can avoid the extra function call via SELECT INTO from a CTE:
CREATE OR REPLACE FUNCTION trigfx_insert_to_my_transactions_view()
RETURNS trigger AS
$BODY$
BEGIN
WITH ins_q as (INSERT INTO my_transactions(asset_id, some_text)
values (NEW.asset_id, NEW.some_text)
RETURNING id, asset_id, some_text)
SELECT ins_q.id, ins_q.asset_id, ins_q.some_text
INTO NEW.id, NEW.asset_id, NEW.some_text
FROM ins_q;
RETURN NEW;
END
$BODY$
LANGUAGE plpgsql;
See online example here.
I was running into initialisation errors (lastval is not yet defined in this session) with the new.id := lastval(); approach.
I have a large number of rows that i want to insert simultaneously into a PostgreSQL database. I need to track what id is assigned for each row that is inserted. For example say we have the table:
CREATE TABLE example
(
id serial,
name text,
CONSTRAINT example_pkey PRIMARY KEY (id),
);
Now i have some data with ids that i dont want inserted (as the serial id column will assign a new id), but i need to keep track of the mapping between the old id and new id:
old id | name
-------------
-1 | foo
-2 | bar
-3 | baz
So i wrote this query
WITH data(oldid,name) AS ( VALUES
(-1,'foo'),
(-2,'bar'),
(-3,'baz')
)
INSERT INTO example(name)
SELECT name FROM data d
RETURNING id, d.oldid
Expecting to get something back like:
id | oldid
-----------
1 | -1
2 | -2
3 | -3
However this doesn't work, as i don't believe you can return a column that wasn't inserted. Is there any alternative way to do this?
I ended up creating a function that wrapped the inserting of a single row:
CREATE OR REPLACE FUNCTION add_example(
in_name text)
RETURNS integer AS
$BODY$
DECLARE
new_id integer;
BEGIN
INSERT INTO example(name)
VALUES (in_name) RETURNING id INTO new_id;
RETURN new_id;
END;
$BODY$
LANGUAGE plpgsql;
Then i can do:
WITH data(oldid, name) AS (VALUES
(-1,'foo'),
(-2,'bar'),
(-3,'baz')
)
SELECT oldid, add_example(name) AS id
FROM data
Which returns what i expect. I'd like to see if this can be done without the function though.
CREATE SEQUENCE data_id_seq;
CREATE TABLE DATA (
id integer default nextval('data_id_seq') NOT NULL PRIMARY KEY,
oldid integer,
name text,
);
INSERT INTO DATA(oldid,name) values (-1,'foo'),(-2,'bar'),(-3,'baz') returning id,oldid;
The optional RETURNING clause causes INSERT to compute and return
value(s) based on each row actually inserted
from https://www.postgresql.org/docs/current/static/sql-insert.html
so column parasite is unavoidable for such solution:
alter table example add column old bigint;
WITH d(oldid,name) AS ( VALUES
(-1,'foo'),
(-2,'bar'),
(-3,'baz')
)
INSERT INTO example(name,old)
SELECT "name", oldid FROM d
RETURNING id, old
Essentially I wish to create a trigger that keeps track and edits the date_created column of a specific row after every insert or update.
These are the columns in my table:
| customer_id | store_id | Quantity | date_created |
the customer_id and store_id together are the primary key of the table
What I have so far:
CREATE OR REPLACE TRIGGER date_trig
BEFORE INSERT ON customer_table
FOR EACH ROW
DECLARE
BEGIN
-- This is where I assume the date will be set or edited
END;
I am brand new to PL/SQL so I am struggling with the actual body of this trigger.
Also, do I have the structure of a trigger correctly formed?
Hi Please find sample code.
create or replace trigger emp_mod_date
before update or insert on emp
for each row
begin
:new.mdate := sysdate;
end;
Use DEFAULT SYSDATE on the colum date_created like already suggested
if you insist to use a trigger, just write :NEW.date_created := SYSDATE;
I want create a table 'product' and have a column date, is it possible that current date will be added when I add some info to table?
If yes please example of this table
create table products (
id number not null,
date number not null
);
Assuming that
Your column is not actually named date since that is a reserved word
Your column is actually defined as a date rather than as a number
You want to populate the column when you insert a new row
you can define a default value for the column.
SQL> ed
Wrote file afiedt.buf
1 create table products (
2 id number not null,
3 dt date default sysdate not null
4* )
SQL> /
Table created.
SQL>
SQL> insert into products( id ) values( 1 );
1 row created.
SQL> select * from products;
ID DT
---------- ---------
1 20-NOV-12
If you want to modify the dt column when you UPDATE the row, you would need a trigger
CREATE OR REPLACE TRIGGER trg_products
BEFORE INSERT OR UPDATE ON products
FOR EACH ROW
BEGIN
:new.dt := sysdate;
END;
A trigger will override any value passed in as part of the INSERT or UPDATE statement for the dt column. A default value will not.