sql >> Base de Datos >  >> RDS >> PostgreSQL

PostgreSQL:incremento automático basado en una restricción única de varias columnas

Sería bueno si PostgreSQL admitiera incrementar "en una columna secundaria en un índice de varias columnas" como las tablas MyISAM de MySQL

Sí, pero tenga en cuenta que, al hacerlo, MyISAM bloquea toda su tabla. Lo que hace que sea seguro encontrar el mayor +1 sin preocuparse por las transacciones simultáneas.

En Postgres, también puede hacer esto y sin bloquear toda la tabla. Un bloqueo de aviso y un disparador serán lo suficientemente buenos:

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;

Esto produce:

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

Hay una advertencia. Los bloqueos de aviso se mantienen hasta que se liberan o hasta que expira la sesión. Si ocurre un error durante la transacción, el bloqueo se mantiene y debe liberarlo manualmente.

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

En Postgres 9.1, puede descartar el disparador de desbloqueo y reemplazar la llamada pg_advisory_lock() con pg_advisory_xact_lock(). Ese se retiene automáticamente hasta que se libera al final de la transacción.

En una nota aparte, me limitaría a usar una buena secuencia antigua. Eso hará que las cosas sean más rápidas, incluso si no se ven tan bien cuando miras los datos.

Por último, también se podría obtener una combinación única de secuencia por (año, mes) agregando una tabla adicional, cuya clave principal es una serie y cuyo valor (año, mes) tiene una restricción única.