Yes, my friends, it's time for another EVIL MYSQL FEATURE!
Often used as an “upsert” (insert if not exists, else update), REPLACE INTO is another non-standard, MySQL-specific feature that makes one ask the question, “What were they thinking??”
Fortunately, once again, we have INSERT INTO...ON DUPLICATE KEY UPDATE which, while slightly less snappy to type, is pretty much always the better solution. But the developer has to know to use this statement and to toss REPLACE INTO into the fire. π₯ Again, standard SQL MERGE (which is also an upsert) is yet to be implemented in MySQL
REPLACE INTO suckage basically all stems from the fact that in the case when the row already exists, the row is deleted and a new one is inserted.
To dig into why that’s bad a little bit:
1. REPLACE INTO sucks for performance. I can show the full test details if anyone’s interested. But to briefly show the results of a benchmark I whipped up, that ran a whole bunch of REPLACE INTO vs. logically identical INSERT INTO...ON DUPLICATE KEY UPDATE statements on the same large table: REPLACE INTO: ~1m 9s vs. INSERT INTO...ON DUPLICATE KEY UPDATE: ~ 0m 21s (the discrepancy can be more or less pronounced depending on some factors such as number of indexes on the table; my test case had a few indexes)
2. It may cause unnecessary foreign key violations. Because REPLACE INTO runs a DELETE followed by an INSERT, it can trigger pointless foreign key violations if the table you’re working with is a parent table in a relationship.
Demo:
/* REPLACE INTO goes KABOOM */
mysql> REPLACE INTO parent (id, a, b) VALUES (1, 123, 456);
ERROR 1451 (23000): Cannot delete or update a parent row: a foreign key constraint fails (`core`.`child`, CONSTRAINT `fk_child_parent` FOREIGN KEY (`parent_id`) REFERENCES `parent` (`id`))
/* alternatively, INSERT INTO...ON DUPLICATE KEY UPDATE LGTM */
mysql> INSERT INTO parent (id, a, b)
-> VALUES (1, 123, 456) AS new
-> ON DUPLICATE KEY UPDATE
-> a = new.a,
-> b = new.b;
Query OK, 2 rows affected (0.00 sec)
mysql> SELECT * FROM parent WHERE id = 1; /* and we see the update worked */
+----+------+------+
| id | a | b |
+----+------+------+
| 1 | 123 | 456 |
+----+------+------+
1 row in set (0.00 sec)
3. Similar principle: if your foreign keys are defined as ON DELETE CASCADE, you won’t get an error, but you’ll get a surprise in that the child rows are deleted! All you want to do is to update or insert in the parent, and it kills all the child rows! Say we have the same parent and child tables, but this time, child’s foreign key was defined with ON DELETE CASCADE:
mysql> SELECT * FROM parent;
+----+------+------+
| id | a | b |
+----+------+------+
| 1 | 123 | 456 |
+----+------+------+
1 row in set (0.00 sec)
mysql> SELECT * FROM child; /* here are the children of parent id 1 */
+----+------+------+-----------+
| id | a | b | parent_id |
+----+------+------+-----------+
| 1 | 9 | 9 | 1 |
| 2 | 8 | 8 | 1 |
+----+------+------+-----------+
2 rows in set (0.00 sec)
mysql> REPLACE INTO parent (id, a, b) VALUES (1, 123, 456); /* DANGER */
Query OK, 2 rows affected (0.01 sec)
mysql> SELECT * FROM child; /* OH GOD NO!! */
Empty set (0.00 sec)
<snip rolling back the above change>
mysql> INSERT INTO parent (id, a, b) /* whereas this is safe */
-> VALUES (1, 123, 456) AS new
-> ON DUPLICATE KEY UPDATE
-> a = new.a,
-> b = new.b;
Query OK, 2 rows affected (0.00 sec)
mysql> SELECT * from child; /* whew... */
+----+------+------+-----------+
| id | a | b | parent_id |
+----+------+------+-----------+
| 1 | 9 | 9 | 1 |
| 2 | 8 | 8 | 1 |
+----+------+------+-----------+
2 rows in set (0.00 sec)
4. Your preexisting row might get assigned new auto-incremented ID value. If the key that REPLACE INTO is checking against is the ID/primary key, then it will retain the value as expected - no problem in this case. But say you have such an ID as your PK, and a unique constraint on another column. If the unique column is instead used to match (i. e. your REPLACE INTO statement doesn’t specify ID), the row receives a new ID value.
mysql> CREATE TABLE dummy (id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(10) UNIQUE, x INT); /* the key thing here is we have an auto-incremented ID and a unique constraint on name */
Query OK, 0 rows affected (0.01 sec)
mysql> INSERT INTO dummy (name, x) VALUES ('thing1', 1), ('thing2', 2); /* populate table with some data */
Query OK, 2 rows affected (0.00 sec)
Records: 2 Duplicates: 0 Warnings: 0
mysql> SELECT * FROM dummy; /* let's take a look at the newly inserted data, notice the id values */
+----+--------+------+
| id | name | x |
+----+--------+------+
| 1 | thing1 | 1 |
| 2 | thing2 | 2 |
+----+--------+------+
2 rows in set (0.00 sec)
mysql> REPLACE INTO dummy (name, x) VALUES ('thing1', 999); /* try to REPLACE INTO by unique name */
Query OK, 2 rows affected (0.00 sec)
mysql> SELECT * FROM dummy; /* notice id changed from 1 to 3!! OMG WTF :scream: */
+----+--------+------+
| id | name | x |
+----+--------+------+
| 2 | thing2 | 2 |
| 3 | thing1 | 999 |
+----+--------+------+
Maybe the above won’t happen too often, and maybe it doesn’t always matter, but I personally don’t like unexpected data changes, especially primary key changes (what if I have some cached data in my application/ORM? what if people tend to memorize or put IDs in stored requests — imagine those changing sporadically?) It’s just another odd case to worry about it - I’d rather remove that worry entirely:
mysql> INSERT INTO dummy (name, x)
-> VALUES ('thing1', 999) AS new
-> ON DUPLICATE KEY UPDATE
-> name = new.name,
-> x = new.x;
Query OK, 2 rows affected (0.00 sec)
mysql> SELECT * FROM dummy; /* id 1 is still id 1 (yay) */
+----+--------+------+
| id | name | x |
+----+--------+------+
| 1 | thing1 | 999 |
| 2 | thing2 | 2 |
+----+--------+------+
5. I have HEARD but could not whip up a quick test case to prove, that REPLACE INTO can cause more heavyweight locking/increase risk of deadlocks. (Source) However my quick tests on MySQL 8 showed the same amount of locking involved. TBD I suppose - maybe I’ll try to dig more deeply into it at some point.
All that said, if you ever really need a DELETE followed by an INSERT, REPLACE INTO may be justified. I feel like I’ve seen the need to do so on rare occasion, but I can’t think of a specific use case right now. But for upsert purposes, do not use REPLACE INTO. It is evil.
tl;dr: MySQL’s special REPLACE INTO statement is slower and breaks stuff more frequently then INSERT INTO … ON DUPLICATE KEY UPDATE . So instead of REPLACE INTO, use INSERT INTO … ON DUPLICATE KEY UPDATE.