Deleting rows from multiple tables in MySQL - mysql

I am trying to delete a project from the projects table and all the images associated with that project in the images table.
Lets say p_id = 10
DELETE FROM projects, images WHERE projects.p_id = ? AND images.p_id = ?
What is wrong with this query?

DELETE projects, images
FROM projects, images
WHERE projects.p_id = ?
AND projects.p_id = images.p_id;

As Chacha102 noted, the problem of your query was the AND in the WHERE clause.
However, you may want to use the JOIN syntax for multi-table DELETEs, which I find easier to read:
DELETE projects, images
FROM projects
LEFT JOIN images ON images.p_id = projects.p_id
WHERE projects.p_id = 10;

DELETE FROM projects, images WHERE projects.p_id = ? or images.p_id = ?
When being deleted, an item will never meet both of these requirements, therefore it must be OR not AND

The answer
DELETE FROM p, i
USING projects p, images i
WHERE p.p_id = ?
AND p.p_id = i.p_id
The test
projects
create table projects (
p_id int unsigned not null auto_increment primary key
);
insert into projects (p_id) values (1),(2),(3);
select * from projects;
-- +------+
-- | p_id |
-- +------+
-- | 1 |
-- | 2 |
-- | 3 |
-- +------+
images
create table images (
i_id int unsigned not null auto_increment primary key,
p_id int unsigned default null
);
insert into images (p_id) values (1),(1),(1),(2),(2),(3),(3);
select * from images;
-- +------+------+
-- | i_id | p_id |
-- +------+------+
-- | 1 | 1 |
-- | 2 | 1 |
-- | 3 | 1 |
-- | 4 | 2 |
-- | 5 | 2 |
-- | 6 | 3 |
-- | 7 | 3 |
-- +------+------+
the delete
delete from p, i
using projects p, images i
where p.p_id = i.p_id
and p.p_id = 1;
the result
select * from projects;
-- +------+
-- | p_id |
-- +------+
-- | 2 |
-- | 3 |
-- +------+
select * from images;
-- +------+------+
-- | i_id | p_id |
-- +------+------+
-- | 4 | 2 |
-- | 5 | 2 |
-- | 6 | 3 |
-- | 7 | 3 |
-- +------+------+
works a treat!

You should use two separate queries to do that :
delete from images where p_id = 123;
delete from projects where p_id = 123;
i.e. :
First, delete the images, that depend on the project (foreign key ? )
And, when nothing depends on the project anymore, delete the project itself.
And, as a security precaution, you should wrap all this in a transaction, to get a all or nothing behavior -- well, if you are using a storage engine that suppors transactions, like InnoDb.
See 12.3.1. START TRANSACTION, COMMIT, and ROLLBACK Syntax, about that, in the MySQL Manual.

Change the AND into an OR.
You might want to use a foreign key constraint with a cascading delete, much easier, but you have to use innoDB and create this FK-constraint. Delete the project and all related images will be deleted as well.

(Wrong answer, MySQL allows this)
You can't delete from two tables in one query.
The closest you can get is wrap the two deletes in a transaction:
begin transaction
delete from projects where p_id = ?
delete from images where p_id = ?
commit transaction

Related

Show warning when deleting rows with CASCADE deletes

Is there any option (or even DB client) to require a confirmation when deleting a row for CASCADE deletes. For example:
DELETE FROM catalogs WHERE id=1
Warning: this will also DELETE CASCADE 92,358 products and 142 catalog_histories. Are you sure? (If so, temporarily turn on ALLOW_CASCADE_DELETES or add WITH CASCADES to query)
I will prefer a solution that doesn't require following particular rules in db schema and works with MySQL. (Alternatively if there is a way to force FK deletes while leaving CASCADE off schema in general then this will also be a good option.)
You can't do that in mysql
i tried it a bit https://dbfiddle.uk/?rdbms=mysql_8.0&fiddle=b6575f084b486025a66f6d52a4df9893
but the foreign key restaont has to be enable before you start to delete the row like in the example
So you must run two codes one with SET FOREIGN_KEY_CHECKS
CREATE TABLE catalogs(id int PRIMARY KEY);
INSERT INTO catalogs VALUES (1),(2);
CREATE TABLE catalogs_sub(id int, cat_id int,
FOREIGN KEY (cat_id)
REFERENCES catalogs( id)
ON DELETE CASCADE
)
INSERT INTO catalogs_sub VALUES (1,1),(2,1),(3,2),(4,2)
SELECT * FROM catalogs_sub
id | cat_id
-: | -----:
1 | 1
2 | 1
3 | 2
4 | 2
SET FOREIGN_KEY_CHECKS=0;
DELETE FROM catalogs WHERE id=1;
SET FOREIGN_KEY_CHECKS=1;
SELECT * FROM catalogs
| id |
| -: |
| 2 |
SELECT * FROM catalogs_sub
id | cat_id
-: | -----:
1 | 1
2 | 1
3 | 2
4 | 2
DELETE FROM catalogs WHERE id=2;
SELECT * FROM catalogs
| id |
| -: |
SELECT * FROM catalogs_sub
id | cat_id
-: | -----:
1 | 1
2 | 1
db<>fiddle here

MySQL reorder row id by date

SELECT time
FROM posts
ORDER BY time ASC;
This will order my posts for me in a list. I would like to reorder the table itself making sure that there are no missing table ids. Thus, if I delete column 2, I can reorder so that row 3 will become row 2.
How can I do this? Reorder a table by its date column so there is always an increment of 1, no non-existing rows.
Disclaimer: I don't really know why you would need to do it, but if you do, here is just one of many ways, fairly independent of the engine or the server version.
Setup:
CREATE TABLE t (
`id` int(11) NOT NULL AUTO_INCREMENT,
`time` time DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB;
INSERT INTO t (`time`) VALUES ('13:00:00'),('08:00:00'),('02:00:00');
DELETE FROM t WHERE id = 2;
Initial condition:
SELECT * FROM t ORDER BY `time`;
+----+----------+
| id | time |
+----+----------+
| 3 | 02:00:00 |
| 1 | 13:00:00 |
+----+----------+
2 rows in set (0.00 sec)
Action:
CREATE TRIGGER tr AFTER UPDATE ON t FOR EACH ROW SET #id:=#id+1;
ALTER TABLE t ADD COLUMN new_id INT NOT NULL AFTER id;
SET #id=1;
UPDATE t SET new_id=#id ORDER BY time;
DROP TRIGGER tr;
Result:
SELECT * FROM t ORDER BY `time`;
+----+--------+----------+
| id | new_id | time |
+----+--------+----------+
| 3 | 1 | 02:00:00 |
| 1 | 2 | 13:00:00 |
+----+--------+----------+
2 rows in set (0.00 sec)
Cleanup:
Further you can do whatever is more suitable for your case (whatever is faster and less blocking, depending on other conditions). You can update the existing id column and then drop the extra one:
UPDATE t SET id=new_id;
ALTER TABLE t DROP new_id;
SELECT * FROM t ORDER BY `time`;
+----+----------+
| id | time |
+----+----------+
| 1 | 02:00:00 |
| 2 | 13:00:00 |
+----+----------+
2 rows in set (0.00 sec)
Or you can drop the existing id column and promote new_id to the primary key.
Comments:
A natural variation of the same approach would be to wrap it into a stored procedure. It's basically the same, but requires a bit more text. The benefit of it is that you could keep the procedure for the next time you need it.
Assuming you have a unique index on id, a temporary column new_id is needed in a general case, because if you start updating id directly, you can get a unique key violation. It shouldn't happen if your id is already ordered properly, and you are only removing gaps.

Mysql/Maridb Update is not working

Suppose a table contains data like
MariaDB [c]> select * from t2;
+-----+
| abc |
+-----+
| 1 |
| 3 |
| 5 |
+-----+
Suppose my update command is
MariaDB [c]> update t2 set abc = abc+2;
It give following error
ERROR 1062 (23000): Duplicate entry '3' for key 'PRIMARY'
While the above command works fine in oracle 10g, Is it some kind of bug or what?
The following is just an illustration and trivial.
create table t2
( id int auto_increment primary key,
abc int not null,
unique key(abc)
);
insert t2(abc) values (1),(3),(5); -- 3 rows added
update t2 set abc = abc+2; -- Error Code 1062: Duplicate entry '3' for key 'abc'
The above error occurred because the update marched in the order of the primary key, also the physical ordering, and changing the 1 to a 3 violated the 3 that was already in place via the unique key. The fact that the end state would make every thing OK, ideally, doesn't keep it from failing at that moment.
To illustrate this working in this highly rigged example knowing there is no other data:
truncate table t2; -- the data wasn't modified but for the paranoid, clear and re-insert
insert t2(abc) values (1),(3),(5); -- 3 rows added
Try it bottom up (so that the Unique constraint is not violated):
update t2 set abc = abc+2 order by abc desc;
select * from t2;
+----+-----+
| id | abc |
+----+-----+
| 1 | 3 |
| 2 | 5 |
| 3 | 7 |
+----+-----+
It leverages the ability to have an order by in an update statement.
So it comes down to knowing your data and what you can get away with. Saying it worked on Oracle as you did in comments is on another db platform and with some other schema. So that is mute.

Restrict insertion based on a count

So, I need to safely restrict the insertion of entries in a table based on the count of other entries in that same table. Say we have the following table:
resource:(id, foreign_key)
I need to create up to a number of entries based on the foreign key. So, as soon as I reach a count, let's say 100 for our example, I want to restrict creating more entries.
The obvious answer would be something like that:
count the entries with the specified foreign key.
if count < limit insert the new entry
And in fact, that's what I have been using. The thing is, this approach is not fail-proof since between 1 and 2 there might occur another insertion. I considered the possibility of using transactions but (unless I'm completely misunderstanding transactions) this has the same issue:
start transaction
insert the new entry
if entries have exceeded the limit, rollback. otherwise commit
Now, say we already have 99/100 entries and two transactions run at the same time. They both will commit since they don't see each-other's entries.
Short of actually creating the entry and then delete it if it's invalid (which feels kindof messy in my mind) I can't think of a way to solve this issue. Any ideas?
edit: upon request I'm providing sample data:
table1
+-------------+------------------+------+-----+----------------+
| Field | Type | Null | Key | Extra |
+-------------+------------------+------+-----+----------------+
| id | int(10) unsigned | NO | PRI | auto_increment |
| limit | int(10) unsigned | NO | MUL | |
+-------------+------------------+------+-----+----------------+
table2
+-------------+------------------+------+-----+----------------+
| Field | Type | Null | Key | Extra |
+-------------+------------------+------+-----+----------------+
| id | int(10) unsigned | NO | PRI | auto_increment |
| foreign_id | int(10) unsigned | NO | MUL | |
+-------------+------------------+------+-----+----------------+
and some sample data:
table1
+----+----------+
| id | limit |
+----+----------+
| 1 | 5 |
+----+----------+
table2
+----+---------------+
| id | foreign_id |
+----+---------------+
| 1 | 1 |
+----+---------------+
| 2 | 1 |
+----+---------------+
| 3 | 1 |
+----+---------------+
| 4 | 1 |
+----+---------------+
At this point, let's say that two users attempt to create table2 entries. The first one will have to be accepted and the 2nd rejected.
With the first approach, if both users go through step 1 (counting the old entries) and then through step 2 (insert the new entry) both entries will be created.
With the second approach, if both of them run at the same time, they both will count 4 slots before themselves and commit instead of one of them rollbacking.
Halo Mate, a Stored Procedure similar to this structure may help you
UPDATE
DROP PROCEDURE IF EXISTS sp_insert_record;
DELIMITER //
CREATE PROCEDURE sp_insert_record(
IN insert_value1 INT(9),
IN chosen_id INT(9)
)
BEGIN
SELECT id, `limit`
INTO #id, #limit
FROM table1
WHERE id = chosen_id;
START TRANSACTION;
INSERT INTO table2 (id, foreign_id)
VALUES (insert_value1, chosen_id);
SELECT COUNT(id)
INTO #count
FROM table2
WHERE foreign_id = #id;
IF #count <= #limit THEN
COMMIT;
ELSE
ROLLBACK;
END IF;
END//
DELIMITER ;
By using a Stored Procedure, you can also add any validation or process based on your requirements.
Hope this can be of help, cheers!

MySQL query to reorder field values?

I have table like this:
===============
| rank | name |
===============
| 3 | john |
| 6 | bob |
| 10 | alex |
| 11 | brad |
| 12 | matt |
| 34 | luke |
| 145 | ben |
===============
(this table is an example. In reality my table consists of ~5000 rows of data).
Is there a query to reorder the rank values starting from 1 and going up so it ends up like this:
===============
| rank | name |
===============
| 1 | john |
| 2 | bob |
| 3 | alex |
| 4 | brad |
| 5 | matt |
| 6 | luke |
| 7 | ben |
===============
It would be preferable to do this in 1 or 2 queries, not 1 query for each row since my table has 5000+ rows.
EDIT: Sorry I wasn't clear. I am trying to UPDATE the values in the database.
This is a little crude but will work in a pinch.
First order your table correctly just incase
ALTER TABLE tablename ORDER BY rank
Then drop the column
ALTER TABLE tablename DROP rank
Then add it again, with auto increment
ALTER TABLE tablename ADD COLUMN rank INT NOT NULL AUTO_INCREMENT FIRST
The auto increment will take care of numbering them in order, plus you don't have to loop through each row.
Here is the solution I came up with for this problem:
1.Create a temporary table without any keys
CREATE TEMPORARY TABLE tempTable (
id INT(11) NOT NULL
)
COLLATE='latin1_swedish_ci'
ENGINE=MyISAM
ROW_FORMAT=DEFAULT;
2.Populate the temporary table with data from the original table, ordered by rank
INSERT INTO tempTable SELECT id FROM myTable ORDER BY rank;
3.Add auto-incrementing rank column, giving all rows a unique rank, counting up from 1
ALTER TABLE tempTable
ADD COLUMN `rank` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
ADD PRIMARY KEY (`rank`);
4.Update the original table with a join to the temp table, overriding the original ranks
UPDATE myTable
INNER JOIN tempTable
ON myTable.id = tempTable.id
SET myTable.rank = tempTable.rank;
5.Drop the temp table
DROP TABLE tempTable;
An alternative to a strict MySQL solution would be to loop through the rows with a scripting language. Not a great idea if you have a large table, but could be acceptable if this is a one time fix.
In PHP
$db = mysql_connect('localhost', 'user', 'password');
mysql_select_db('database', $db);
$result = mysql_query("SELECT rank
FROM myTable
ORDER BY rank");
$i = 1;
while ($row = mysql_fetch_assoc($result)) {
mysql_query("UPDATE myTable
SET rank = " . $i++ . "
WHERE rank = " . $row['rank']);
}
Note that this will only work if rank is unique and you traverse in an order.
set #a:=(select max(id) from mytable)+1;
update mytable set id=(#a:=#a+1)
order by id;
set #a := 0;
update mytable set id=(#a:=#a+1)
order by id;
simple way, work for me. easy way.