14

I'm trying to insert a new row, but if the key already exists, I want to update the row ONLY if a certain other value is in the table is different. Is this possible in a mysql query/statement?

My table consist of the following columns: hat, mittens, name, last_update

hat+mittens make up the unique index (say the values for "hat" and "mittens" are colors)

Let's assume this is already in the table:

1. hat=blue mittens=green name=george last_update=tuesday
2. hat=red mittens=green name=bill last_update=monday

On a new key, I want to insert as usual. On duplicate key, I want to do an update ONLY IF the name changes, otherwise ignore. The reason for this is that I want to preserve the last_update value (timestamp).

hat=yellow mittens=purple name=jimmy -- insert new row
hat=blue mittens=green name=george -- ignore 
hat=blue mittens=green name=betty -- update row

Is this possible without using separate statements to first look up the existing row, compare values and then issue an update if necessary? If so, what would be the syntax?


Thanks for your responses. I tried all of them. Indeed, using just a simple UPDATE statement like

update tbl set name='george' where hat='blue' and mittens='green'

results in no rows being updated. But, using either

 INSERT INTO tbl (hat,mittens,name) VALUES ('blue','green','george') ON DUPLICATE KEY UPDATE name='george';

or

INSERT INTO tbl (hat, mittens, name) VALUES ('blue','green','george') ON DUPLICATE KEY UPDATE name=CASE WHEN name <> VALUES(name) THEN VALUES(name) ELSE name END;

somehow results in the row being updated (and the timestamp changed).

FWIW, this is the table I'm using:

CREATE TABLE `tbl` (
`hat` varchar(11) default NULL,
`mittens` varchar(11) default NULL,
`name` varchar(11) default NULL,
`stamp` timestamp NOT NULL default CURRENT_TIMESTAMP on update CURRENT_TIMESTAMP,
UNIQUE KEY `clothes` (`hat`,`mittens`)
) ENGINE=MyISAM DEFAULT CHARSET=latin1

MySQL is version 4.1.22 (perhaps this matters?) Again, my appreciation for all of the replies.

javalina
  • 178
  • 1
  • 1
  • 7
  • I would guess that it is your MySQL version that is the problem here. It looks like they fixed a bug (http://bugs.mysql.com/bug.php?id=28904) with INSERT ... ON DUPLICATE KEY UPDATE in the 5.0 branch, after active support for the 4.1 branch ended (Dec 31, 2006). I re-created your table and test case on my machine and it worked as expected, and I am running MySQL Server 5.0.67. If you can't update your MySQL installation, then you might be stuck without a 'single query' solution to your problem. – zombat Jun 25 '09 at 19:28
  • CONFIRMED. Both solutions work on 5.0.45 (unfortunately, that's just a dev server). Thanks again! – javalina Jun 25 '09 at 20:21
  • Did you try the second of the two queries I suggested? The one that manually controlled what happened to the stamp (last_update) column? I believe that one should work even though that bug exists as it explicitly decides whether to update the column or not with another case statement. – Dipin Jun 25 '09 at 21:40
  • I had a moment to install version 4.1.22 and the following single statement works as intended and does not update the stamp column unless the name changes: mysql> INSERT INTO tbl (hat,mittens,name) VALUES ('blue','green','george') ON DUPLICATE KEY UPDATE name = CASE WHEN name <> VALUES(name) THEN VALUES(name) ELSE name END, stamp = CASE WHEN name <> VALUES(name) THEN now() ELSE stamp END; See my answer for a log of my test case validating the behavior. – Dipin Jun 27 '09 at 16:22

4 Answers4

25

You can use normal sql constructs in the ON DUPLICATE KEY syntax. So in order to do conditional updates during an insert you can do the following:

INSERT INTO tbl (hat, mittens, name) 
VALUES ('yellow','purple','jimmy')
ON DUPLICATE KEY UPDATE name = CASE WHEN name <> VALUES(name) 
                                    THEN VALUES(name) ELSE name END;

This will change the value to what you provided to the insert statement when it's different from what's in the row and will set the value to be what it already is if it hasn't changed and will result in MySQL not doing anything to the row preserving the last_update timestamp as Quassnoi pointed out.

If you wanted to make 100% sure that you weren't relying on the behavior of MySQL where it doesn't update a row if you set a value to itself you can do the following to force the timestamp:

INSERT INTO tbl (hat, mittens, name) 
VALUES ('yellow','purple','jimmy')
ON DUPLICATE KEY UPDATE name = CASE WHEN name <> VALUES(name) 
                                    THEN VALUES(name) ELSE name END
                      , last_update = CASE WHEN name <> VALUES(name) 
                                      THEN now() ELSE last_update END;

This will only update the last_update to now() when the name has changed else it will tell MySQL to retain the value of last_update.

Also, in the ON DUPLICATE KEY section of the statement you can refer to the columns in the table by their name and you can get the values that you provided to the insert statement values section using the VALUES(column_name) function.


The following is a log that shows that the last statement provided works even on 4.1 where the others don't work due to a bug that was fixed in version 5.0.

C:\mysql\bin>mysql -u root -p
Enter password:
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 1 to server version: 4.1.22-community

Type 'help;' or '\h' for help. Type '\c' to clear the buffer.

mysql> show databases;
+----------+
| Database |
+----------+
| mysql    |
| test     |
+----------+
2 rows in set (0.00 sec)

mysql> use test;
Database changed
mysql> show tables;
Empty set (0.00 sec)

mysql> CREATE TABLE `tbl` (
    -> `hat` varchar(11) default NULL,
    -> `mittens` varchar(11) default NULL,
    -> `name` varchar(11) default NULL,
    -> `stamp` timestamp NOT NULL default CURRENT_TIMESTAMP on update CURRENT_TIMESTAMP,
    -> UNIQUE KEY `clothes` (`hat`,`mittens`)
    -> ) ENGINE=MyISAM DEFAULT CHARSET=latin1;
Query OK, 0 rows affected (0.01 sec)

mysql> INSERT INTO tbl (hat,mittens,name) VALUES ('blue','green','george');
Query OK, 1 row affected (0.00 sec)

mysql> select * from tbl;
+------+---------+--------+---------------------+
| hat  | mittens | name   | stamp               |
+------+---------+--------+---------------------+
| blue | green   | george | 2009-06-27 12:15:16 |
+------+---------+--------+---------------------+
1 row in set (0.00 sec)

mysql> INSERT INTO tbl (hat,mittens,name) VALUES ('blue','green','george') ON DUPLICATE KEY UPDATE name='george';
Query OK, 2 rows affected (0.00 sec)

mysql> select * from tbl;
+------+---------+--------+---------------------+
| hat  | mittens | name   | stamp               |
+------+---------+--------+---------------------+
| blue | green   | george | 2009-06-27 12:15:30 |
+------+---------+--------+---------------------+
1 row in set (0.00 sec)

mysql> INSERT INTO tbl (hat, mittens, name) VALUES ('blue','green','george') ON DUPLICATE KEY UPDATE name=CASE WHEN name <> VALUES(name) THEN VALUES(name) ELSE name END;
Query OK, 2 rows affected (0.00 sec)

mysql> select * from tbl;
+------+---------+--------+---------------------+
| hat  | mittens | name   | stamp               |
+------+---------+--------+---------------------+
| blue | green   | george | 2009-06-27 12:15:42 |
+------+---------+--------+---------------------+
1 row in set (0.00 sec)

mysql> INSERT INTO tbl (hat,mittens,name) VALUES ('blue','green','george') ON DUPLICATE KEY UPDATE name = CASE WHEN name <> VALUES(name) THEN VALUES(name) ELSE name END, stamp = CASE WHEN name <> VALUES(name) THEN now() ELSE stamp END;
Query OK, 2 rows affected (0.00 sec)

mysql> select * from tbl;
+------+---------+--------+---------------------+
| hat  | mittens | name   | stamp               |
+------+---------+--------+---------------------+
| blue | green   | george | 2009-06-27 12:15:42 |
+------+---------+--------+---------------------+
1 row in set (0.00 sec)

mysql>

Let me know if you have any questions.

HTH,

-Dipin

Dipin
  • 1,174
  • 9
  • 8
  • 4
    For me it was a bit difficult to find out what <> stands for (not equal to!) so I just thought I'd add this here in a comment in case someone else wonders in the future. :) – Alisso Aug 05 '12 at 14:32
  • Hi small help how can I query when I have multiple columns when updating – Chaitanya Aug 07 '12 at 12:42
  • is this old answer still the best way? what if instead of 1 non-key field (name), there were 50 fields to compare for changes.. would you list out 50 case statements in "last_update = CASE WHEN name <> VALUES(name) THEN now() ELSE last_update END;" – da Bich Mar 29 '21 at 01:05
3

You need INSERT ... ON DUPLICATE KEY UPDATE syntax.

Your query would look like this:

    INSERT INTO tbl (hat,mittens,name) VALUES ('blue','green','george')
    ON DUPLICATE KEY UPDATE name='george';

If you had a record with blue/green/george for hat/mittens/name already, no UPDATE would actually be performed, and your timestamp would not be updated. If however you had a record with blue/green/betty, then 'betty' would be overwritten with 'george', and your timestamp would be updated.

zombat
  • 92,731
  • 24
  • 156
  • 164
1

If you are performing a multiple INSERT (either by SELECT or providing multiple rows to VALUES), you can do the following:

INSERT INTO tbl (hat,mittens,name) VALUES
    ('yellow','purple','jimmy'),
    ('blue','green','george'),
    ('blue','green','betty')
ON DUPLICATE KEY UPDATE name = VALUES(name);

This will:

  • Insert rows that aren't duplicates w/ resp. to the UNIQUE indexes
  • Update the name column for the others (when the new value is different)

Caveat: the comments in the documentation on the ON DUPLICATE syntax says that the mysql_affected_rows() result afterwards isn't reliable.

instanceof me
  • 38,520
  • 3
  • 31
  • 40
0

You need to do nothing, this is default behavior.

If your query selects a row for UPDATE but updated value remains the same, as in:

UPDATE  table
SET     col = col

, the timestamp also remains the same.

This row doesn't even count as affected, the query will return 0 rows affected.

Quassnoi
  • 413,100
  • 91
  • 616
  • 614