Update multiple rows with unique constraint - mysql

I have two tables in my database, foo and bar. Multiple foos have one bar, and contain a second column that controls the ordering of foos with the same bar. The values used for ordering should be unique, so the table has a unique constraint:
CREATE TABLE bar (
id int NOT NULL AUTO_INCREMENT,
PRIMARY KEY (id)
);
CREATE TABLE foo (
id INT NOT NULL AUTO_INCREMENT,
bar INT,
`order` INT,
PRIMARY KEY (id),
FOREIGN KEY (bar) REFERENCES bar (id),
UNIQUE KEY (bar, order)
);
What is the most efficient way to update the ordering of the foos for one bar? For example, if I have this data:
id | bar | order
1 | 1 | 1
2 | 1 | 2
3 | 1 | 3
4 | 1 | 4
And want to reorder them (1, 2, 4, 3), I now have the following queries:
UPDATE foo SET `order` = NULL WHERE id IN (3, 4);
UPDATE foo SET `order` = 3 WHERE id = 4;
UPDATE foo SET `order` = 4 WHERE id = 3;
The first query is necessary to prevent an integrity error for the other updates. Can this be improved?

The only I can think is you should have your update values in a separated table/query to make it more generic and can work with multiple ID
newQuery
ID newOrder
3 4
4 3
You update your order to null before update because the integrity restriction.
UPDATE foo SET `order` = NULL WHERE id IN (SELECT ID FROM newQuery);
Then update with a JOIN
UPDATE foo AS f
INNER JOIN newQuery AS n ON f.id = n.id
SET f.order = n.newOrder

Related

MySQL constraint to ensure data integrity between two tables (1-n cardinality)

I made a minimal example to describe my problem using the concept of player using items in a game (which makes my problem easier to understand).
My problematic is the following. Let's say I have to store items and the player holding them in my database.
I didn't show it on the image but a player could hold no item but it's really important for my problem.
I translated that into :
Which gives the two following tables (with PK_ and #FK) :
In my example I can then fill them as such :
By doing :
Now, I want any player to have a "favorite" item, so I want to add a foreign key #item_id in the table Player but I want to make sure this new value refers to an item being hold by the right player in the Item table. How can I add a (check?) constraint to my table declarations so that condition is always true to ensure data integrity between my two tables? I don't think I can solve this by creating a third table since it's still a 1n cardinality. Also I don't want to ALTER the table I want to CREATE, since my database is not yet to be deployed.
You can add a third table which links the player and item table for the favorite item for each player (if you don't want a cycle reference between player and item). There are two restrictions to solve:
A player must own the item, they want to have as a favorite.
A player can have only one favorite item.
The first point can be solved with a multi-column foreign key. The table item needs an index for the playerId and the id of the item. Then your new table for the favorite items can reference this index as a foreign key. The inclusion of the playerId in the foreign key ensures that the item isn't "moved" to a different player while it is marked as favorite. The queries will look like this:
CREATE TABLE item(
id INT AUTO_INCREMENT PRIMARY KEY NOT NULL,
playerId INT NOT NULL,
INDEX item_id_and_playerId (playerId, id)
);
CREATE TABLE favItem(
id INT AUTO_INCREMENT PRIMARY KEY NOT NULL,
playerId INT NOT NULL,
itemId INT NOT NULL,
FOREIGN KEY favItem_FQ_id_and_playerId (playerId, itemId) REFERENCES item(playerId, id)
);
The second point can be solved by a simple UNIQUE constraint on the playerId column of the new favorite items table. That way only one item can be marked as favorite. We adjust the CREATE TABLE query for the favItem table as follow:
CREATE TABLE favItem(
id INT AUTO_INCREMENT PRIMARY KEY NOT NULL,
playerId INT NOT NULL,
itemId INT NOT NULL,
FOREIGN KEY favItem_FQ_id_and_playerId (playerId, itemId) REFERENCES item(playerId, id),
CONSTRAINT favItem_UQ_playerId UNIQUE (playerId)
);
See the following queries how they work:
mysql> SELECT * FROM player;
+----+
| id |
+----+
| 1 |
| 2 |
| 3 |
+----+
3 rows in set (0.00 sec)
mysql> SELECT * FROM item;
+----+----------+
| id | playerId |
+----+----------+
| 1 | 1 |
| 2 | 1 |
| 3 | 2 |
| 4 | 3 |
| 5 | 3 |
| 6 | 3 |
+----+----------+
6 rows in set (0.00 sec)
mysql> INSERT INTO favItem(playerId, itemId) VALUES (1, 2);
Query OK, 1 row affected (0.01 sec)
mysql> INSERT INTO favItem(playerId, itemId) VALUES (3, 2);
ERROR 1452 (23000): Cannot add or update a child row: a foreign key constraint fails
(`test`.`favItem`, CONSTRAINT `favItem_ibfk_1` FOREIGN KEY
(`playerId`, `itemId`) REFERENCES `item` (`playerId`, `id`))
mysql> INSERT INTO favItem(playerId, itemId) VALUES (1, 1);
ERROR 1062 (23000): Duplicate entry '1' for key 'favItem.favItem_UQ_playerId'
The first row can be added without problem. However, the second row can't be added because the player with the id playerId=3 does not "own" the item with the id itemId=2. The third row can't be added either because the player with the id playerId=1 cannot mark the item with the id itemId=1 (which he owns) as a favorite because of the UNIQUE constraint on playerId.
You can add an isFavorite column in the Item table. It's value is either NULL or a predefined value, like a ENUM value. Then you can add a UNIQUE constraint over the two columns playerId and isFavorite. That way, a player can have only one item as favorite, since multiple rows with the same playerId and isFavorite value would result in a unique constraint error. The table can look like this:
CREATE TABLE items(
id INT AUTO_INCREMENT PRIMARY KEY NOT NULL,
playerId INT NOT NULL,
isFavorite ENUM('FAV') NULL,
CONSTRAINT items_UQ_fav UNIQUE (PlayerId, isFavorite)
);
Check the following queries how a new row would validate the unique constraint:
mysql> INSERT INTO
items(playerId, isFavorite)
VALUES
(4, NULL), (7, NULL), (7, 'FAV'), (5, 'FAV');
Query OK, 4 rows affected (0.02 sec)
Records: 4 Duplicates: 0 Warnings: 0
mysql> SELECT * FROM items;
+----+----------+------------+
| id | playerId | isFavorite |
+----+----------+------------+
| 1 | 4 | NULL |
| 4 | 5 | FAV |
| 2 | 7 | NULL |
| 3 | 7 | FAV |
+----+----------+------------+
4 rows in set (0.00 sec)
mysql> INSERT INTO items(playerId, isFavorite) VALUES (5, 'FAV');
ERROR 1062 (23000): Duplicate entry '5-FAV' for key 'items.items_UQ_fav'

foreign Key and innodb engine insert

I couldn't insert salary values into this table
error massage Field 'Tid' doesn't have a default value
I have tried so many times by adding constraint or default fk but it doesn't work out
My code is this
CREATE TABLE Actor (
aid INTEGER NOT NULL AUTO_INCREMENT,
NAME VARCHAR(20),
PRIMARY KEY (aid)
) ENGINE=INNODB;
CREATE TABLE Movie (
mid INTEGER NOT NULL AUTO_INCREMENT,
NAME VARCHAR(30),
YEAR INTEGER,
PRIMARY KEY (mid)
) ENGINE=INNODB;
CREATE TABLE acts_in (
aid INTEGER NOT NULL AUTO_INCREMENT,
mid INTEGER NOT NULL,
PRIMARY KEY (aid, mid),
FOREIGN KEY (aid) REFERENCES Actor (aid),
FOREIGN KEY (mid) REFERENCES Movie (mid)
) ENGINE=INNODB;
SHOW TABLES;
INSERT INTO Actor(NAME)
VALUES ('Brad Pitt'), ('Edward Norton'), ('Julia Roberts');
INSERT INTO Movie (NAME, YEAR)
VALUES ('Fight Club', 1999), ('Ocean\s Eleven', 2001), ('Fury', 2014), ('The Incredible Hulk', 2008), ('Closer', 2004);
INSERT INTO acts_in (aid, mid)
VALUES (1, 1), (1, 2), (1, 3),(2, 1), (2, 4), (3,2), (3, 5);
ALTER TABLE Acts_in
ADD COLUMN Salary INTEGER NOT NULL;
INSERT INTO acts_in (Salary)
VALUES (2000), (4000), (5000), (50), (7000), (750000), (888888);
Your acts_in table is a child table. The parent tables of acts_in table are:
actor
movie
So you cannot add or update a child table row if the corresponding foreign key values don't exist in the parent tables.
In this case you cannot execute this query:
INSERT INTO acts_in (Salary)
VALUES (2000), (4000), (5000), (50), (7000), (750000), (888888);
Look you are trying to add or update a child table but you didn't set any value of aid and mid.
Try instead the following queries to update the salaries:
UPDATE acts_in SET Salary = 2000 WHERE aid = 1 AND mid = 1;
UPDATE acts_in SET Salary = 4000 WHERE aid = 1 AND mid = 2;
UPDATE acts_in SET Salary = 5000 WHERE aid = 1 AND mid = 3;
UPDATE acts_in SET Salary = 50 WHERE aid = 2 AND mid = 1;
UPDATE acts_in SET Salary = 7000 WHERE aid = 2 AND mid = 4;
UPDATE acts_in SET Salary = 750000 WHERE aid = 3 AND mid = 2;
UPDATE acts_in SET Salary = 888888 WHERE aid = 3 AND mid = 5;
OR
this single query:
UPDATE
acts_in
SET Salary = CASE WHEN aid = 1 AND mid = 1 THEN 2000
WHEN aid = 1 AND mid = 2 THEN 4000
WHEN aid = 1 AND mid = 3 THEN 5000
WHEN aid = 2 AND mid = 1 THEN 50
WHEN aid = 2 AND mid = 4 THEN 7000
WHEN aid = 3 AND mid = 2 THEN 750000
WHEN aid = 3 AND mid = 5 THEN 888888
END
WHERE (aid,mid) IN ((1,1),(1,2),(1,3),(2,1),(2,4),(3,2),(3,5));
Note: In this case you are trying to update a child table where the corresponding foreign keys (in this case aid and mid) are containing valid values.
Check this SQL FIDDLE.

How to update on cascade in MySQL?

Let's look at this example database:
As we can see, person depends on the city (person.city_id is a foreign key). I don't delete rows, I just set them inactive (active=0). After setting city inactive, how can I automatically set all persons who are dependent on this city inactive? Is there a better way than writing triggers?
EDIT: I am interested only in setting person's rows inactive, not setting them active.
Here's a solution that uses cascading foreign keys to do what you describe:
mysql> create table city (
id int not null auto_increment,
name varchar(45),
active tinyint,
primary key (id),
unique key (id, active));
mysql> create table person (
id int not null auto_increment,
city_id int,
active tinyint,
primary key (id),
foreign key (city_id, active) references city (id, active) on update cascade);
mysql> insert into city (name, active) values ('New York', 1);
mysql> insert into person (city_id, active) values (1, 1);
mysql> select * from person;
+----+---------+--------+
| id | city_id | active |
+----+---------+--------+
| 1 | 1 | 1 |
+----+---------+--------+
mysql> update city set active = 0 where id = 1;
mysql> select * from person;
+----+---------+--------+
| id | city_id | active |
+----+---------+--------+
| 1 | 1 | 0 |
+----+---------+--------+
Tested on MySQL 5.5.31.
Maybe you should reconsider how you define a person to be active.. Instead of having active defined twice, you should just keep it in the city table and have your SELECT statements return Person WHERE city.active = 1..
But if you must.. you could do something like:
UPDATE city C
LEFT JOIN person P ON C.id = P.city
SET C.active = 0 AND P.active = 0
WHERE C.id = #id

MySql unique index over two colums - auto increment [duplicate]

I have multiple databases with the same structure in which data is sometimes copied across. In order to maintain data integrity I am using two columns as the primary key. One is a database id, which links to a table with info about each database. The other is a table key. It is not unique because it may have multiple rows with this value being the same, but different values in the database_id column.
I am planning on making the two columns into a joint primary key. However I also want to set the table key to auto increment - but based on the database_id column.
EG, With this data:
table_id database_id other_columns
1 1
2 1
3 1
1 2
2 2
If I am adding data that includes the dabase_id of 1 then I want table_id to be automatically set to 4. If the dabase_id is entered as 2 then I want table_id to be automatically set to 3. etc.
What is the best way of achieving this in MySql.
if you are using myisam
http://dev.mysql.com/doc/refman/5.0/en/example-auto-increment.html
For MyISAM and BDB tables you can
specify AUTO_INCREMENT on a secondary
column in a multiple-column index. In
this case, the generated value for the
AUTO_INCREMENT column is calculated as
MAX(auto_increment_column) + 1 WHERE
prefix=given-prefix. This is useful
when you want to put data into ordered
groups.
CREATE TABLE animals (
grp ENUM('fish','mammal','bird') NOT NULL,
id MEDIUMINT NOT NULL AUTO_INCREMENT,
name CHAR(30) NOT NULL,
PRIMARY KEY (grp,id)
) ENGINE=MyISAM;
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;
Which returns:
+--------+----+---------+
| grp | id | name |
+--------+----+---------+
| fish | 1 | lax |
| mammal | 1 | dog |
| mammal | 2 | cat |
| mammal | 3 | whale |
| bird | 1 | penguin |
| bird | 2 | ostrich |
+--------+----+---------+
For your example:
mysql> CREATE TABLE mytable (
-> table_id MEDIUMINT NOT NULL AUTO_INCREMENT,
-> database_id MEDIUMINT NOT NULL,
-> other_column CHAR(30) NOT NULL,
-> PRIMARY KEY (database_id,table_id)
-> ) ENGINE=MyISAM;
Query OK, 0 rows affected (0.03 sec)
mysql> INSERT INTO mytable (database_id, other_column) VALUES
-> (1,'Foo'),(1,'Bar'),(2,'Baz'),(1,'Bam'),(2,'Zam'),(3,'Zoo');
Query OK, 6 rows affected (0.00 sec)
Records: 6 Duplicates: 0 Warnings: 0
mysql> SELECT * FROM mytable ORDER BY database_id,table_id;
+----------+-------------+--------------+
| table_id | database_id | other_column |
+----------+-------------+--------------+
| 1 | 1 | Foo |
| 2 | 1 | Bar |
| 3 | 1 | Bam |
| 1 | 2 | Baz |
| 2 | 2 | Zam |
| 1 | 3 | Zoo |
+----------+-------------+--------------+
6 rows in set (0.00 sec)
Here's one approach when using innodb which will also be very performant due to the clustered composite index - only available with innodb...
http://dev.mysql.com/doc/refman/5.0/en/innodb-index-types.html
drop table if exists db;
create table db
(
db_id smallint unsigned not null auto_increment primary key,
next_table_id int unsigned not null default 0
)engine=innodb;
drop table if exists tables;
create table tables
(
db_id smallint unsigned not null,
table_id int unsigned not null default 0,
primary key (db_id, table_id) -- composite clustered index
)engine=innodb;
delimiter #
create trigger tables_before_ins_trig before insert on tables
for each row
begin
declare v_id int unsigned default 0;
select next_table_id + 1 into v_id from db where db_id = new.db_id;
set new.table_id = v_id;
update db set next_table_id = v_id where db_id = new.db_id;
end#
delimiter ;
insert into db (next_table_id) values (null),(null),(null);
insert into tables (db_id) values (1),(1),(2),(1),(3),(2);
select * from db;
select * from tables;
you can make the two column primary key unique and the auto-increment key primary.
The solution provided by DTing is excellent and working. But when tried the same in AWS Aurora, it didn't worked and complaining the below error.
Error Code: 1075. Incorrect table definition; there can be only one auto column and it must be defined as a key
Hence suggesting json based solution here.
CREATE TABLE DB_TABLE_XREF (
db VARCHAR(36) NOT NULL,
tables JSON,
PRIMARY KEY (db)
)
Have the first primary key outside, and second primary key inside the json and make second primary key value as auto_incr_sequence.
INSERT INTO `DB_TABLE_XREF`
(`db`, `tables`)
VALUES
('account_db', '{"user_info": 1, "seq" : 1}')
ON DUPLICATE KEY UPDATE `tables` =
JSON_SET(`tables`,
'$."user_info"',
IFNULL(`tables` -> '$."user_info"', `tables` -> '$."seq"' + 1),
'$."seq"',
IFNULL(`tables` -> '$."user_info"', `tables` -> '$."seq"' + 1)
);
And the output is like below
account_db {"user_info" : 1, "user_details" : 2, "seq" : 2}
product_db {"product1" : 1, "product2" : 2, "product3" : 3, "seq" : 3}
If your secondary keys are huge, and afraid of using json, then i would suggest to have stored procedure, to check for MAX(secondary_column) along with lock like below.
SELECT table_id INTO t_id FROM DB_TABLE_XREF WHERE database = db_name AND table = table_name;
IF t_id = 0 THEN
SELECT GET_LOCK(db_name, 10) INTO acq_lock;
-- CALL debug_msg(TRUE, "Acquiring lock");
IF acq_lock = 1 THEN
SELECT table_id INTO t_id FROM DB_TABLE_XREF WHERE database_id = db_name AND table = table_name;
-- double check lock
IF t_id = 0 THEN
SELECT IFNULL((SELECT MAX(table_id) FROM (SELECT table_id FROM DB_TABLE_XREF WHERE database = db_name) AS something), 0) + 1 into t_id;
INSERT INTO DB_TABLE_XREF VALUES (db_name, table_name, t_id);
END IF;
ELSE
-- CALL debug_msg(TRUE, "Failed to acquire lock");
END IF;
COMMIT;

mysql two column primary key with auto-increment

I have multiple databases with the same structure in which data is sometimes copied across. In order to maintain data integrity I am using two columns as the primary key. One is a database id, which links to a table with info about each database. The other is a table key. It is not unique because it may have multiple rows with this value being the same, but different values in the database_id column.
I am planning on making the two columns into a joint primary key. However I also want to set the table key to auto increment - but based on the database_id column.
EG, With this data:
table_id database_id other_columns
1 1
2 1
3 1
1 2
2 2
If I am adding data that includes the dabase_id of 1 then I want table_id to be automatically set to 4. If the dabase_id is entered as 2 then I want table_id to be automatically set to 3. etc.
What is the best way of achieving this in MySql.
if you are using myisam
http://dev.mysql.com/doc/refman/5.0/en/example-auto-increment.html
For MyISAM and BDB tables you can
specify AUTO_INCREMENT on a secondary
column in a multiple-column index. In
this case, the generated value for the
AUTO_INCREMENT column is calculated as
MAX(auto_increment_column) + 1 WHERE
prefix=given-prefix. This is useful
when you want to put data into ordered
groups.
CREATE TABLE animals (
grp ENUM('fish','mammal','bird') NOT NULL,
id MEDIUMINT NOT NULL AUTO_INCREMENT,
name CHAR(30) NOT NULL,
PRIMARY KEY (grp,id)
) ENGINE=MyISAM;
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;
Which returns:
+--------+----+---------+
| grp | id | name |
+--------+----+---------+
| fish | 1 | lax |
| mammal | 1 | dog |
| mammal | 2 | cat |
| mammal | 3 | whale |
| bird | 1 | penguin |
| bird | 2 | ostrich |
+--------+----+---------+
For your example:
mysql> CREATE TABLE mytable (
-> table_id MEDIUMINT NOT NULL AUTO_INCREMENT,
-> database_id MEDIUMINT NOT NULL,
-> other_column CHAR(30) NOT NULL,
-> PRIMARY KEY (database_id,table_id)
-> ) ENGINE=MyISAM;
Query OK, 0 rows affected (0.03 sec)
mysql> INSERT INTO mytable (database_id, other_column) VALUES
-> (1,'Foo'),(1,'Bar'),(2,'Baz'),(1,'Bam'),(2,'Zam'),(3,'Zoo');
Query OK, 6 rows affected (0.00 sec)
Records: 6 Duplicates: 0 Warnings: 0
mysql> SELECT * FROM mytable ORDER BY database_id,table_id;
+----------+-------------+--------------+
| table_id | database_id | other_column |
+----------+-------------+--------------+
| 1 | 1 | Foo |
| 2 | 1 | Bar |
| 3 | 1 | Bam |
| 1 | 2 | Baz |
| 2 | 2 | Zam |
| 1 | 3 | Zoo |
+----------+-------------+--------------+
6 rows in set (0.00 sec)
Here's one approach when using innodb which will also be very performant due to the clustered composite index - only available with innodb...
http://dev.mysql.com/doc/refman/5.0/en/innodb-index-types.html
drop table if exists db;
create table db
(
db_id smallint unsigned not null auto_increment primary key,
next_table_id int unsigned not null default 0
)engine=innodb;
drop table if exists tables;
create table tables
(
db_id smallint unsigned not null,
table_id int unsigned not null default 0,
primary key (db_id, table_id) -- composite clustered index
)engine=innodb;
delimiter #
create trigger tables_before_ins_trig before insert on tables
for each row
begin
declare v_id int unsigned default 0;
select next_table_id + 1 into v_id from db where db_id = new.db_id;
set new.table_id = v_id;
update db set next_table_id = v_id where db_id = new.db_id;
end#
delimiter ;
insert into db (next_table_id) values (null),(null),(null);
insert into tables (db_id) values (1),(1),(2),(1),(3),(2);
select * from db;
select * from tables;
you can make the two column primary key unique and the auto-increment key primary.
The solution provided by DTing is excellent and working. But when tried the same in AWS Aurora, it didn't worked and complaining the below error.
Error Code: 1075. Incorrect table definition; there can be only one auto column and it must be defined as a key
Hence suggesting json based solution here.
CREATE TABLE DB_TABLE_XREF (
db VARCHAR(36) NOT NULL,
tables JSON,
PRIMARY KEY (db)
)
Have the first primary key outside, and second primary key inside the json and make second primary key value as auto_incr_sequence.
INSERT INTO `DB_TABLE_XREF`
(`db`, `tables`)
VALUES
('account_db', '{"user_info": 1, "seq" : 1}')
ON DUPLICATE KEY UPDATE `tables` =
JSON_SET(`tables`,
'$."user_info"',
IFNULL(`tables` -> '$."user_info"', `tables` -> '$."seq"' + 1),
'$."seq"',
IFNULL(`tables` -> '$."user_info"', `tables` -> '$."seq"' + 1)
);
And the output is like below
account_db {"user_info" : 1, "user_details" : 2, "seq" : 2}
product_db {"product1" : 1, "product2" : 2, "product3" : 3, "seq" : 3}
If your secondary keys are huge, and afraid of using json, then i would suggest to have stored procedure, to check for MAX(secondary_column) along with lock like below.
SELECT table_id INTO t_id FROM DB_TABLE_XREF WHERE database = db_name AND table = table_name;
IF t_id = 0 THEN
SELECT GET_LOCK(db_name, 10) INTO acq_lock;
-- CALL debug_msg(TRUE, "Acquiring lock");
IF acq_lock = 1 THEN
SELECT table_id INTO t_id FROM DB_TABLE_XREF WHERE database_id = db_name AND table = table_name;
-- double check lock
IF t_id = 0 THEN
SELECT IFNULL((SELECT MAX(table_id) FROM (SELECT table_id FROM DB_TABLE_XREF WHERE database = db_name) AS something), 0) + 1 into t_id;
INSERT INTO DB_TABLE_XREF VALUES (db_name, table_name, t_id);
END IF;
ELSE
-- CALL debug_msg(TRUE, "Failed to acquire lock");
END IF;
COMMIT;