postgres trigger creates index: BEFORE INSERT ON hides one row - postgresql

I have a trigger AFTER INSERT ON mytable that calls a function
CREATE OR REPLACE FUNCTION myfunction() RETURNS trigger AS
$BODY$
DECLARE
index TEXT;
BEGIN
index := 'myIndex_' || NEW.id2::text;
IF to_regclass(index::cstring) IS NULL THEN
EXECUTE 'CREATE INDEX ' || index || ' ON mytable(id) WITH (FILLFACTOR=100) WHERE id2=' || NEW.id2|| ';';
RAISE NOTICE 'Created new index %',index;
END IF;
RETURN NULL;
END;
$BODY$
LANGUAGE plpgsql VOLATILE
SECURITY DEFINER
COST 100;
ALTER FUNCTION myfunction()
OWNER TO theadmin;
This works wonderfully. For each distinct id2 I create an index. Speeds up relevant queries by a lot.
As mentioned above I trigger this AFTER INSERT ON. Before doing that however I had the trigger set to BEFORE INSERT ON. And the function did some strange things. (Yes, I had changed the RETURN NULL to RETURN NEW)
insert of a new row insert into mytable VALUES(1391, 868, 0.5, 0.5);
creates the corresponding index myIndex_868
the inserted row does not appear in mytable when doing a select :(
trying to insert the same row results in ERROR: duplicate key value violates unique constraint "mytable_pkey" because of course DETAIL: Key (id, id2)=(1391, 868) already exists.
inserting other rows for the same id2 works as expected :)
DELETE FROM mytable WHERE id = 1391 and id2 = 868 does nothing
DROP INDEX myIndex_868; drops the index. And suddenly the initial row that never appeared in the table is suddenly there!
Why does BEFORE INSERT ON behave so differently? Is this a bug in postgres 9.4 or did I overlook something?
Just for completeness' sake:
CREATE TRIGGER mytrigger
AFTER INSERT ON mytable
FOR EACH ROW EXECUTE PROCEDURE myfunction();
vs.
CREATE TRIGGER mytrigger
BEFORE INSERT ON mytable
FOR EACH ROW EXECUTE PROCEDURE myfunction();

I'd argue that this is a bug in PostgreSQL. I could reproduce it with 9.6.
It is clear that the row is not contained in the index as it is created in the BEFORE trigger, but the fact that the index is not updated when the row is inserted is a bug in my opinion.
I have written to pgsql-hackers to ask for an opinion.
But apart from that, I don't see the point of the whole exercise.
Better than creating a gazillion indexes would be to create a single one:
CREATE INDEX ON mytable(id2, id);

Related

How to DELETE/INSERT rows in the same table using a UPDATE Trigger?

I want to create a trigger function, which copies certain columns of an recent updated row and deletes the old data. After that I want to insert the copied columns in exact the same table in the same row (overwrite). I need the data to be INSERTED because this function will be embedded in an existing program, with predefined Triggers.
That's what I have so far:
CREATE OR REPLACE FUNCTION update_table()
RETURNS TRIGGER AS
$func$
BEGIN
WITH tmp AS (DELETE FROM table
WHERE table.id = NEW.id
RETURNING id, geom )
INSERT INTO table (id, geom) SELECT * FROM tmp;
END;
$func$ language plpgsql;
CREATE TRIGGER T_update
AFTER UPDATE OF geom ON table
EXECUTE PROCEDURE update_table();
But I get the Error message:
ERROR: cannot perform DELETE RETURNING on relation "table"
HINT: You need an unconditional ON DELETE DO INSTEAD rule with a RETURNING clause.
Why I should use a rule here?
I'm using PostgreSQL 9.6
UPDATE:
A little bit of clarification. When I have two columns in my table (id, geom), after I updated geom I want to make a copy of this (new)row and insert it into the same table, while overwriting the updated row. (I'm not interested in any value before the update) I know that this is odd but I need this row to be inserted again because the program i embed this function in, listens to a INSERT statement and cannot be changed by me.
Right after you update a row, its old values will no longer be available. So, if you simply want to preserve the old row in case of an update you need to create a BEFORE UPDATE trigger, so that you can still access the OLD values and create a new row, e.g.
CREATE TABLE t (id int, geom geometry(point,4326));
CREATE OR REPLACE FUNCTION update_table() RETURNS TRIGGER AS $$
BEGIN
INSERT INTO t (id, geom) VALUES (OLD.id,OLD.geom);
RETURN NEW;
END; $$ LANGUAGE plpgsql;
CREATE TRIGGER t_update
BEFORE UPDATE OF geom ON t FOR EACH ROW EXECUTE PROCEDURE update_table();
INSERT INTO t VALUES (1,'SRID=4326;POINT(1 1)');
If you update the record 1 ..
UPDATE t SET geom = 'SRID=4326;POINT(2 2)', id = 2 WHERE id = 1;
UPDATE t SET geom = 'SRID=4326;POINT(3 3)', id = 3 WHERE id = 2;
.. you get a new record in the same table as you wished
SELECT id, ST_AsText(geom) FROM t;
id | st_astext
----+------------
1 | POINT(1 1)
2 | POINT(2 2)
3 | POINT(3 3)
Demo: db<>fiddle
Unrelated note: consider upgrading your PostgreSQL version! 9.6 will reach EOL in November, 2021.
First thanks to #JimJones for the answer. I´d like to post his answer modified for this purpose. This code "overwrites" the updated row by inserting a copy of itself and then deleting the old duplicate. That way I can Trigger on INSERT.
CREATE TABLE t (Unique_id SERIAL,id int, geom geometry(point,4326));
CREATE OR REPLACE FUNCTION update_table() RETURNS TRIGGER AS $$
BEGIN
INSERT INTO t (id, geom) VALUES (NEW.id,NEW.geom);
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER t_update
BEFORE UPDATE OF geom ON t FOR EACH ROW EXECUTE PROCEDURE update_table();
CREATE OR REPLACE FUNCTION delete_table() RETURNS TRIGGER AS $$
BEGIN
DELETE FROM t a
USING t b
WHERE a.Unique_id < b.Unique_id
AND a.geom = b.geom;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER t_delete
AFTER UPDATE OF geom ON t FOR EACH ROW EXECUTE PROCEDURE delete_table();
INSERT INTO t VALUES (1,1,'SRID=4326;POINT(1 1)');
UPDATE t SET geom = 'SRID=4326;POINT(2 2)' WHERE id = 1;

Using rules in postgresql to insert several rows

I need to insert several rows using a rule.
My problem is:
I draw a poligon and save on a table z0_poligono_corte, then I use a rule to get the intersection between the polygon in z0_poligono_corte and polygons in other table, but the result is not inserted in the table I created to save the results. Also there are no errors in the postgres log.
CREATE RULE cortar_anp AS ON INSERT TO z0_poligono_corte
DO also insert into z0_a_n_p_13 (id_poligono, folio, obs, anp, geometria) select pol.id, pol.folio, pol.obs, capa.anp, ST_Multi(ST_Intersection(capa.geom, pol.geometria))
from a_n_p_13 capa, z0_poligono_corte pol
where pol.id=NEW.id
and capa.geom && pol.geometria
and ST_IsEmpty(ST_Intersection(capa.geom, pol.geometria)) != TRUE;
I don't know if my rule is correct. If I run the insert into sentence using for example pol.id=1 then the result saves correctly.
image
I have not 10 reputation to put the image :(
Here the image
Thanks.
Here is how solved my problem with a trigger.
First i created a table
CREATE TABLE public.z0_poligono_corte (
id serial,
nombre character varying(255),
giro text,
geometria geometry(MultiPolygon,32614),
CONSTRAINT z0_poligono_corte_pkey PRIMARY KEY (id) )
After that I created a trigger function that makes the slice
CREATE OR REPLACE FUNCTION public.cortes_interseccion_amias()
RETURNS trigger AS
$BODY$
declare var_sql text;
begin
var_sql := 'insert into z0_amias08 (id_poligono, nombre, giro, tipo, geometria)
select pol.id, pol.nombre, pol.giro, capa.tipo, ST_Intersection(capa.geom, pol.geometria)
from amias08 capa, z0_poligono_corte pol
where pol.id=$1
and capa.geom && pol.geometria
and ST_IsEmpty(ST_Intersection(capa.geom, pol.geometria)) != TRUE';
execute var_sql using NEW.id;
return NEW;
end;
$BODY$
LANGUAGE plpgsql VOLATILE
COST 100;
Then the trigger.
CREATE TRIGGER interseccion_amias
AFTER INSERT
ON public.z0_poligono_corte
FOR EACH ROW
EXECUTE PROCEDURE public.cortes_interseccion_amias();
So when I insert a polygon on that table, the trigger function will cut on the table with data, then insert on the new table

FOR EACH STATEMENT trigger example

I've been looking at the documentation of postgresql triggers, but it seems to only show examples for row-level triggers, but I can't find an example for a statement-level trigger.
In particular, it is not quite clear how to iterate in the update/inserted rows in a single statement, since NEW is for a single record.
OLD and NEW are null or not defined in a statement-level trigger. Per documentation:
NEW
Data type RECORD; variable holding the new database row for INSERT/UPDATE operations in row-level triggers. This variable is
null in statement-level triggers and for DELETE operations.
OLD
Data type RECORD; variable holding the old database row for UPDATE/DELETE operations in row-level triggers. This variable is null in statement-level triggers and for INSERT operations.
Bold emphasis mine.
Up to Postgres 10 this read slightly different, much to the same effect, though:
... This variable is unassigned in statement-level triggers. ...
While those record variables are still of no use for statement level triggers, a new feature very much is:
Transition tables in Postgres 10+
Postgres 10 introduced transition tables. Those allow access to the whole set of affected rows. The manual:
AFTER triggers can also make use of transition tables to inspect the entire set of rows changed by the triggering statement.
The CREATE TRIGGER command assigns names to one or both transition
tables, and then the function can refer to those names as though they
were read-only temporary tables. Example 43.7 shows an example.
Follow the link to the manual for code examples.
Example statement-level trigger without transition tables
Before the advent of transition tables, those were even less common. A useful example is to send notifications after certain DML commands.
Here is a basic version of what I use:
-- Generic trigger function, can be used for multiple triggers:
CREATE OR REPLACE FUNCTION trg_notify_after()
RETURNS trigger
LANGUAGE plpgsql AS
$func$
BEGIN
PERFORM pg_notify(TG_TABLE_NAME, TG_OP);
RETURN NULL;
END
$func$;
-- Trigger
CREATE TRIGGER notify_after
AFTER INSERT OR UPDATE OR DELETE ON my_tbl
FOR EACH STATEMENT
EXECUTE PROCEDURE trg_notify_after();
For Postgres 11 or later use the equivalent, less confusing syntax:
...
EXECUTE FUNCTION trg_notify_after();
See:
Trigger function does not exist, but I am pretty sure it does
Well, here are some examples of statement-level triggers.
Table:
CREATE TABLE public.test (
number integer NOT NULL,
text character varying(50)
);
Trigger function:
OLD and NEW are still NULL
The return value can also be always left NULL.
CREATE OR REPLACE FUNCTION public.tr_test_for_each_statement()
RETURNS trigger
LANGUAGE plpgsql
AS
$$
DECLARE
x_rec record;
BEGIN
raise notice '=operation: % =', TG_OP;
IF (TG_OP = 'UPDATE' OR TG_OP = 'DELETE') THEN
FOR x_rec IN SELECT * FROM old_table LOOP
raise notice 'OLD: %', x_rec;
END loop;
END IF;
IF (TG_OP = 'INSERT' OR TG_OP = 'UPDATE') THEN
FOR x_rec IN SELECT * FROM new_table LOOP
raise notice 'NEW: %', x_rec;
END loop;
END IF;
RETURN NULL;
END;
$$;
Settings statement-level triggers
Only AFTER and only one event is supported.
CREATE TRIGGER tr_test_for_each_statement_insert
AFTER INSERT ON public.test
REFERENCING NEW TABLE AS new_table
FOR EACH STATEMENT
EXECUTE PROCEDURE public.tr_test_for_each_statement();
CREATE TRIGGER tr_test_for_each_statement_update
AFTER UPDATE ON public.test
REFERENCING NEW TABLE AS new_table OLD TABLE AS old_table
FOR EACH STATEMENT
EXECUTE PROCEDURE public.tr_test_for_each_statement();
CREATE TRIGGER tr_test_for_each_statement_delete
AFTER DELETE ON public.test
REFERENCING OLD TABLE AS old_table
FOR EACH STATEMENT
EXECUTE PROCEDURE public.tr_test_for_each_statement();
Examples:
INSERT INTO public.test(number, text) VALUES (1, 'a');
=operation: INSERT =
NEW: (1,a)
INSERT INTO public.test(number, text) VALUES (2, 'b'), (3, 'b');
=operation: INSERT =
NEW: (2,b)
NEW: (3,b)
UPDATE public.test SET number = number + 1 WHERE text = 'a';
=operation: UPDATE =
OLD: (1,a)
NEW: (2,a)
UPDATE public.test SET number = number + 10 WHERE text = 'b';
=operation: UPDATE =
OLD: (2,b)
OLD: (3,b)
NEW: (12,b)
NEW: (13,b)
DELETE FROM public.test;
=operation: DELETE =
OLD: (2,a)
OLD: (12,b)
OLD: (13,b)

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.

Postgresql, update if row with some unique value exists, else insert

I have a URLs table. They contain
(id int primary key,
url character varying unique,
content character varying,
last analyzed date).
I want to create trigger or something(rule may be), so each time i make insert from my java program, it updates some single row if row with such URL exists. Else it should perform an Insert.
Please, can you provide a complete code in Postgresql. Thanks.
This has been asked many times. A possible solution can be found here:
https://stackoverflow.com/a/6527838/552671
This solution requires both an UPDATE and INSERT.
UPDATE table SET field='C', field2='Z' WHERE id=3;
INSERT INTO table (id, field, field2)
SELECT 3, 'C', 'Z'
WHERE NOT EXISTS (SELECT 1 FROM table WHERE id=3);
With Postgres 9.1 it is possible to do it with one query:
https://stackoverflow.com/a/1109198/2873507
If INSERTS are rare, I would avoid doing a NOT EXISTS (...) since it emits a SELECT on all updates. Instead, take a look at wildpeaks answer: https://dba.stackexchange.com/questions/5815/how-can-i-insert-if-key-not-exist-with-postgresql
CREATE OR REPLACE FUNCTION upsert_tableName(arg1 type, arg2 type) RETURNS VOID AS $$
DECLARE
BEGIN
UPDATE tableName SET col1 = value WHERE colX = arg1 and colY = arg2;
IF NOT FOUND THEN
INSERT INTO tableName values (value, arg1, arg2);
END IF;
END;
$$ LANGUAGE 'plpgsql';
This way Postgres will initially try to do a UPDATE. If no rows was affected, it will fall back to emitting an INSERT.
I found this post more relevant in this scenario:
WITH upsert AS (
UPDATE spider_count SET tally=tally+1
WHERE date='today' AND spider='Googlebot'
RETURNING *
)
INSERT INTO spider_count (spider, tally)
SELECT 'Googlebot', 1
WHERE NOT EXISTS (SELECT * FROM upsert)
Firstly It tries insert. If there is a conflict on url column then it updates content and last_analyzed fields. If updates are rare this might be better option.
INSERT INTO URLs (url, content, last_analyzed)
VALUES
(
%(url)s,
%(content)s,
NOW()
)
ON CONFLICT (url)
DO
UPDATE
SET content=%(content)s, last_analyzed = NOW();
create table urls (
url_id serial primary key,
url text unique,
content text,
last_analyzed timestamptz);
insert into urls(url) values('hello'),
('How'),('are'),
('you'),('doing');
By creating procedure, you also also do upsert.
CREATE OR REPLACE PROCEDURE upsert_url(_url text) LANGUAGE plpgsql
as $$
BEGIN
INSERT INTO URLs (url) values (_url)
ON CONFLICT (url)
DO UPDATE SET last_analyzed = NOW();
END
$$;
Test it through call the procedure.
call upsert_url('I am is ok');
call upsert_url('hello');