Track last modification timestamp of a row in Postgres - postgresql

In Postgres I want to store table's last update/insert time. Microsoft SQL Server offers a type timestamp which is automatically maintained by the database.
But timestamp in Postgres works differently, it is not updated automatically and the column is always null.

In postgresql, you have to use a trigger. You can follow this link on how to do it https://x-team.com/blog/automatic-timestamps-with-postgresql/ .
To summarize the article you can do the following:
Create the Pl/Pgsql function that will be triggered:
CREATE OR REPLACE FUNCTION trigger_set_timestamp()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = NOW();
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
Create your table
CREATE TABLE mytable (
id SERIAL NOT NULL PRIMARY KEY,
content TEXT,
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
And finally add the trigger:
CREATE TRIGGER set_timestamp
BEFORE UPDATE ON mytable
FOR EACH ROW
EXECUTE FUNCTION trigger_set_timestamp();
You can find more informations about the question here: https://dba.stackexchange.com/questions/58214/getting-last-modification-date-of-a-postgresql-database-table
Hope it'll help you.

Expanding on SofienM's brilliant answer (and lolung's comment correction), we can also extend this by automatically updating a column with the user name (role) of the row creator, and updating another column with the user name of the last user to modify an existing row. Below is a complete test you can run (I use two imaginary users (user1 & user2) to demonstrate the test):
--create dummy table, execute as user1
CREATE TABLE public.test_table (
id SERIAL PRIMARY KEY,
comments VARCHAR(250),
create_by VARCHAR(250) DEFAULT user,
create_dat TIMESTAMPTZ DEFAULT now(),
modify_by VARCHAR(250) DEFAULT user,
modify_dat TIMESTAMPTZ DEFAULT NOW()
);
--create function trigger to change a timestamp value upon an update
CREATE OR REPLACE FUNCTION trigger_set_timestamp()
RETURNS TRIGGER AS $$
BEGIN
NEW.modify_dat = NOW();
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
--create a trigger to execute the function
CREATE TRIGGER set_timestamp
BEFORE UPDATE ON public.test_table
FOR EACH ROW
EXECUTE PROCEDURE trigger_set_timestamp();
--test the created by and created date columns, execute this as user1
INSERT INTO public.test_table (comments)
VALUES ('hello world'),
('hello system'),
('hello universe')
;
--wait a minute or two, execute this query as user1 and observe 'modify_dat' column
UPDATE public.test_table
SET comments = 'hello secret'
WHERE comments = 'hello world';
--create function trigger to change a username value upon an update
CREATE OR REPLACE FUNCTION trigger_set_usertimestamp()
RETURNS TRIGGER AS $$
BEGIN
NEW.modify_by = user;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
--create a trigger to execute the function
CREATE TRIGGER set_usertimestamp
BEFORE UPDATE ON public.test_table
FOR EACH ROW
EXECUTE PROCEDURE trigger_set_usertimestamp();
--test the created by and created date columns, execute this as user2 for variation
INSERT INTO public.test_table (comments)
VALUES ('goodbye world'),
('goodbye system'),
('goodbye universe')
;
--wait a minute or two, execute this query as user2 and observe 'modify_by' column
UPDATE public.test_table
SET comments = 'hello unknown'
WHERE comments = 'hello system';
What would improve this is to make the trigger apply to any/every table in a given schema, not just on one specific table (assuming each table has the 'modify_dat' and 'modify_by' columns of course...).

Related

Postgres update column value to time.Now()

I need to update column paid_at to time.Now() on table orders when column status is updated to paid is it possible to create a trigger on postgres? they are all in a same table orders
is it possible to create a trigger on postgres
Yes, that's possible.
As documented in the manual you need a trigger function first:
create function update_paid_at()
returns trigger
as
$$
begin
new.paid_at := now();
return new;
end;
$$
language plpglsql;
Then you need a trigger definition:
create trigger update_orders_trigger
before update on orders
for each row
when (new.status = 'paid' and new.status <> old.status)
execute procedure update_paid_at();
The trigger only fires if the status is changed to 'paid' and then sets the value of paid_at column.

Having multiple trigger events when redirecting insertions to partition tables

I am trying to set up triggers for insert and update events for the master table of some partition tables in PostgreSQL. Each time an insertion is made into the master table, the insert trigger event will redirect it into the correct partition table. Consequently, I will need to return NULL from this function call, since I don't want the master table to be populated as well. If the master table receives an update event, it will update a timestamp before making the change in the table. The problem is that the update trigger is never fired. I am using PostgreSQL version 9.6.
I have tried to combine the trigger functions into one, and merged the called trigger procedures into one as well, but the results are the same. The update trigger is only triggered if I return NEW from the insertion trigger function (which populates the master table), or if I comment out the insertion trigger function altogether.
DROP SCHEMA IF EXISTS test CASCADE;
CREATE SCHEMA test;
SET SCHEMA 'test';
CREATE TYPE test_type AS ENUM ('unit', 'performance');
CREATE TABLE test (
type test_type NOT NULL,
score INTEGER NOT NULL CHECK (score > 0),
id SERIAL PRIMARY KEY,
updated_at TIMESTAMP DEFAULT current_timestamp
);
CREATE TABLE performance_test (
CHECK (type = 'performance')
) INHERITS (test);
CREATE FUNCTION insert_test()
RETURNS trigger AS
$$
BEGIN
INSERT INTO performance_test VALUES (NEW.*);
RETURN NULL;
END;
$$ LANGUAGE plpgsql;
CREATE FUNCTION update_timestamp()
RETURNS trigger AS
$$
BEGIN
RAISE NOTICE 'This is never reached.';
UPDATE performance_test
SET updated_at = current_timestamp
WHERE id = NEW.id;
RETURN NULL;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER test_insertion BEFORE INSERT ON test
FOR EACH ROW EXECUTE PROCEDURE insert_test();
CREATE TRIGGER test_update BEFORE UPDATE ON test
FOR EACH ROW EXECUTE PROCEDURE update_timestamp();
---------------------------------------------------------------------------
INSERT INTO test VALUES ('performance', 10);
SELECT * FROM performance_test;
UPDATE test SET score = 20 WHERE id = 1;
SELECT * FROM performance_test;
I am not sure if it is possible to achieve what I want with this method, so I'm reaching out here for any advice. Thanks in advance!
/ Hampus
Row triggers must be defined on individual partitions, not the partitioned table. See https://www.postgresql.org/docs/10/ddl-partitioning.html#DDL-PARTITIONING-DECLARATIVE-LIMITATIONS
I don't know why the documentation for 9.6 doesn't mention this
working update trigger:
CREATE FUNCTION update_timestamp()
RETURNS trigger AS
$$
BEGIN
NEW.updated_at = now();
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER test_update BEFORE UPDATE ON performance_test
FOR EACH ROW EXECUTE PROCEDURE update_timestamp();
if you do UPDATE test SET score = 30, updated_at=DEFAULT; or UPDATE test SET score = 30, updated_at=current_timestamp; you might not need the update trigger.
Partitioning is not a free lunch because it has non-obvious effects on both behavior and performance, as you noticed by the trigger not behaving as you expected. If you make a mistake it can easily lead to failing queries and even bad data.
If you are really sure you need it you should make sure you understand it in detail and otherwise I'd recommend you to avoid it, most issues with slow queries can be solved by making sure the table statistics is up to date, using the right indexes, optimizing queries, changing Postgres configuration or adding more hardware.

sync two tables after insert

I am using postgresql. I have two schemas main and sec containing only one table datastore with the same structure (this is only an extract)
I am trying unsucessfully to create a trigger for keep sync both tables when insert occurs in one of them. The problem is some kind of circular or recursive reference.
Can you create some example for solve this?
I am working on this, I'll post my solution later.
You can use this code as reference for creating schemas and tables
CREATE SCHEMA main;
CREATE SCHEMA sec;
SET search_path = main, pg_catalog;
CREATE TABLE datastore (
fullname character varying,
age integer
);
SET search_path = sec, pg_catalog;
CREATE TABLE datastore (
fullname character varying,
age integer
);
An updatable view is the best solution and is as simple as (Postgres 9.3+):
drop table sec.datastore;
create view sec.datastore
as select * from main.datastore;
However, if you cannot do it for some inscrutable reasons, use pg_trigger_depth() function (Postgres 9.2+) to ensure that the trigger function is not executed during replication. The trigger on main.datastore may look like this:
create or replace function main.datastore_insert_trigger()
returns trigger language plpgsql as $$
begin
insert into sec.datastore
select new.fullname, new.age;
return new;
end $$;
create trigger datastore_insert_trigger
before insert on main.datastore
for each row when (pg_trigger_depth() = 0)
execute procedure main.datastore_insert_trigger();
The trigger on sec.datastore should be defined analogously.
create OR REPLACE function copytosec() RETURNS TRIGGER AS $$
BEGIN
insert into sec.datastore(fullname,age) values (NEW.fullname,NEW.age);
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
create trigger copytosectrigger after insert on public.datastore
for each row
execute procedure copytosec();`

Triger update function when column updates in Postgres

I have users table with columns status_id (int), additional_status(int) and status_changed(DATE).
I want to autoupdate status_changed field every time when status_id or additional_status changes.
Here is what I have by now:
CREATE OR REPLACE FUNCTION update_status_changed()
RETURNS TRIGGER
AS $$
BEGIN
NEW.status_changed := CURRENT_TIMESTAMP;
RETURN NEW;
END $$ LANGUAGE plpgsql;
CREATE TRIGGER set_update_status_changed
AFTER INSERT OR UPDATE OF status_id, additional_status ON users
FOR EACH ROW
EXECUTE PROCEDURE update_status_changed();
I'm not sure if the syntax is correct. When I change my status_id from phpPgAdmin - status_changed stays NULL. What am I'm missing?, help pls.

Capture columns in plpgsql during UPDATE

I am writing a trigger in plpgsql for Postgres 9.1. I need to be able to capture the column names that were issued in the SET clause of an UPDATE so I can record the specified action in an audit table. The examples in the Postgres documentation are simple and inadequate for my needs. I have searched the internet for days and I am unable to find any other examples that try to achieve what I want to do here.
I am on a tight schedule to resolve this soon. I don't know Tcl so pl/Tcl is out of the question for me at this point. pl/Perl may work but I don't know where to start with it. Also I wanted to find a way to accomplish this in pl/pgsql if at all possible for portability and maintenance. If someone can recommend a pl/Perl solution to this I would be grateful.
Here is the table structure of the target table that will be audited:
Note: There are many other columns in the record table but I have not listed them here in order to keep things simple. But the trigger should be able to record changes to any of the columns in the row.
CREATE TABLE record (
record_id integer NOT NULL PRIMARY KEY,
lastname text,
frstname text,
dob date,
created timestamp default NOW(),
created_by integer,
inactive boolean default false
);
create sequence record_record_id_seq;
alter table record alter record_id set default nextval('record_record_id_seq');
Here is my audit table:
CREATE TABLE record_audit (
id integer NOT NULL PRIMARY KEY,
operation char(1) NOT NULL, -- U, I or D
source_column text,
source_id integer,
old_value text,
new_value text,
created_date timestamp default now(),
created_by integer
);
create sequence record_audit_id_seq;
alter table record_audit alter id set default nextval('record_audit_id_seq');
My goal is to record INSERTS and UPDATES to the record table in the record_audit table that will detail not only what the target record_id was (source_id) that was updated and what column was updated (source_column), but also the old_value and the new_value of the column.
I understand that the column values will have to be CAST() to a type of text. I believe I can access the old_value and new_value by accessing NEW and OLD but I am having difficulty figuring out how to obtain the column names used in the SET clause of the UPDATE query. I need the trigger to add a new record to the record_audit table for every column specified in the SET clause. Note, there are not DELETE actions as records are simply UPDATED to inactive = 't' (and thus recorded in the audit table)
Here is my trigger so far (obviously incomplete). Please forgive me, I am learning pl/pgsql as I go.
-- Trigger function for record_audit table
CREATE OR REPLACE FUNCTION audit_record() RETURNS TRIGER AS $$
DECLARE
insert_table text;
ref_col text; --how to get the referenced column name??
BEGIN
--
-- Create a new row in record_audit depending on the operation (TG_OP)
--
IF (TG_OP = 'INSERT') THEN
-- old_value and new_value are meaningless for INSERTs. Just record the new ID.
INSERT INTO record_audit
(operation,source_id,created_by)
VALUES
('I', NEW.record_id, NEW.created_by);
ELSIF (TG_OP = 'UPDATE') THEN
FOR i in 1 .. TG_ARGV[0] LOOP
ref_col := TG_ARGV[i].column; -- I know .column doesn't exist but what to use?
INSERT INTO record_audit
(operation, source_column, source_id, old_value, new_value, created_by)
VALUES
('U', ref_col, NEW.record_id, OLD.ref_col, NEW.ref_col, NEW.created_by);
END LOOP;
END IF;
RETURN NULL; -- result is ignored anyway since this is an AFTER trigger
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER record_audit_trig
AFTER INSERT OR UPDATE on record
FOR EACH ROW EXECUTE PROCEDURE audit_record();
Thanks for reading this long and winding question!
you cannot to get this information - not in PL level - probably it is possible in C.
Good enough solution is based on changed fields in records NEW and OLD. You can get list of fields from system tables ~ are related to table that is joined to trigger.