2

这不是对两列的典型约束。

这是一个带有外键 ref1、ref2 的表:

connection_id | ref1_id  | ref2_id
1             |     1    |    2

我想允许:

connection_id | ref1_id  | ref2_id
1             |     1    |    2
2             |     1    |    3

但不允许这两个:

(typical unique key on ref1,ref2 - this I know how to do)
connection_id | ref1_id  | ref2_id
1             |     1    |    2
2             |     1    |    2

但是也!!:

(this is the problem)
connection_id | ref1_id  | ref2_id
1             |     1    |    2
2             |     2    |    1

因为我只想要一个 ref1-ref2 对 - 对我来说,这对 (ref1,ref2) 或 (1,2) 与 (2,1) 相同,并且应该被唯一键约束禁止。有没有办法在 MySQL 中做到这一点?

我确信它已经得到了回答,但是在搜索时,我总是在两列上遇到典型的唯一约束。

4

3 回答 3

1

您需要在INSERT/UPDATE查询中使用规范的数据顺序(例如 ref1_id 始终小于 ref2_id)或使用ON BEFORE INSERT/UPDATE触发器自定义检查重复项。

仅使用约束无法解决此任务。

编辑:没有办法用触发器中止INSERTUPDATE声明,所以整个解决方案更糟:-)

DELIMITER ###

CREATE TRIGGER `after_up` 
  AFTER UPDATE ON `my_table`
FOR EACH ROW 
BEGIN
  DECLARE collision INT DEFAULT 0;
  SELECT 1  
    INTO collision
    FROM my_table 
    WHERE ref1_id = NEW.ref2_id AND ref2_id = NEW.ref1_id;
  IF collision
  THEN -- reverting update
    UPDATE my_table SET ref2_id = OLD.ref2_id, ref1_id = OLD.ref1_id WHERE connection_id = OLD.connection_id;
  END IF;
END

###

CREATE TRIGGER `after_in` 
  AFTER INSERT ON `my_table`
FOR EACH ROW 
BEGIN
  DECLARE collision INT DEFAULT 0;
  SELECT 1  
    INTO collision
    FROM my_table 
    WHERE ref1_id = NEW.ref2_id AND ref2_id = NEW.ref1_id;
  IF collision
  THEN -- deleting new row
    DELETE FROM my_table WHERE connection_id = NEW.connection_id;
  END IF;
END

###

delimiter ;

编辑 2:刚刚发现在触发器 ( DROP TABLE nonexistent_table_name)中中止查询的黑客攻击

DELIMITER ###

CREATE TRIGGER `before_up` 
  BEFORE UPDATE ON `my_table`
FOR EACH ROW 
BEGIN
  DECLARE collision INT DEFAULT 0;
  SELECT 1  
    INTO collision
    FROM my_table 
    WHERE ref1_id = NEW.ref2_id AND ref2_id = NEW.ref1_id;
  IF collision
  THEN -- throwing error
     DROP TABLE __error_duplicate_detected;        
  END IF;
END

###

CREATE TRIGGER `before_in` 
  BEFORE INSERT ON `my_table`
FOR EACH ROW 
BEGIN
  DECLARE collision INT DEFAULT 0;
  SELECT 1  
    INTO collision
    FROM my_table 
    WHERE ref1_id = NEW.ref2_id AND ref2_id = NEW.ref1_id;
  IF collision
  THEN -- throwing error
     DROP TABLE __error_duplicate_detected;        
  END IF;
END

###

delimiter ;
于 2012-09-19T10:08:08.933 回答
1
DROP TABLE pair CASCADE;
CREATE TABLE pair
      ( pair_id SERIAL NOT NULL PRIMARY KEY
      , aaa INTEGER NOT NULL
      , bbb INTEGER NOT NULL
      , CONSTRAINT aaa_bbb UNIQUE (aaa, bbb)
    );

CREATE UNIQUE INDEX aaaXXXbbb ON pair ( LEAST(aaa, bbb), GREATEST(aaa, bbb) )
        ;
INSERT INTO pair(aaa,bbb) VALUES(1,1), (1,2),(2,2);

INSERT INTO pair(aaa,bbb) VALUES(2,1);

SELECT * FROM pair;

结果:

INSERT 0 3
ERROR:  duplicate key value violates unique constraint "aaaxxxbbb"
DETAIL:  Key ((LEAST(aaa, bbb)), (GREATEST(aaa, bbb)))=(1, 2) already exists.
 pair_id | aaa | bbb 
---------+-----+-----
       1 |   1 |   1
       2 |   1 |   2
       3 |   2 |   2
(3 rows)

我不知道 mysql 是否允许并强制执行表达式的约束或索引。Postgres 确实允许对表达式进行索引,但不幸的是对表达式没有限制。如果索引不可能,那么明显的打破平局的约束当然是aaa >= bbb,就像在@vearutob 的回答中一样。

顺便说一句:在这个模型中,对 (aaa,bbb) 的约束是多余的,因为条件已经被索引覆盖了。(我想让它们交换:{least,greatest} 上的约束和裸列上的索引,但 postgres 似乎还不允许这样做)

于 2012-09-19T11:01:39.977 回答
1

变体 1:考虑到 ref1_id 和 ref2_id 的数据类型在整数数据类型的限制范围内,并且存在可以适合sizeof ref1_id + sizeof ref2_idmysql 的整数数据类型。

将一个新的计算列添加到您的表中,其类型可以适合它并 min(ref1_id, ref2_id) << (size_in_bits_of_the_biggest_of_the_types_of_ref1_id_and_ref2_id) + max(ref1_id, ref2_id)在插入或更新之前在触发器内计算它 - 这样整个行将由于违反唯一约束而被拒绝。

变体 2:如果变体 1 不适用,请使用此选项,因为它会更慢。

将一个新的计算列添加到您的表中,作为 varchar 足够长以适合文本表示concat(max(ref1_id), ' ', max(ref2_id))- 假代码,在此处使用最大可能值并 CONCAT(min(ref1_id, ref2_id), ' ', max(ref1_id, ref2_id))在插入或更新之前在触发器内计算它 - 这样整行将因唯一而被拒绝违反约束。

于 2012-09-19T15:00:07.263 回答