PostgreSQL: auto-incrément basé sur une contrainte unique à plusieurs colonnes

Un de mes tableaux a la définition suivante:

CREATE TABLE incidents
(
  id serial NOT NULL,
  report integer NOT NULL,
  year integer NOT NULL,
  month integer NOT NULL,
  number integer NOT NULL, -- Report serial number for this period
  ...
  CONSTRAINT PRIMARY KEY (id),
  CONSTRAINT UNIQUE (report, year, month, number)
);

Comment feriez-vous pour incrémenter le number colonne pour chaque report, year et monthindépendamment<!--12? Je voudrais éviter de créer une séquence ou une table pour chacun (report, year,month).

Ce serait bien si PostgreSQL est pris en charge incrémentation " sur une colonne secondaire dans un index à plusieurs colonnes" comme MySQL tables MyISAM, mais je ne pouvais pas trouver un mention d'une telle caractéristique dans le manuel.

une solution évidente est de sélectionner la valeur actuelle dans le tableau + 1, mais ce n'est évidemment pas sûr pour les sessions simultanées. Peut-être un pré-déclencheur d'insertion, mais sont-ils garantis non-simultanées?

notez aussi que j'insère les incidents individuellement, donc je ne peux pas utiliser generate_series comme l'a suggéré ailleurs.

14
demandé sur Community 2011-06-16 12:39:24

3 réponses

Ce serait bien si PostgreSQL est pris en charge incrémentation "sur une seconde colonne dans un index multi-colonnes" comme MySQL tables MyISAM

Oui, mais notez qu'en faisant cela, MyISAM verrouille votre table entière. Ce qui permet de trouver le plus gros +1 en toute sécurité sans se soucier des transactions simultanées.

à Postgres, vous pouvez le faire aussi, et sans verrouiller toute la table. Un verrou d'avertissement et un déclencheur seront bons. assez:

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;

Cela donne:

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

il y a une mise en garde. Les serrures consultatives sont maintenues jusqu'à libération ou jusqu'à l'expiration de la session. Si une erreur se produit au cours de la transaction, la serrure est conservée et vous devez la Débloquer manuellement.

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

dans Postgres 9.1, vous pouvez rejeter le déclencheur de déverrouillage, et remplacer l'appel pg_advisory_lock() par pg_advisory_xact_lock(). Que l'on est automatiquement maintenu jusqu'à ce que et libéré à la fin de la transaction.


sur une note séparée, Je m'en tiendrais à utiliser une bonne vieille séquence. Cela accélérera les choses, même si ce n'est pas aussi joli quand on regarde les données.

enfin, une séquence unique par (Année, Mois) combo pourrait aussi être obtenue en ajoutant une table supplémentaire, dont la clé primaire est une série, et dont la valeur (Année, Mois) a une contrainte unique sur elle.

12
répondu Denis de Bernardy 2011-06-16 09:58:41

je pense que cela va aider: http://www.varlena.com/GeneralBits/130.php

notez qu'en MySQL c'est pour les tables MyISAM seulement.

PP j'ai testé les serrures de notification et les ai trouvées inutiles pour plus d'une transaction en même temps. J'utilise 2 fenêtres de pgAdmin. La première est la plus simple possible:

BEGIN;
INSERT INTO animals (grp,name) VALUES ('mammal','dog');
COMMIT;

BEGIN;
INSERT INTO animals (grp,name) VALUES ('mammal','cat');
COMMIT;

ERROR: duplicate key violates unique constraint "animals_pkey"

Deuxième:

BEGIN;
INSERT INTO animals (grp,name) VALUES ('mammal','dog');
INSERT INTO animals (grp,name) VALUES ('mammal','cat');
COMMIT;

ERROR: deadlock detected
SQL state: 40P01
Detail: Process 3764 waits for ExclusiveLock on advisory lock [46462,46496,2,2]; blocked by process 2712.
Process 2712 waits for ShareLock on transaction 136759; blocked by process 3764.
Context: SQL statement "SELECT  pg_advisory_lock(  ,   )"
PL/pgSQL function "animals_id_auto" line 15 at perform

et la base de données est verrouillée et ne peut pas être déverrouillée - on ne sait pas quoi déverrouiller.

2
répondu jordani 2011-06-18 17:15:16

je pense que j'ai trouvé une meilleure solution. Il ne dépend pas du Type de grp (il peut être enum, entier et chaîne) et peut être utilisé dans de nombreux cas.

myFunc () - fonction pour un déclencheur. Vous pouvez le nommer comme vous le souhaitez. nombre-colonne d'autoincrément qui croît pour chaque existe valeur de grp. grp - votre colonne que vous souhaitez compter le nombre. mytrigger-trigger pour votre table. myTable-table où vous voulez faire la gâchette. unique_grp_number_key-clé de contrainte unique. Nous avons besoin de le faire pour une paire unique de valeurs: grp et nombre.

ALTER TABLE "myTable"
    ADD CONSTRAINT "unique_grp_number_key" UNIQUE(grp, number);

CREATE OR REPLACE FUNCTION myFunc() RETURNS trigger AS $body_start$
BEGIN
    SELECT COALESCE(MAX(number) + 1, 1)
        INTO NEW.number
        FROM "myTable"
        WHERE grp = NEW.grp;
    RETURN NEW;
END;
$body_start$ LANGUAGE plpgsql;

CREATE TRIGGER myTrigger BEFORE INSERT ON "myTable"
    FOR EACH ROW
    WHEN (NEW.number IS NULL) 
    EXECUTE PROCEDURE myFunc();

Comment ça marche? Lorsque vous insérez quelque chose dans myTable, trigger invoque et vérifie si le champ nombre est vide. S'il est vide, myFunc() sélectionne la valeur MAX du nombre où grp est égal à la nouvelle valeur grp que vous voulez insérer. Il renvoie la valeur max + 1 comme auto_ incrément et remplace le champ de nombre nul à la nouvelle valeur d'auto-incrément.

Cette solution est plus unique que Denis de Bernardy car elle ne dépend pas de grp Type, mais grâce à lui, son code m'aide à écrire ma solution. Peut-être qu'il est trop tard pour écrire réponse, mais je ne peux pas trouver une solution unique pour ce problème dans stackoverflow, de sorte qu'il peut aider quelqu'un. Enjoy et merci pour l'aide!

2
répondu haveacigar 2015-04-01 07:50:33