0

I want to have an ordinal column in which values always start from 1 and have no gaps. I have devised a solution with triggers, but I'd like to know if there is a better or more elegant way.

BEFORE INSERT trigger renumbers the rows that come after the inserted value. If value is not provided or too high, it is set to row count + 1. Similarly, AFTER DELETE trigger renumbers the rows that come after the deleted value. Both triggers lock rows before changing the value.

CREATE OR REPLACE FUNCTION ids_insert() RETURNS trigger AS $BODY$
DECLARE
    _lock_sql text;
    _id bigint;
BEGIN
    IF TG_OP = 'INSERT' THEN
        IF NEW.id < 1 THEN
            RAISE EXCEPTION 'ID must be greater than zero.';
        END IF;
        EXECUTE format('SELECT COUNT(*) + 1 FROM %I', TG_TABLE_NAME)
            INTO _id;
        IF NEW.id IS NULL OR NEW.id > _id THEN
            NEW.id := _id;
        ELSE
            _lock_sql := format(
                'SELECT id FROM %I '
                'WHERE id >= %s '
                'ORDER BY id DESC '
                'FOR UPDATE', TG_TABLE_NAME, NEW.id
            );
            FOR _id IN EXECUTE _lock_sql LOOP
                EXECUTE format('UPDATE %I SET id = id + 1 WHERE id = %s', TG_TABLE_NAME, _id);
            END LOOP;
        END IF;
    ELSE
        IF NEW.id != OLD.id THEN
            RAISE EXCEPTION 'Changing the ID directly is not allowed.';
        END IF;
    END IF;

    RETURN NEW;
END;
$BODY$ LANGUAGE plpgsql;

CREATE OR REPLACE FUNCTION ids_delete() RETURNS trigger AS $BODY$
DECLARE
    _lock_sql text;
    _id bigint;
BEGIN
    _lock_sql := format(
        'SELECT id FROM %I '
        'WHERE id > %s '
        'ORDER BY id '
        'FOR UPDATE', TG_TABLE_NAME, OLD.id
    );
    FOR _id IN EXECUTE _lock_sql LOOP
        EXECUTE format('UPDATE %I SET id = id - 1 WHERE id = %s', TG_TABLE_NAME, _id);
    END LOOP;
    RETURN OLD;
END;
$BODY$ LANGUAGE plpgsql;

CREATE TABLE test (
    id bigint PRIMARY KEY,
    ...
)

CREATE TRIGGER test_insert BEFORE INSERT OR UPDATE OF id ON test
    FOR EACH ROW WHEN (pg_trigger_depth() < 1) EXECUTE PROCEDURE ids_insert();

CREATE TRIGGER test_delete AFTER DELETE ON test
    FOR EACH ROW EXECUTE PROCEDURE ids_delete();
Nikša Baldun
  • 1,854
  • 4
  • 28
  • 39
  • That's still unlikely to work, due to transaction visibility. I think the only way to get the result you want is to use `SERIALIZABLE` transaction isolation, in all transactions which touch that table -- that will slow things down significantly. What's the reason for this requirement? – jmelesky Oct 28 '18 at 00:43
  • @jmelesky It's for a firewall-like table in which every rule has a sequential number. Gaps would not break functionality in any way, but it would not look good. Concurrency is not a problem, there will rarely, if ever, be more than one user writing to the table at the same time. – Nikša Baldun Oct 28 '18 at 07:43
  • This is a very bad idea. 1) updating a PK is a bad idea(it could cascade into oher table' s FKs referring to this table) 2) it will ruine history 3) if you need an ordinal, you can always construct it instantly using `row_number() OVER (ORDER by id) AS seqid` – wildplasser Oct 28 '18 at 15:11
  • @wildplasser Every record needs to have an actual ordinal that can be referred to in updates. ROW_NUMBER can't help with that. If only a subset of rows is selected, ROW_NUMBER will show the wrong ordinal. I don't see a problem with cascading, but for the sake of argument, this column does not need to be a primary key. – Nikša Baldun Oct 28 '18 at 17:03
  • `If only a subset of rows is selected,` This could cause gaps on your display, too. Just live with the gaps, and never renumber keys. – wildplasser Oct 28 '18 at 18:07
  • Not renumbering is not an option, because rules need to be processed in a user-defined order. So, if an user wants to insert a new rule at the beginning, all subsequent rules must be renumbered. – Nikša Baldun Oct 28 '18 at 18:30
  • 1
    It sounds to me like you'd be better served by a separate PK that has no meaning other than being PK. That allows you to reorder your rules without issue, and gets around the aesthetic issue of gap keys (since the PK would never be displayed). – jmelesky Oct 29 '18 at 13:24
  • @jmelesky Yes, I agree. It makes matters considerably easier. – Nikša Baldun Oct 29 '18 at 14:12

0 Answers0