Reputation: 6590
This is not the typical constraint on two columns.
Here is a table with foreign keys ref1, ref2:
connection_id | ref1_id | ref2_id
1 | 1 | 2
I would like to allow:
connection_id | ref1_id | ref2_id
1 | 1 | 2
2 | 1 | 3
but disallow these two:
(typical unique key on ref1,ref2 - this I know how to do)
connection_id | ref1_id | ref2_id
1 | 1 | 2
2 | 1 | 2
but also!!:
(this is the problem)
connection_id | ref1_id | ref2_id
1 | 1 | 2
2 | 2 | 1
because I would like only one ref1-ref2 pair - for me the pair (ref1,ref2) or (1,2) is the same as (2,1) and should be disallowed by a unique key constraint. Is there a way to do that in MySQL?
I am sure that it was answered already, but when searching I always hit the typical unique constraint on two columns.
Upvotes: 2
Views: 595
Reputation: 4287
Variant 1: Considering your datatype for ref1_id and ref2_id are within the limits of a integer data type and there exists an integer data type which can fit sizeof ref1_id + sizeof ref2_id
in mysql.
Add a new calculated column to your table with the type which can fit it and calculate it as
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)
within a trigger before insert or update - this way the whole row will be rejected due to unique constraint violation.
Variant 2: Use this if variant 1 is not applicable as it will be slower.
Add a new calculated column to your table as varchar long enough to fit the text representations of concat(max(ref1_id), ' ', max(ref2_id))
- fake code, use max posible value here and calculate it as
CONCAT(min(ref1_id, ref2_id), ' ', max(ref1_id, ref2_id))
within a trigger before insert or update - this way the whole row will be rejected due to unique constraint violation.
Upvotes: 1
Reputation: 4062
You need to use canonical order of data in INSERT
/UPDATE
queries (for example ref1_id always less than ref2_id) or custom check duplicates with ON BEFORE INSERT
/UPDATE
trigger.
This task can not be solved using constraints only.
Edit:
There is no way to abort INSERT
or UPDATE
statement with trigger, so the whole solution is even worse :-)
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 ;
Edit 2: Just found hack to abort query in trigger (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 ;
Upvotes: 1
Reputation: 44250
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;
Result:
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)
I don't know if mysql allows and enforces constraints or indexes on expressions. Postgres does allow indexes on expressions, but unfortunately no constraints on expressions. If indexes are not possible the obvious tie-breaking constraint would of course be aaa >= bbb
, like in @vearutob 's answer.
BTW: in this model, the constraint on (aaa,bbb) is redundant, because the condition is already covered by the index. (I would like to have them swapped: the constraint on {least,greatest} and the index on the bare columns, but postgres does not seem to allow this yet)
Upvotes: 1