Auto incremented column scoped to user id

Viewed 87

I am really struggling how to implement requirement which is going to be best described with example. Consider everything below to be written in pseudocode although I am interested in solutions for postgres.

id id_for_user note created_by
1 1 Buy milk 1
1 2 Winter tyres 1
1 3 Read for 1h 1
2 1 Clean dishes 2
2 2 Learn how magnets work 2
INSERT INTO notes VALUES (note: 'Learn icelandic', created_by: 1);
id id_for_user note created_by
1 1 Buy milk 1
2 2 Winter tyres 1
3 3 Read for 1h 1
4 1 Clean dishes 2
5 2 Learn how magnets work 2
6 4 Learn Icelandic 1
INSERT INTO notes VALUES (note: 'Are birds real?', created_by: 2);
id id_for_user note created_by
1 1 Buy milk 1
2 2 Winter tyres 1
3 3 Read for 1h 1
4 1 Clean dishes 2
5 2 Learn how magnets work 2
6 4 Learn Icelandic 1
7 3 Are birds real? 2

I would like to achieve something like this:

CREATE TABLE notes (
    id SERIAL,
    id_for_user INT DEFAULT nextval(created_by) -- Dynamic name for sequence so every user gets its own,
    note VARCHAR,
    created_by INT,
    PRIMARY KEY(id, id_for_user),
    CONSTRAINT fk_notes_created_by
      FOREIGN KEY(created_by) 
        REFERENCES users(created_by)
);

So that user 1 sees (notice how id_for_user is just id on front end)

id note
1 Buy milk
2 Winter tyres
3 Read for 1h
4 Learn Icelandic

And user 2

id note
1 Clean dishes
2 Learn how magnets work
3 Are birds real?

Basically I want to have auto incremented field for each user. I am then also probably going to query for the record by id_for_user filling create_by on backend based on which user made the request.

Is something like this even possible? What are my options? I would really like to have this logic on db level.

https://www.db-fiddle.com/f/6eBvq4VCQPTmmR3W6fCnEm/2

3 Answers

Try with a sequence, this object will have control of the autonumeric of the ID

example:

CREATE SEQUENCE sequence_notes1
  INCREMENT BY 1
  MINVALUE 1
  MAXVALUE 100;

  CREATE SEQUENCE sequence_notes2
  INCREMENT BY 1
  MINVALUE 1
  MAXVALUE 100;

CREATE TABLE notes (
  id SERIAL,
  id_for_user INT,
  note VARCHAR,
  created_by INT,
  PRIMARY KEY(id)
);


INSERT INTO notes (id_for_user, note, created_by) VALUES (nextval('sequence_notes1'),'Foo', 1);
INSERT INTO notes (id_for_user, note, created_by) VALUES (nextval('sequence_notes1'),'Moo', 1);
INSERT INTO notes (id_for_user, note, created_by) VALUES (nextval('sequence_notes2'),'Boo', 2);
INSERT INTO notes (id_for_user, note, created_by) VALUES (nextval('sequence_notes2'),'Loo', 2);

You can have a separate table to store "the next ordinal value for each user". Then a trigger can fill the value and increment the related table.

For example:

create table usr (
  id int primary key,
  next_ordinal int default 1
);

create table note (
  id int primary key,
  note varchar(100),
  created_by int references usr (id),
  user_ord int
);

create or replace function add_user_ord() returns trigger as $$
begin
  select next_ordinal into new.user_ord from usr where id = new.created_by;
  update usr set next_ordinal = next_ordinal + 1 where id = new.created_by;
  return new;
end;
$$ language plpgsql;
//

create trigger trg_note1 before insert on note 
for each row execute procedure add_user_ord();

Then, the trigger will add the correct ordinal numbers automatically behind the scenes during INSERTs:

insert into usr (id) values (10), (20);

insert into note (id, note, created_by) values (1, 'Hello', 10);
insert into note (id, note, created_by) values (2, 'Lorem', 20);
insert into note (id, note, created_by) values (3, 'World', 10);
insert into note (id, note, created_by) values (4, 'Ipsum', 20);

Result:

id  note   created_by  user_ord
--  -----  ----------  --------
 1  Hello          10         1
 2  Lorem          20         1
 3  World          10         2
 4  Ipsum          20         2

Note: This solution does not address multi-threading inserts. If your application needs this you'll need to add some isolation (or pessimistic, or optimistic locking) for it.

In MySQL, this is supported in the MyISAM storage engine.

https://dev.mysql.com/doc/refman/8.0/en/example-auto-increment.html says:

For MyISAM tables, you can specify AUTO_INCREMENT on a secondary column in a multiple-column index. In this case, the generated value for the AUTO_INCREMENT column is calculated as MAX(auto_increment_column) + 1 WHERE prefix=given-prefix. This is useful when you want to put data into ordered groups.

CREATE TABLE animals (
    grp ENUM('fish','mammal','bird') NOT NULL,
    id MEDIUMINT NOT NULL AUTO_INCREMENT,
    name CHAR(30) NOT NULL,
    PRIMARY KEY (grp,id)
) ENGINE=MyISAM;

INSERT INTO animals (grp,name) VALUES
    ('mammal','dog'),('mammal','cat'),
    ('bird','penguin'),('fish','lax'),('mammal','whale'),
    ('bird','ostrich');

SELECT * FROM animals ORDER BY grp,id; Which returns:

+--------+----+---------+
| grp    | id | name    |
+--------+----+---------+
| fish   |  1 | lax     |
| mammal |  1 | dog     |
| mammal |  2 | cat     |
| mammal |  3 | whale   |
| bird   |  1 | penguin |
| bird   |  2 | ostrich |
+--------+----+---------+

The reason this works in MyISAM is that MyISAM only supports table-level locking.

In a storage engine with row-level locking, you get race conditions if you try to have a primary key that works like this. This is why others on this thread have commented that implementing this with triggers requires some pessimistic locking. You have to use locking to ensure that only one client at a time is inserting, so they don't allocate the same value.

This will be limiting in a high-traffic application. InnoDB's auto-increment is implemented the way it is to allow applications in which many client threads are executing inserts concurrently.

So you could use MyISAM or you could use InnoDB and invent your own way of allocating new id's per user, but either way it will severely limit your app's scalability.

Related