Bi-directional unique key constraint for combination of two columns - mysql

I have the below table columns in MySQL.
id
user_primary_email
user_secondary_email
I want to make the combination of columns user_primary_email and user_secondary_email unique which I can achieve by using UNIQUE KEY unique_key_name (user_primary_email, user_secondary_email)
The above addition of unique key constraint will help me achieve the below scenario or rather just by adding a unique key to the individual column itself.
user_primary_email = 'xyz#gmail.com' AND user_secondary_email = 'pqr#gmail.com'
user_primary_email = 'xyz#gmail.com' AND user_secondary_email = 'pqr#gmail.com' //This will not be allowed to enter due to unique key constraint
Now the problem which I am facing is the same combination should not be allowed to add in a reverse way as mentioned below.
user_primary_email = 'pqr#gmail.com' AND user_secondary_email = 'xyz#gmail.com' //This should not be allowed to add since already same email id combination added once
id | user_primary_email | user_secondary_email
-------------------------------------------------------
1 | xyz#gmail.com | pqr#gmail.com
-------------------------------------------------------
2 | pqr#gmail.com | xyz#gmail.com
-------------------------------------------------------
In the above case during insert of row id 2 it should throw error as both the email id combination is already used in row id 1.
Any help would be great.

In any MariaDB:
CREATE TABLE `t` (
`id` int(11) NOT NULL,
`user_primary_email` varchar(64) DEFAULT NULL,
`user_secondary_email` varchar(64) DEFAULT NULL,
`mycheck` varchar(128) AS (IF(user_primary_email<user_secondary_email,CONCAT(user_primary_email,user_secondary_email),CONCAT(user_secondary_email,user_primary_email))) PERSISTENT,
PRIMARY KEY (`id`),
UNIQUE KEY `mycheck` (`mycheck`)
);
MariaDB [test]> insert into t values (1,'a','b',null);
Query OK, 1 row affected (0.03 sec)
MariaDB [test]> insert into t values (2,'b','a',null);
ERROR 1062 (23000): Duplicate entry 'ab' for key 'mycheck'

There is no direct support for that, but you can use a workaround to create your bidirectional key: You need a unique key on an ordered version of your two columns.
Fortunately, you can very easily do that. MySQL 5.7.6+ supports generated columns and unique indexes for them, which you can use to order your two values and to enforce uniqueness.
create table testBiDirKey (
a varchar(100),
b varchar(100),
a_ordered varchar(100) as (least(a, b)) STORED,
b_ordered varchar(100) as (greatest(a, b)) STORED,
unique key unqBi_test_ab (a_ordered, b_ordered)
);
insert into testBiDirKey(a,b) values('a', 'b');
insert into testBiDirKey(a,b) values('b', 'a');
Error Code: 1062. Duplicate entry 'a-b' for key 'unqBi_test_ab'
This will treat null exactly as your current normal unique key, so
insert into testBiDirKey(a,b) values('a', null);
insert into testBiDirKey(a,b) values('a', null);
insert into testBiDirKey(a,b) values(null, 'a');
are all allowed. You can add coalesce(x,'') to only allow one empty value (either null OR '') if you want. If you verify your values before you add them (e.g. if they don't contain a ,), you can combine the two columns to just one, concatenated with an , - although with little benefit apart from just having 1 additional column.
For 5.7.8+, you don't need the STORED keyword anymore to be able to use these columns in an index. That keyword effects if the values are stored (using disk space) or calculated when required (default).
Before MySQL 5.7.6, you can use a trigger (on update and insert) to update the two columns with the these values, the same logic applies, it's just a little more code.

Related

How can I create a column whose entries depend on the contents of a column in another table?

I have a database with multiple tables, and I want to add a column to one table that will be populated with different strings based on the contents of another table.
Below are the tables of interest.
CREATE TABLE Locations(
id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
Location VARCHAR(17) NOT NULL,
Is_Property BOOLEAN NOT NULL
);
CREATE TABLE Players(
id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
Player_Name VARCHAR(17) NOT NULL,
Token VARCHAR(17) NOT NULL,
FOREIGN KEY (Token) REFERENCES Tokens(Token),
P_Location VARCHAR(17) NOT NULL,
FOREIGN KEY (P_Location) REFERENCES Locations(Location),
Bank_Balance INT NOT NULL DEFAULT 200);
ALTER TABLE Locations ADD INDEX `Location` (`Location`);
CREATE TABLE Properties AS SELECT id,Location FROM Locations
WHERE Is_Property = 1;
ALTER TABLE Properties
ADD CONSTRAINT PK_Properties PRIMARY KEY (id),
ADD COLUMN Colour VARCHAR(6),
ADD COLUMN Cost_And_Rent INT,
ADD COLUMN Owned VARCHAR(3);
CREATE TABLE Properties_Owned(
Player_id INT NOT NULL,
Prop_id INT NOT NULL,
PRIMARY KEY(Player_id, Prop_id),
FOREIGN KEY (Player_id) REFERENCES Players(id),
FOREIGN KEY (Prop_id) REFERENCES Properties(id));
The Properties and Properties_Owned tables are of interest in this case. I want to create a column called Owned in Properties and populate it with "Yes" or "No" based on if the primary key appears under Prop_id in Properties_Owned. Ergo if it does, Properties.Owned will show "Yes", and if not, "No".
I've tried using the CASE function, but I'm unsure of if it can be used without calling a SELECT query. Below is my last attempt to do so, but the syntax is wrong somewhere or just misguided altogether.
CASE
WHEN id IS IN properties_owned.Prop_id THEN Properties.Owned = "Yes"
ELSE "No" ;
It generates the error code:
Error Code: 1064. You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'CASE WHEN id IS IN properties_owned.Prop_id THEN Properties.Owned = "Yes" ELSE ' at line 1
Edit: As there was a request for sample data, here it is:
INSERT INTO Locations(Location,Is_Property) VALUES ("GO", 0),
("London", 1),
("Paris", 0),
("China", 1),
("New Zealand", 0),
("Sydney", 1),;
INSERT INTO Players(Player_Name,Token,P_Location,Bank_Balance) VALUES
("Mary","Battleship","London",190),
("Bill","Dog","Paris",500),
("Jane","Car","China",150),
("Norman","Thimble","London",250);
INSERT INTO Properties_Owned(Player_id,Prop_id) VALUES
(1,1),
(2,2),
(3,3),
(4,4),
(4,5);
Thus the Properties_Owned table will look like this:
Player_id | Prop_id
----------|---------
1 |1
2 |2
3 |3
4 |4
4 |5
And so in the Properties table under Owned, if Properties.id appears in Prop_id above, the Owned column should yield "Yes".
The table Properties_Owned reflects an (n:n)-relation (many-to-many). But according to your comment, a property cannot be owned by multiple players at the same time. And that would be an (n:1)-relation (many-to-one). In that case you do not need the Properties_Owned table and can just add a column Player_id (or owned_by_player_id) to the Properties table as foreign key referencing the Players table.
alter table Properties
add column Player_id int default null,
add foreign key (Player_id) references Players(id);
Then the information required for the Owned column will be already in the same table. You just need to "manipulate" it in your SELECT statements. For example with:
select
p.*,
case when Player_id is null then 'No' else 'Yes' end as Owned
from Properties p;
No need to store the same information redundantly. That would "bite" you sooner or later. Avoid redundancy when possible.
If your MySQL version (5.7+ required) supports Generated Columns you can also let the database maintain the redundancy.
alter table Properties
drop column Owned,
add column Owned varchar(3)
as (case when Player_id is null then 'No' else 'Yes' end) virtual;
Now the (genrated) column is dependent on Player_id column and you don't need (and cannot) store anything there but can select it. virtual means that it is not stored but generated (on the fly) when it's needed. Now you can read it in your queries as if it is normal column.
See example on db-fiddle.com
And again: Avoid redundant data when possible. At least use foreign keys to avoid data inconsistency.
You could create a view that extends your table by the column you want. There you can use a CASE statement.
CASE WHEN id IN SELECT Prop_id FROM properties_owned THEN 'Yes' ELSE 'No'
If this is not what you want, you could possibly use triggers on both tables that fill/update the column on on create/on delete
Initially you could fill the column with something like this:
UPDATE Prop_id SET properties_owned = CASE WHEN id IN SELECT Prop_id FROM properties_owned THEN 'Yes' ELSE 'No' WHERE

Manually updating primary key in a row causes subsequent insert to fail / Duplicate entry for key

I have a table with schema like this:
CREATE TABLE `things` (
`thing_id` INT(11) NOT NULL AUTO_INCREMENT,
`name` VARCHAR(32) NOT NULL DEFAULT '',
PRIMARY KEY (`thing_id`)
) ENGINE=INNODB DEFAULT CHARSET=utf8mb4;
And some sample data:
INSERT INTO things VALUES (NULL, 'thing 1'), (NULL, 'thing 2');
In my application, sometimes I want to bump the primary key of a row to be the highest in the table. For example, I want the thing_id of thing 1 changed to 3 (the table's current autoincrement value). Previously the table was MyISAM and this was accomplished as follows:
UPDATE things t
JOIN (SELECT MAX(thing_id) + 1 AS max_id FROM things) v
SET t.thing_id = v.max_id
WHERE thing_id = 1;
That part still works. However, now with InnoDB, the next insert fails since doing that update leaves the table's autoincrement value still pointing to the same thing. So now if I do an insert now like this:
INSERT INTO things VALUES (NULL, 'thing 3');
I will get an error like:
Duplicate entry '3' for key 'PRIMARY'
This was not an issue with MyISAM. How can I accomplish the same thing using InnoDB without running into this error? My guess is that there's a better way to change the thing_id which will keep the table's autoincrement value intact, but that's where I'm stumped.
And lastly, here's a dbfiddle:
https://www.db-fiddle.com/f/enJPVkwNN6hocjquw38BHD/0
Reference: Innodb Auto Increment Initialization
If you specify an AUTO_INCREMENT column for an InnoDB table, the table handle in the InnoDB data dictionary contains a special counter called the auto-increment counter that is used in assigning new values for the column. This counter is stored only in main memory, not on disk.
Update:
You can reset this counter using below query, works for InnoDB storage engine in MySQL 5.7
ALTER TABLE things AUTO_INCREMENT = 1;
Executing this is resetting auto-increment counter to Max + 1 value.

Prevent auto increment on duplicated entry?

I have Table called url_info and the structure of the table is:
url_info:
url_id ( auto_increment, primary key )
url ( unique,varchar(500) )
When I insert into table like this:
INSERT INTO url_info(url) VALUES('Tom');
INSERT INTO url_info(url) VALUES('Jerry');
The output is:
1 Tom
2 Jerry
When I insert like this
INSERT INTO url_info(url) VALUES('Tom');
INSERT INTO url_info(url) VALUES('Tom');
INSERT INTO url_info(url) VALUES('Jerry');
The output is
1 Tom
3 Jerry
The auto-increment id is incremented when I try to insert to duplicate entry. I have also tried Insert Ignore
How to prevent it from incrementing when I try to insert a duplicate entry?
It's probably worth creating a stored procedure to insert what you want into the table. But, in the stored procedure check what items you have already in the table. If these match what you're trying to insert, then the query should not even attempt the insert.
Ie. The procedure needs to contain something like this:
IF NOT EXISTS(SELECT TOP 1 url_id FROM url_info WHERE url = 'Tom')
INSERT INTO url_info(url) VALUES('Tom')
So, in your stored procedure, it would look like this (assuming the arguments/variables have been declared)
IF NOT EXISTS(SELECT TOP 1 url_id FROM url_info WHERE url = #newUrl)
INSERT INTO url_info(url) VALUES(#newUrl)
This is expected behaviour in InnoDB. The reason is that they want to let go of the auto_increment lock as fast as possible to improve concurrency. Unfortunately this means they increment the AUTO_INCREMENT value before resolving any constraints, such as UNIQUE.
You can read more about the idea in the manual on AUTO_INCREMENT Handling in InnoDB, but the manual is also unfortunately buggy and doesn't tell why your simple insert will give non-consecutive values.
If this is a real problem for you and you really need consecutive numbers, consider setting the innodb_autoinc_lock_mode option to 0 in your server, but this is not recommended as it will have severe effects on your database (you cannot do any inserts concurrently).
Auto_increment is performed updated by the engine. This is done before hand of checking a value is unique or not. And we can't roll back the operation to get back to former value of auto_increment.
Hence NO to start from where you last read on auto_increment.
And it is not an issue in loosing some intermediate values on auto_increment field.
The MAX value you can store into a SIGNED INT field is 2^31-1 equal to 2,147,483,647. If you read it loud, it sounds 2 billion+.
And I don't think it is small and won't suite your requirement.
CREATE TABLE `url_info` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`url` varchar(255) COLLATE utf8_unicode_ci NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci AUTO_INCREMENT=4 ;
When I execute:
INSERT INTO url_info(url) VALUES('Tom');
INSERT INTO url_info(url) VALUES('Tom');
INSERT INTO url_info(url) VALUES('Jerry');
I get:
Make sure you ID column is UNIQUE too.
As the manual says:
A UNIQUE index creates a constraint such that all values in the index
must be distinct. An error occurs if you try to add a new row with a
key value that matches an existing row. This constraint does not apply
to NULL values except for the BDB storage engine. For other engines, a
UNIQUE index permits multiple NULL values for columns that can contain
NULL. If you specify a prefix value for a column in a UNIQUE index,
the column values must be unique within the prefix.

ERROR 1062 (23000): Duplicate entry '2147483647' for key 'PRIMARY'

I have a table:
CREATE TABLE passenger_details
(
bank_card INT(20) NOT NULL AUTO_INCREMENT,
email VARCHAR(20),
mobile INT(15) NOT NULL,
p_id INT NOT NULL,
PRIMARY KEY (bank_card),
FOREIGN KEY (p_id) REFERENCES passenger(p_id)
);
INSERT INTO passenger_details
VALUES (0123012301230123,'blah_#hotmail.com',0872863492,1234);
select*from passenger_details;
+------------+--------------------+-----------+------+
| bank_card | email | mobile | p_id |
+------------+--------------------+-----------+------+
| 2147483647 | blah_#hotmail.com | 872863492 | 1234 |
+------------+--------------------+-----------+------+
1 row in set (0.00 sec)
As we can see, the previous value, just went wrong into table. Should be 16 numbers and not only 10, actually different numbers.
When i try to insert a new value:
INSERT INTO passenger_details
VALUES (1234258431681842,'blah#icloud.com',0895764829,123548);
I get this error:
ERROR 1062 (23000): Duplicate entry '2147483647' for key 'PRIMARY'
If bank_card is AUTO_INCREMENT why there is error? Should I change the PK to another place?
INT has a maximum signed value of 2147483647. Any number greater than that will be truncated to that value. What you need to do is change that column to be a varchar(20) which will hold strings of up to 20 characters since bank card numbers are strings and not actually numbers (you don't do math with the). You also should remove AUTO_INCREMENT as that is not a value you will be incrementing.
Something to ask yourself. How did the number get that big? I did not insert 2 billion rows!
Well, possibly you 'burned' that many AUTO_INCREMENT ids. This can happen in man ways:
INSERT IGNORE ... -- when the insert is ignored (because it the row already exists)
REPLACE
IODKU
and probably others.
With MySQL phpmyadmin panel , this is how I got rid from this issue
>Go to your Table's structure from phpmyadmin panel
>> select that PRIMARY column , click on change
>>> Change Column type to "BIGINT" and Attributes to "unsigned"
with other SQLs > change column attributes to "BIGINT unsigned"
that worked for me

where is the duplicate in ON DUPLICATE KEY query?

Description:
I am trying to insert user's preferences into a database. If the user hasn't yet placed any, I want a insert, otherwise, I want an update. I know I can insert default values in the creation of the user and than exclusively use update, but that adds another query (I think)
Problem:
I have read up on ON DUPLICATE KEY UPDATE but I don't understand it. This is almost the exact question I have but without the answer. The answer says:
It does sound like it will work for what you want to do as long as you hav the proper column(s) defined as UNIQUE KEY or PRIMARY KEY.
If I do a simple insert like so:
INSERT INTO table (color, size) VALUES ('blue', '18') ...
How will that ever produce at DUPLICATE KEY? As far as mysql knows it's just another insert and the id is auto-incremented. I have the primary key in the table set to unique, but the insert isn't going to check against that, right?
Table:
CREATE TABLE `firm_pref` (
`id` int(9) NOT NULL AUTO_INCREMENT,
`firm_id` int(9) NOT NULL, //user_id in this case
`header_title` varchar(99) NOT NULL,
`statement` varchar(99) NOT NULL,
`footer_content` varchar(99) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=latin1
Well, unless you want your application to be used by a single person only, you would have to specify someone's user_id in that INSERT - when this 'someone' guy or girl updates his/her preferences, right?
This field (user_id) is exactly what would be checked by ON DUPLICATE KEY UPDATE clause.
And if you want to insert a new record, just send NULL instead:
INSERT INTO table (user_id, color, size) VALUES (NULL, 'blue', 18);
... so auto-increment will have a chance to move on and save the day. )
UPDATE: Take note that to understand that some field should be considered a unique identifier, you should mark it as such. Usually it's done naturally, as this field is used as a PRIMARY KEY. But sometimes it's not enough; it means some work for UNIQUE constraint. For example, in your table it can be used like this:
CREATE TABLE `prefs` (
`id` int(9) NOT NULL AUTO_INCREMENT,
`firm_id` int(9) NOT NULL,
...
PRIMARY KEY (`id`),
UNIQUE KEY (`firm_id`)
);
(or you can add this constraint to the existing table with ALTER TABLE prefs ADD UNIQUE (firm_id) command)
Then insert/update query will look like...
INSERT INTO prefs(firm_id, header_title, statement, footer_content)
VALUES(17, 'blue', '18', 'some_footer')
ON DUPLICATE KEY UPDATE
header_title = 'blue',
statement = '18',
footer_content = 'some_footer';
I've built a sort of demo in SQL Fiddle. You can play with it some more to better understand that concept. )
For options, you would normally have an options table that has a list of available options (like color, size etc), and then a table that spans both your options table and users table with the users' values.
For example, your options table:
id | name
=========
1 | color
2 | size
Your users table:
id | name
================
1 | Martin Bean
And an options_users join table:
option_id | user_id | value
===========================
1 | 1 | Blue
2 | 1 | Large
With the correct foreign keys set up in your options_users table, you can have redundant values removed when an option or user is removed from your system. Also, when saving a user's preferences, you can first delete their previous answers and insert the new ones.
DELETE FROM `options_users`
WHERE `user_id` = #user_id;
INSERT INTO `options_users` (`option_id`, `user_id`, `value`)
VALUES (1, #user_id, 'Blue'), (2, #user_id, 'Large');
Hope that helps.