user11962606
user11962606

Reputation:

Generating incremental numbers based on a different column

I have got a composite primary key in a table in PostgreSQL (I am using pgAdmin4)

Let's call the the two primary keys productno and version.

version represents the version of productno.

So if I create a new dataset, then it needs to be checked if a dataset with this productno already exists.

So that we get something like:

  productno | version 
 -----|----------- 
    1 |         1 
    1 |         2 
    1 |         3 
    2 |         1 
    2 |         2 

I found a quite similar problem: auto increment on composite primary key

But I can't use this solution because PostgreSQL syntax is obviously a bit different - so tried a lot around with functions and triggers but couldn't figure out the right way to do it.

Upvotes: 1

Views: 1467

Answers (3)

user330315
user330315

Reputation:

You can keep the version numbers in a separate table (one for each "base PK" value). That is way more efficient than doing a max() + 1 on every insert and has the additional benefit that it's safe for concurrent transactions.

So first we need a table that keeps track of the version numbers:

create table version_counter 
(
  product_no integer primary key, 
  version_nr integer not null
);

Then we create a function that increments the version for a given product_no and returns that new version number:

create function next_version(p_product_no int) 
  returns integer
as
$$
   insert into version_counter (product_no, version_nr) 
   values (p_product_no, 1)
   on conflict (product_no) 
   do update 
      set version_nr = version_counter.version_nr + 1
   returning version_nr;
$$
language sql
volatile;

The trick here is the the insert on conflict which increments an existing value or inserts a new row if the passed product_no does not yet exists.

For the product table:

create table product
(
  product_no integer not null, 
  version_nr integer not null,
  created_at timestamp default clock_timestamp(),
  primary key (product_no, version_nr)
);

then create a trigger:

create function increment_version()
  returns trigger
as
$$
begin
  new.version_nr := next_version(new.product_no);
  return new;
end;
$$
language plpgsql;

create trigger base_table_insert_trigger
  before insert on product
  for each row
  execute procedure increment_version();

This is safe for concurrent transactions because the row in version_counter will be locked for that product_no until the transaction inserting the row into the product table is committed - which will commit the change to the version_counter table as well (and free the lock on that row).

If two concurrent transactions insert the same value for product_no, one of them will wait until the other finishes.

If two concurrent transactions insert different values for product_no, they can work without having to wait for the other.

If we then insert these rows:

insert into product (product_no) values (1);
insert into product (product_no) values (2);
insert into product (product_no) values (3);
insert into product (product_no) values (1);
insert into product (product_no) values (3);
insert into product (product_no) values (2);

The product table looks like this:

select *
from product
order by product_no, version_nr;
product_no | version_nr | created_at             
-----------+------------+------------------------
         1 |          1 | 2019-08-23 10:50:57.880
         1 |          2 | 2019-08-23 10:50:57.947
         2 |          1 | 2019-08-23 10:50:57.899
         2 |          2 | 2019-08-23 10:50:57.989
         3 |          1 | 2019-08-23 10:50:57.926
         3 |          2 | 2019-08-23 10:50:57.966

Online example: https://rextester.com/CULK95702

Upvotes: 2

user11962606
user11962606

Reputation:

So - I got it work now

So if you want a column to update depending on another column in pg sql - have a look at this:

This is the function I use:

CREATE FUNCTION public.testfunction()
    RETURNS trigger
    LANGUAGE 'plpgsql'
    COST 100
    VOLATILE NOT LEAKPROOF 
AS $BODY$
DECLARE v_productno INTEGER := NEW.productno; 
BEGIN

IF NOT EXISTS (SELECT * 
            FROM testtable 
            WHERE productno = v_productno)
THEN 
NEW.version := 1;
ELSE
NEW.version := (SELECT MAX(testtable.version)+1
                FROM testtable
                WHERE testtable.productno = v_productno);
END IF;
RETURN NEW;
END;
$BODY$;

And this is the trigger that runs the function:

CREATE TRIGGER testtrigger
    BEFORE INSERT
    ON public.testtable
    FOR EACH ROW
    EXECUTE PROCEDURE public.testfunction();

Thank you @ChechoCZ, you definetly helped me getting in the right direction.

Upvotes: 0

ChechoCZ
ChechoCZ

Reputation: 312

You can do it like this:

-- Check if pk exists 

SELECT pk INTO temp_pk FROM table a WHERE a.pk = v_pk1;

-- If exists, inserts it

IF temp_pk IS NOT NULL THEN
  INSERT INTO table(pk, versionpk) VALUES (v_pk1, temp_pk);      
END IF;

Upvotes: 0

Related Questions