CHECK CONSTRAINT иногда не срабатывает.

kalombo
Дата: 01.04.2015 07:45:19
Есть таблица:
CREATE TABLE link
(
  id serial NOT NULL,
  "group" character varying(250),
  portid1 integer NOT NULL,
  portid2 integer NOT NULL,
  active boolean NOT NULL DEFAULT true,
  CONSTRAINT link_pkey PRIMARY KEY (id),
  CONSTRAINT unique_portid1 CHECK (check_link(portid1, id)),
  CONSTRAINT unique_portid2 CHECK (check_link(portid2, id))
)

CREATE OR REPLACE FUNCTION check_link(portid integer, linkid integer)
  RETURNS boolean AS
$BODY$
DECLARE
  i int;
BEGIN
  IF EXISTS (SELECT * FROM link 
  WHERE (portid1 = portid or portid2=portid) and id!=linkid) THEN
    RETURN false;
  END IF;

  RETURN true;  
END


Смысл этого CHECK CONSTRAINT в том, что в таблице значения в колноках portid1 и portid2 должны быть различны, то есть если в колонке portid1 уже присутствует какое-то значение, то оно уже не может появится ни в колонке portid1, ни в колонке portid2, аналогично и для portid2.

Я написал данный constraint, проверил его с помощью простых INSERT и UPDATE - работает. Однако, сегодня обнаружил в таблице вот такой набор записей:

select * from link where portid1 = 74581 or portid2=74581;
  id   | group | portid1 | portid2 | active
-------+-------+---------+---------+--------
 76628 |       |  113945 |   74581 | t
 76629 |       |  113945 |   74581 | t
 76630 |       |  113945 |   74581 | t
 76632 |       |  113945 |   74581 | t
 76633 |       |  113945 |   74581 | t
 76634 |       |   74581 |  113945 | t
 76645 |       |  113945 |   74581 | t
(7 rows)


Пробовал сделать Insert с этими же portid, constraint отрабатывает, не получается вставить запись. Попробовал удалить constraint, повесить заново - не получилось, то есть никто не мог удалить constraint, вставить записи и снова повесить. Как оказалось такая ситуация не единичная.
Подскажите, как такое могло получится? Потому что я использую SELECT в constraint? И как сделать правильно?
Alexander A. Sak
Дата: 01.04.2015 08:13:25
Параллельность, транзакции, их изоляция и типа все такое.

postgres=# insert into link (portid1, portid2) values (1,2);
INSERT 0 1

postgres=# insert into link (portid1, portid2) values (2,3);
ОШИБКА:  новая строка в отношении "link" нарушает ограничение-проверку "unique_portid1"
ПОДРОБНОСТИ:  Ошибочная строка содержит (2, null, 2, 3, t).

postgres=# begin transaction;
BEGIN

postgres=# insert into link (portid1, portid2) values (3, 4);
INSERT 0 1

--
-- ПОСЛЕ ЭТОГО В ДРУГОМ КОННЕКТЕ ВЫПОЛНИЛИ ТОТ ЖЕ САМЫЙ INSERT С (3, 4)
--

postgres=# commit;
COMMIT

postgres=# select * from link;
 id | group | portid1 | portid2 | active 
----+-------+---------+---------+--------
  1 |       |       1 |       2 | t
  3 |       |       3 |       4 | t
  4 |       |       3 |       4 | t
Alexander A. Sak
Дата: 01.04.2015 08:29:32
Как сделать правильно точно не скажу. Придумал вариант -- два уникальных индекса по выражению:

create unique index ui_1 on link ((case when portid1<portid2 then portid1 else portid2 end));

create unique index ui_2 on link ((case when portid1>portid2 then portid1 else portid2 end));


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

А вообще-то задача вызывает подозрение на ошибку в проектировании.
kalombo
Дата: 01.04.2015 08:43:59
Alexander A. Sak
Как сделать правильно точно не скажу. Придумал вариант -- два уникальных индекса по выражению:

create unique index ui_1 on link ((case when portid1<portid2 then portid1 else portid2 end));

create unique index ui_2 on link ((case when portid1>portid2 then portid1 else portid2 end));



Спасибо, мне тоже похожее решение подсказали:

CREATE TABLE link
(
  id serial NOT NULL,
  "group" character varying(250),
  portid1 integer NOT NULL,
  portid2 integer NOT NULL,
  active boolean NOT NULL DEFAULT true,
  CONSTRAINT link_pkey PRIMARY KEY (id),
  CONSTRAINT snmp_ports_fkey1 FOREIGN KEY (portid1)
      REFERENCES snmp_ports (id) MATCH SIMPLE
      ON UPDATE NO ACTION ON DELETE CASCADE,
  CONSTRAINT snmp_ports_fkey2 FOREIGN KEY (portid2)
      REFERENCES snmp_ports (id) MATCH SIMPLE
      ON UPDATE NO ACTION ON DELETE CASCADE,
  CONSTRAINT portid1_unique UNIQUE (portid1),
  CONSTRAINT portid2_unique UNIQUE (portid2),
  CONSTRAINT portids_check CHECK (portid1 <> portid2)
)

CREATE UNIQUE INDEX portids_unique
  ON link
  USING btree
  ((LEAST(portid1, portid2)), (GREATEST(portid1, portid2)));

Только еще check приходится использовать.


Alexander A. Sak
Приведенный выше пример с дублированием уже не проходит. Вторая транзакция начинает ждать окончания первой, и в зависимости от нее либо завершаться нормально, либо выдавать ошибку. Дублей не появляется.

А вообще-то задача вызывает подозрение на ошибку в проектировании.


Да, похоже на ошибку в проектировании, я специально показал еще FOREIGN KEY, чтобы было понятно, что из себя таблица представляет. То есть это порты оборудования, которые могут быть соединены между собой, естественно, один порт дважды соединен быть не может. Я так понимаю правильней было бы не создавать таблицу link, а просто добавить поле link_to в табличке snmp_ports?