PostgreSQL: автоинкремент, основанный на уникальном ограничении на несколько столбцов - PullRequest
15 голосов
/ 16 июня 2011

Одна из моих таблиц имеет следующее определение:

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)
);

Как бы вы увеличили столбец number для каждого report, year и month независимо?Я бы хотел избежать создания последовательности или таблицы для каждого (report, year, month) набора.

Было бы неплохо, если бы PostgreSQL поддерживал увеличение " во вторичном столбце".в многостолбцовом индексе", как в таблицах MyISAM MySQL, но я не нашел упоминания о такой возможности в руководстве .

Очевидным решением является выбортекущее значение в таблице + 1, но это явно не безопасно для одновременных сеансов.Может быть, сработал бы триггер с предварительной вставкой, но гарантированно, что они не являются одновременными?

Также обратите внимание, что я вставляю инциденты по отдельности, поэтому я не могу использовать generate_series , как предложено в другом месте .

Ответы [ 3 ]

13 голосов
/ 16 июня 2011

Было бы неплохо, если бы PostgreSQL поддерживал увеличение "на вторичном столбце в индексе с несколькими столбцами", как в MySQL-таблицах MySQL

Да, но учтите, что при этом MyISAM блокирует всю вашу таблицу. Что делает безопасным поиск наибольшего +1, не беспокоясь о параллельных транзакциях.

В Postgres вы можете сделать это и без блокировки всей таблицы. Консультативный замок и триггер будут достаточно хороши:

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;

Это дает:

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

Есть одна оговорка. Консультативные блокировки удерживаются до освобождения или до истечения сеанса. Если во время транзакции возникает ошибка, блокировка сохраняется, и вам необходимо снять ее вручную.

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

В Postgres 9.1 вы можете сбросить триггер разблокировки и заменить вызов pg_advisory_lock () на pg_advisory_xact_lock (). Этот автоматически удерживается до и освобождается в конце транзакции.


В отдельном примечании я бы придерживался старой доброй последовательности. Это ускорит процесс, даже если вы посмотрите на данные не так привлекательно.

Наконец, уникальная последовательность для комбинированного списка (год, месяц) также может быть получена путем добавления дополнительной таблицы, первичный ключ которой является последовательным, а значение (год, месяц) которой имеет уникальное ограничение.

2 голосов
/ 01 апреля 2015

Я думаю, что нашел лучшее решение. Он не зависит от типа grp (это может быть enum, integer и string) и может использоваться во многих случаях.

myFunc () - функция для триггера. Вы можете назвать это как хотите. число - столбец автоинкремента, который растет для каждого существующего значения grp. grp - ваш столбец, который вы хотите посчитать в число. myTrigger - триггер для вашего стола. myTable - таблица, где вы хотите сделать триггер. unique_grp_number_key - уникальный ключ ограничения. Нам нужно сделать это для уникальной пары значений: grp и number.

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();

Как это работает? Когда вы вставляете что-то в myTable, триггер вызывает и проверяет, является ли поле числа пустым. Если он пуст, myFunc () выбирает MAX-значение числа, где grp равно новому значению grp, которое вы хотите вставить. Он возвращает максимальное значение + 1, как auto_increment, и заменяет пустое числовое поле новым значением автоинкремента.

Это решение более уникально, чем Дени де Бернарди, потому что оно не зависит от GRP Type, но благодаря ему его код помогает мне написать мое решение. Возможно, уже слишком поздно, чтобы написать ответ, но я не могу найти уникальное решение для этой проблемы в stackoverflow, поэтому он может кому-то помочь. Наслаждайтесь и спасибо за помощь!

2 голосов
/ 16 июня 2011

Я думаю, что это поможет: http://www.varlena.com/GeneralBits/130.php

Обратите внимание, что в MySQL это только для таблиц MyISAM.

PP Я проверил консультативные блокировки и обнаружил, что они бесполезны для более чем одной транзакции одновременно. Я использую 2 окна pgAdmin. Первый как можно проще:

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"

Второе:

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( $1 ,  $2 )"
PL/pgSQL function "animals_id_auto" line 15 at perform

А база данных заблокирована и не может быть разблокирована - неизвестно, что разблокировать.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...