PostgreSQL
 sql >> Base de Dados >  >> RDS >> PostgreSQL

PostgreSQL:incremento automático baseado em restrição única de várias colunas


Seria bom se o PostgreSQL suportasse o incremento "em uma coluna secundária em um índice de várias colunas" como as tabelas MyISAM do MySQL

Sim, mas observe que, ao fazer isso, o MyISAM bloqueia toda a sua tabela. O que torna seguro encontrar o maior +1 sem se preocupar com transações simultâneas.

No Postgres, você também pode fazer isso e sem travar a tabela inteira. Um bloqueio consultivo e um gatilho serão bons o suficiente:
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;

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

Há uma ressalva. Os bloqueios consultivos são mantidos até serem liberados ou até que a sessão expire. Se ocorrer um erro durante a transação, o bloqueio é mantido e você precisa liberá-lo manualmente.
SELECT pg_advisory_unlock('animals'::regclass::int, i)
FROM generate_series(1, array_length(enum_range(NULL::animal_grp),1)) i;

No Postgres 9.1, você pode descartar o gatilho de desbloqueio e substituir a chamada pg_advisory_lock() por pg_advisory_xact_lock(). Esse é automaticamente retido até e liberado no final da transação.

Em uma nota separada, eu continuaria usando uma boa e velha sequência. Isso tornará as coisas mais rápidas - mesmo que não seja tão bonita quando você analisa os dados.

Por fim, uma sequência única por combinação (ano, mês) também pode ser obtida adicionando uma tabela extra, cuja chave primária é uma série e cujo valor (ano, mês) possui uma restrição exclusiva.