PostgreSQL:基于多列唯一约束的自动增量

时间:2011-06-16 08:39:24

标签: postgresql auto-increment

我的一张桌子有以下定义:

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

您将如何为每个numberreportyear 独立增加month列?我想避免为每个(reportyearmonth)集创建序列或表格。

如果PostgreSQL支持像MySQL的MyISAM表那样增加“on a secondary column in a multiple-column index”会很好,但我在manual中找不到这样的功能。

一个明显的解决方案是选择表 + 1中的当前值,但这对于并发会话显然是不安全的。也许预插入触发器可以工作,但它们是否保证是非并发的?

另请注意,我是单独插入事件的,因此我无法按照建议generate_series使用elsewhere

3 个答案:

答案 0 :(得分:13)

  

如果PostgreSQL支持在“多列索引中的辅助列”上增加,如MySQL的MyISAM表,那将是很好的

是的,但请注意,在这样做时,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_xact_lock()替换pg_advisory_lock()调用。该交易将自动保留至交易结束时并在交易结束时解除。


另外,我会坚持使用一个好的旧序列。这将使事情变得更快 - 即使在查看数据时它看起来并不漂亮。

最后,还可以通过添加一个额外的表来获得每个(年,月)组合的唯一序列,该表的主键是序列号,并且其(年,月)值对其具有唯一约束。

答案 1 :(得分:2)

我认为这会有所帮助: http://www.varlena.com/GeneralBits/130.php

请注意,在MySQL中,它仅适用于MyISAM表。

PP我已经测试过咨询锁,发现它们同时用于超过1次交易。我正在使用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

数据库已锁定且无法解锁 - 未知要解锁的内容。

答案 2 :(得分:2)

我想我找到了更好的解决方案。它不依赖于grp类型(它可以是枚举,整数和字符串),并且可以在很多情况下使用。

myFunc() - 触发器的函数。您可以根据需要命名。 number - 自动增量列,它为每个存在的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中插入内容时,触发器会调用并检查number字段是否为空。如果为空,则myFunc()选择数值的MAX值,其中grp等于要插入的新grp值。它返回最大值+ 1,如auto_increment,并将空数字段替换为新的自动增量值。

这个解决方案比Denis de Bernardy更独特,因为它不依赖于grp Type,但多亏了他,他的代码帮助我编写了我的解决方案。 也许现在写答案为时已晚,但我无法在stackoverflow中找到解决此问题的唯一解决方案,因此它可以帮助某人。享受并感谢您的帮助!