PostgreSQL: Auto-increment based on multi-column unique constraint

前端 未结 3 1039
自闭症患者
自闭症患者 2020-12-02 21:02

One of my tables has the following definition:

CREATE TABLE incidents
(
  id serial NOT NULL,
  report integer NOT NULL,
  year integer NOT NULL,
  month int         


        
3条回答
  •  隐瞒了意图╮
    2020-12-02 21:32

    It would be nice if PostgreSQL supported incrementing "on a secondary column in a multiple-column index" like MySQL's MyISAM tables

    Yeah, but note that in doing so, MyISAM locks your entire table. Which then makes it safe to find the biggest +1 without worrying about concurrent transactions.

    In Postgres, you can do this too, and without locking the whole table. An advisory lock and a trigger will be good enough:

    CREATE TYPE animal_grp AS ENUM ('fish','mammal','bird');
    
    CREATE TABLE animals (
        grp animal_grp NOT NULL,
        id INT NOT NULL DEFAULT 0,
        name varchar NOT NULL,
        PRIMARY KEY (grp,id)
    );
    
    CREATE OR REPLACE FUNCTION animals_id_auto()
        RETURNS trigger AS $$
    DECLARE
        _rel_id constant int := 'animals'::regclass::int;
        _grp_id int;
    BEGIN
        _grp_id = array_length(enum_range(NULL, NEW.grp), 1);
    
        -- Obtain an advisory lock on this table/group.
        PERFORM pg_advisory_lock(_rel_id, _grp_id);
    
        SELECT  COALESCE(MAX(id) + 1, 1)
        INTO    NEW.id
        FROM    animals
        WHERE   grp = NEW.grp;
    
        RETURN NEW;
    END;
    $$ LANGUAGE plpgsql STRICT;
    
    CREATE TRIGGER animals_id_auto
        BEFORE INSERT ON animals
        FOR EACH ROW WHEN (NEW.id = 0)
        EXECUTE PROCEDURE animals_id_auto();
    
    CREATE OR REPLACE FUNCTION animals_id_auto_unlock()
        RETURNS trigger AS $$
    DECLARE
        _rel_id constant int := 'animals'::regclass::int;
        _grp_id int;
    BEGIN
        _grp_id = array_length(enum_range(NULL, NEW.grp), 1);
    
        -- Release the lock.
        PERFORM pg_advisory_unlock(_rel_id, _grp_id);
    
        RETURN NEW;
    END;
    $$ LANGUAGE plpgsql STRICT;
    
    CREATE TRIGGER animals_id_auto_unlock
        AFTER INSERT ON animals
        FOR EACH ROW
        EXECUTE PROCEDURE animals_id_auto_unlock();
    
    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;
    

    This yields:

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

    There is one caveat. Advisory locks are held until released or until the session expires. If an error occurs during the transaction, the lock is kept around and you need to release it manually.

    SELECT pg_advisory_unlock('animals'::regclass::int, i)
    FROM generate_series(1, array_length(enum_range(NULL::animal_grp),1)) i;
    

    In Postgres 9.1, you can discard the unlock trigger, and replace the pg_advisory_lock() call with pg_advisory_xact_lock(). That one is automatically held until and released at the end of the transaction.


    On a separate note, I'd stick to using a good old sequence. That will make things faster -- even if it's not as pretty-looking when you look at the data.

    Lastly, a unique sequence per (year, month) combo could also be obtained by adding an extra table, whose primary key is a serial, and whose (year, month) value has a unique constraint on it.

提交回复
热议问题