How insert rows with max(order_field) + 1 transactionally in PostgreSQL - sql

I need to insert in a PostgreSQL table a row with a column containing the max value + 1 for this same column on a subset of the rows of the table. That column is used to ordering the rows in that subset.
I´m trying to update the column value in an after insert trigger but I´m obtaining duplicate values for this column in different rows.
What´s the best way to do that avoiding duplicate values for the ordering column in the subset in a concurrent environment with a lot of inserts in a short time?
Thanks in advance
EDIT:
The subset is defined by another column of the same table: this column has the same value for all the related rows.

If that column is used only for ordering then use a sequence:
create table t (
column1 integer,
ordering_column serial
);
http://www.postgresql.org/docs/current/static/datatype-numeric.html#DATATYPE-NUMERIC-TABLE

New transactional-safe answer:
To make it in a transactional-safe way you could use this trigger, which creates sequences for each different "set_id" value:
create or replace function calculate_index() returns trigger
as $$
declare my_indexer_name text;
begin
my_indexer_name = 'my_indexer_name_' || NEW.my_set_id;
if NOT EXISTS (SELECT * FROM pg_class WHERE relname = my_indexer_name)
then
execute 'create sequence ' || my_indexer_name;
end if;
select nextval(my_indexer_name) into NEW.my_index;
return new;
end
$$
language plpgsql;
CREATE TRIGGER my_indexer_trigger
BEFORE INSERT ON my_table FOR EACH ROW
EXECUTE PROCEDURE calculate_index();
Also you could create manually sequences named 'my_indexer_name_1', 'my_indexer_name_2', etc. if your set_id possible values are known beforehand, then you could eliminate the if-then from the trigger function above.
This was my initial and not transactional-safe answer:
I would create a new helper table let's call it set_indexes:
create table set_indexes( set_id integer, max_index integer );
each record has the set_id and the max index value of that set. e.g.:
set_id, max_index
1 53
2 12
3 43
in the trigger code you would:
select max_index + 1 from set_indexes where set_indexes.set_id = NEW.my_set_id
into NEW.my_index;
// Chek if the set_id is new:
if NEW.my_index is null then
insert into set_indexes( set_id, max_index) values (NEW.my_set_id, 1);
NEW.my_index = 0;
else
update set_indexes set max_index = NEW.my_index where set_indexes.set_id = NEW.my_set_id;
end if;

Related

Postgresql function (upsert and delete): how to pass a set of rows of table type to function call

I have a table
CREATE TABLE items(
id SERIAL PRIMARY KEY,
group_id INT NOT NULL,
item_id INT NOT NULL,
name TEXT,
.....
.....
);
I am creating a function that
takes set of row values for a single group_id, fail if multiple group_ids present in in input rows
compares it with matching values in the table (only for that group_id
updates changed values (only for the input group_id)
inserts new values
deletes table rows that are absent in the row input (compare rows with group_id and item_id)(only for the input group_id)
this is my function definition
CREATE OR REPLACE FUNCTION update_items(rows_input items[]) RETURNS boolean as $$
DECLARE
rows items[];
group_id_input integer;
BEGIN
-- get single group_id from input rows, fail if multiple group_id's present in input
-- read items of that group_id in table
-- compare input rows and table rows (of the same group_id)
-- create transaction
-- delete absent rows
-- upsert
-- return success of transaction (boolean)
END;
$$ LANGUAGE plpgsql;
I am trying to call the function in a query
select update_items(
(38,1,1283,"Name1"),
(39,1,1471,"Name2"),
(40,1,1333,"Name3")
);
I get the following error
Failed to run sql query: column "Name1" does not exist
I tried removing the id column values: that gives me the same error
What is the correct way to pass row values to a function that accepts table type array as arguments?
updates changed values
inserts new values deletes table rows that are
absent in the row input (compare rows with group_id and item_id)
If you want do upsert, you must upsert with unique constraint.
So there is two unique constraints. primary key(id), (group_id, item_id).
insert on conflict need consider these two unique constraint.
Since You want pass items[] type to the functions. So it also means that any id that is not in the input function arguments will also be deleted.
drop table if exists items cascade;
begin;
CREATE TABLE items(
id bigint GENERATED BY DEFAULT as identity PRIMARY KEY,
group_id INT NOT NULL,
item_id INT NOT NULL,
name TEXT
,unique(group_id,item_id)
);
insert into items values
(38,1,1283,'original_38'),
(39,1,1471,'original_39'),
(40,1,1333,'original_40'),
(42,1,1332,'original_42');
end;
main function:
CREATE OR REPLACE FUNCTION update_items (in_items items[])
RETURNS boolean
AS $FUNC$
DECLARE
iter items;
saved_ids bigint[];
BEGIN
saved_ids := (SELECT ARRAY (SELECT (unnest(in_items)).id));
DELETE FROM items
WHERE NOT (id = ANY (saved_ids));
FOREACH iter IN ARRAY in_items LOOP
INSERT INTO items
SELECT
iter.*
ON CONFLICT (id)
DO NOTHING;
INSERT INTO items
SELECT
iter.*
ON CONFLICT (group_id,
item_id)
DO UPDATE SET
name = EXCLUDED.name;
RAISE NOTICE 'rec.groupid: %, rec.items_id:%', iter.group_id, iter.item_id;
END LOOP;
RETURN TRUE;
END
$FUNC$
LANGUAGE plpgsql;
call it:
SELECT
*
FROM
update_items ('{"(38, 1, 1283, Name1) "," (39, 1, 1471, Name2) "," (40, 1, 1333, Name3)"}'::items[]);
references:
Iterating over integer[] in PL/pgSQL
How to match elements in an array of composite type?
IN vs ANY operator in PostgreSQL
Here's how I achieved UPSERT with DELETE missing rows, if anyone is looking to do the same.
CREATE OR REPLACE FUNCTION update_items(in_rows items[]) RETURNS INT AS $$
DECLARE
in_groups INTEGER[];
in_group_id INTEGER;
in_item_ids INTEGER[];
BEGIN
-- get single group id from input rows, fail if multiple group ids present in input
in_groups = (SELECT ARRAY (SELECT distinct(group_id) FROM UNNEST(in_rows)));
IF ARRAY_LENGTH(in_groups,1)>1 THEN
RAISE EXCEPTION 'Multiple group_ids found in input items: %', in_groups;
END IF;
in_group_id = in_groups[1];
-- delete items of this group that are absent in in_rows
in_item_ids := (SELECT ARRAY (SELECT (UNNEST(in_rows)).item_id));
DELETE FROM items
WHERE
master_code <> ANY (in_item_ids)
AND group_id = in_group_id;
-- upsert in_rows
INSERT INTO items
SELECT * FROM UNNEST(in_rows)
ON CONFLICT (group_id,item_d)
DO UPDATE SET
parent_group_id = EXCLUDED.parent_group_id,
mat_centre_id = EXCLUDED.mat_centre_id,
NAME = EXCLUDED.NAME,
opening_date = EXCLUDED.opening_date;
RETURN in_group_id;
-- return success of transaction (boolean)
END;
$$ LANGUAGE plpgsql;
This function removes rows that are missing from your in_rows

Is SELECT "faster" than function with nested INSERT?

I'm using a function that inserts a row to a table if it doesn't exist, then returns the id of the row.
Whenever I put the function inside a SELECT statement, with values that don't exist in the table yet, e.g.:
SELECT * FROM table WHERE id = function(123);
... it returns an empty row. However, running it again with the same values will return the row with the values I want to see.
Why does this happen? Is the INSERT running behind the SELECT speed? Or does PostgreSQL cache the table when it didn't exist, and at next run, it displays the result?
Here's a ready to use example of how this issue can occur:
CREATE TABLE IF NOT EXISTS test_table(
id INTEGER,
tvalue boolean
);
CREATE OR REPLACE FUNCTION test_function(user_id INTEGER)
RETURNS integer
LANGUAGE 'plpgsql'
AS $$
DECLARE
__user_id INTEGER;
BEGIN
EXECUTE format('SELECT * FROM test_table WHERE id = $1')
USING user_id
INTO __user_id;
IF __user_id IS NOT NULL THEN
RETURN __user_id;
ELSE
INSERT INTO test_table(id, tvalue)
VALUES (user_id, TRUE)
RETURNING id
INTO __user_id;
RETURN __user_id;
END IF;
END;
$$;
Call:
SELECT * FROM test_table WHERE id = test_function(4);
To reproduce the issue, pass any integer that doesn't exist in the table, yet.
The example is broken in multiple places.
No need for dynamic SQL with EXECUTE.
SELECT * in the function is wrong.
Your table definition should have a UNIQUE or PRIMARY KEY constraint on (id).
Most importantly, the final SELECT statement is bound to fail. Since the function is VOLATILE (has to be), it is evaluated once for every existing row in the table. Even if that worked, it would be a performance nightmare. But it does not. Like #user2864740 commented, there is also a problem with visibility. Postgres checks every existing row against the result of the function, which in turn adds 1 or more rows, and those rows are not yet in the snapshot the SELECT is operating on.
SELECT * FROM test_table WHERE id = test_function(4);
This would work (but see below!):
CREATE TABLE test_table (
id int PRIMARY KEY --!
, tvalue bool
);
CREATE OR REPLACE FUNCTION test_function(_user_id int)
RETURNS test_table LANGUAGE sql AS
$func$
WITH ins AS (
INSERT INTO test_table(id, tvalue)
VALUES (_user_id, TRUE)
ON CONFLICT DO NOTHING
RETURNING *
)
TABLE ins
UNION ALL
SELECT * FROM test_table WHERE id = _user_id
LIMIT 1
$func$;
And replace your SELECT with just:
SELECT * FROM test_function(1);
db<>fiddle here
Related:
Return a value if no record is found
How to use RETURNING with ON CONFLICT in PostgreSQL?
There is still a race condition for concurrent calls. If that can happen, consider:
Is SELECT or INSERT in a function prone to race conditions?

PostgreSQL - How to keep a column updated

I'm new to SQL and I'm trying to update a column (ex_counter) of a table (ex_table). This column consists of a counter of the number of times an ID (ex_id) appears on a second table (ex2_id in ex2_table).
An ID can be inserted into the second table at any moment. If that ID is already existing, the counter of its corresponding ID in the first table must be updated (by simply adding 1, I guess).
These are the two tables:
CREATE TABLE ex_table(
ex_id SMALLINT,
ex_counter SMALLINT;)
CREATE TABLE ex2_table(
ex2_id SMALLINT;)
I think it should be done more or less like this. The commented code is the pseudocode that I don't know how to implement:
CREATE TRIGGER ex_trigger AFTER
INSERT ON ex2_table
FOR EACH ROW EXECUTE PROCEDURE ex_func();
CREATE FUNCTION ex_func() RETURNS trigger AS $$ BEGIN
/*
if ex2_id = ex_id
ex_counter = ex_counter + 1
*/
RETURN NULL;
END;
$$ LANGUAGE plpgsql;
Thanks in advance!
Something like this
IF EXISTS (SELECT * FROM ex_table WHERE ex_id = new.ex2_id) THEN
UPDATE ex_table
SET ex_counter = ex_counter + 1
WHERE ex_id = new.ex2_id
ELSE
INSERT INTO ex_table VALUES (new.ex2_id, 1)
END IF;
Note that it is no point really to store a counter since you so easily can retrieve the value by doing a SELECT COUNT()...

Values of the inserted row in a Trigger Oracle

I want a trigger that updates the value of a column, but I just want to update a small set of rows that depends of the values of the inserted row.
My trigger is:
CREATE OR REPLACE TRIGGER example
AFTER INSERT ON table1
FOR EACH ROW
BEGIN
UPDATE table1 t
SET column2 = 3
WHERE t.column1 = :new.column1;
END;
/
But as I using FOR EACH ROW I have a problem when I try it, I get the mutating table runtime error.
Other option is not to set the FOR EACH ROW, but if I do this, I dont know the inserted "column1" for comparing (or I dont know how to known it).
What can I do for UPDATING a set of rows that depends of the last inserted row?
I am using Oracle 9.
You should avoid the DML statements on the same table as defined in a trigger. Use before DML to change values of the current table.
create or replace trigger example
before insert on table1
for each row
begin
:new.column2 := 3;
end;
/
You can modify the same table with pragma autonomous_transaction:
create or replace trigger example
after insert on table1 for each row
declare
procedure setValues(key number) is
pragma autonomous_transaction;
begin
update table1 t
set column2 = 3
where t.column1 = key
;
end setValues;
begin
setValues(:new.column1);
end;
/
But I suggest you follow #GordonLinoff answere to your question - it's a bad idea to modify the same table in the trigger body.
See also here
If you need to update multiple rows in table1 when you are updating one row, then you would seem to have a problem with the data model.
This need suggests that you need a separate table with one row per column1. You can then fetch the value in that table using join. The trigger will then be updating another table, so there will be no mutation problem.
`create table A
(
a INTEGER,
b CHAR(10)
);
create table B
(
b CHAR (10),
d INTEGER
);
create trigger trig1
AFTER INSERT ON A
REFERENCING NEW AS newROW
FOR EACH ROW
when(newROW.a<=10)
BEGIN
INSERT into B values(:newROW.b,:newROW.a);
END trig1;
insert into A values(11,'Gananjay');
insert into A values(5,'Hritik');
select * from A;
select * from B;`

Merge statement issue in oracle

I came from Microsoft SQL environment.I have two tables tak_ne and tak_beb and my requirement was to insert values from tak_beb to tak_ne if value is not present,if it is present just update.So i made a merge statement as shown below.But the problem now i am facing is veryday 50000 count is getting increment for sequence number.Oracle is stable database, and i don't know why they made it like that.So i create a Function and prevented incrementing sequence number.My question is ,is it a right approach by creating function.Following is what i did
merge into tak_ne a using tak_beb b ON (a.NAME=b.NAME)
When matched then
Update
Set a.AC_NO = b.AC_NO
a.LOCATION = b.LOCATION
a.MODEL = b.MODEL
When not matched then
insert
(
sl_no,
AC_NO,
LOCATION
MODEL
)
Values
(
s_slno_nextval
b.AC_NO
b.LOCATION
b.MODEL
)
and then i created a function
CREATE OR REPLACE FUNCTION s_slno_nextval
RETURN NUMBER
AS
v_nextval NUMBER;
BEGIN
SELECT s_emp.nextval
INTO v_nextval
FROM dual;
RETURN v_nextval;
END;
Oracle uses this approach to generate unique id for each row inserted by a statement. Your TAK_BEB table has probably 50000 rows, so the sequence is incremented 50000 times.
To hide increment into a function does not help. Function is called AND EXECUTED for every row, it increments sequence for 50000 times again. And it adds overhead with 50000 selects from dual table.
If you really need to use ONE value from sequence for ALL rows inserted by statement, use package variable:
create package single_id_pkg is
id Number;
function get_id return number;
end;
/
create or replace package body single_id_pkg is
function get_id return number is
begin
return id;
end;
end;
/
Now use for example before statement trigger on table to set the variable:
create trigger tak_ne_BSI_trg
before insert
on tak_ne
begin
select s_emp.nextval
into single_id_pkg.id
from dual;
end;
Insert trigger has one disadvantage - with MERGE clause it fires even if the statement does only updates rows (see https://asktom.oracle.com/pls/asktom/f?p=100:11:0::::P11_QUESTION_ID:25733900083512). If it is a problem, you have to initialize the variable in other way.
Next modify your statement to use a package variable:
merge into tak_ne a
using tak_beb b
on (a.NAME=b.NAME)
when matched then
update
set a.AC_NO = b.AC_NO
a.LOCATION = b.LOCATION
a.MODEL = b.MODEL
when not matched then
insert (sl_no,
AC_NO,
LOCATION,
MODEL)
values (single_id_pkg.get_id
b.AC_NO,
b.LOCATION,
b.MODEL)
In Oracle standard way to use autoincrement field is by using sequences. And of course it will increment sequence number each time you want to use it.
But you can omit calling sequence_name.nextval, hiding it in trigger it is considered the standard approach also.
CREATE OR REPLACE EDITIONABLE TRIGGER TAK_NE_ID_TR"
BEFORE INSERT ON tak_ne
FOR EACH ROW
BEGIN
IF :old.sl_no IS NULL THEN
:new.sl_no := s_emp.nextval;
END IF;
END;
If you want to add same id for a batch of your inserts you can use global temporary table for saving it. For example, like this:
create global temporary table tak_ne_id ("id" number) on commit delete rows
create or replace trigger tak_ne_BSI_trg
before insert
on tak_ne
begin
insert into tak_ne_id("id")
values(s_emp.nextval);
end
create or replace TRIGGER TAK_NE_ID_TR
BEFORE INSERT ON tak_ne
FOR EACH ROW
BEGIN
if :old.sl_no is null then
SELECT "id"
INTO :new.sl_no
FROM tak_ne_id;
end if;
END;
Then you can use you merge as before, and without calling nextval:
merge into tak_ne a using tak_beb b ON (a.NAME=b.NAME)
When matched then
update
set a.AC_NO = b.AC_NO,
a.LOCATION = b.LOCATION,
a.MODEL = b.MODEL
When not matched then
insert
(
AC_NO,
LOCATION,
MODEL
)
Values
(
b.AC_NO,
b.LOCATION,
b.MODEL
);