Byłoby dobrze, gdyby PostgreSQL obsługiwał inkrementację "na drugorzędnej kolumnie w indeksie wielokolumnowym", jak tabele MyISAM MySQL
Tak, ale pamiętaj, że w ten sposób MyISAM blokuje cały stół. Dzięki temu można bezpiecznie znaleźć największy +1 bez martwienia się o równoczesne transakcje.
W Postgresie możesz to zrobić również bez blokowania całego stołu. Wystarczy blokada doradcza i wyzwalacz:
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;
To daje:
grp | id | name
--------+----+---------
fish | 1 | lax
mammal | 1 | dog
mammal | 2 | cat
mammal | 3 | whale
bird | 1 | penguin
bird | 2 | ostrich
(6 rows)
Jest jedno zastrzeżenie. Blokady doradcze są utrzymywane do momentu zwolnienia lub wygaśnięcia sesji. Jeśli podczas transakcji wystąpi błąd, blokada zostanie zachowana i trzeba ją zwolnić ręcznie.
SELECT pg_advisory_unlock('animals'::regclass::int, i)
FROM generate_series(1, array_length(enum_range(NULL::animal_grp),1)) i;
W Postgresie 9.1 możesz odrzucić wyzwalacz odblokowujący i zastąpić wywołanie pg_advisory_lock() funkcją pg_advisory_xact_lock(). Ten jest automatycznie wstrzymywany do czasu zakończenia transakcji i zwolniony.
Oddzielnie, trzymałbym się starej dobrej sekwencji. To przyspieszy sprawę - nawet jeśli nie wygląda to tak ładnie, gdy spojrzysz na dane.
Na koniec, unikatową sekwencję na (rok, miesiąc) można również uzyskać, dodając dodatkową tabelę, której kluczem podstawowym jest numer seryjny i której wartość (rok, miesiąc) ma na nią ograniczenie unikalności.