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.