4

Greetings,

How would one go about performing two UPDATE statements in one query, for example:

UPDATE albums SET isFeatured = '0' WHERE isFeatured = '1'

combined with

UPDATE albums SET isFeatured = '1' WHERE id = '$id'

Basically, when a new album is featured, the previously featured album is switched back to normal and the newly featured one is set to active.

Thanks!

linuxbuild
  • 15,843
  • 6
  • 60
  • 87
NightMICU
  • 9,000
  • 30
  • 89
  • 121

7 Answers7

9

Try this:

UPDATE albums SET isFeatured = IF(id!='$id', '0','1')
Chandu
  • 81,493
  • 19
  • 133
  • 134
  • Looks like this is pretty zippy to me, easy to understand and short. Thanks :) – NightMICU Jan 09 '11 at 22:04
  • 1
    @a1ex07: I didn't know that, thanks. Does it still extend the query time because it needs to check each row though? Can this be mitigated with indexes? – richo Jan 09 '11 at 22:19
  • I think it does slow down (not too much though). But we need take into account that the first (original) query updates everything where `isFeatured = '1'. And I'm pretty sure that full table scan was involved anyway - index on boolean(bit) field cannot be selective enough to be considered in execution plan. – a1ex07 Jan 09 '11 at 22:25
  • Answer is correct but the data model is not normalized if (as you imply) there can be only one featured album from all in the table. +1 to Richard Harrison's answer for a solution to that. – ypercubeᵀᴹ Jan 09 '11 at 22:42
  • @Cybernate: Your answer will force a full table scan. – nate c Jan 09 '11 at 23:27
  • 1
    @Nate: I completely agree on this query resulting in a full scan. Given the table structure I can't think of any other solution(without changing the tables) which would avoid a full table scan. If you can think of one pls let me know. – Chandu Jan 10 '11 at 01:27
7

When you have to do this sort of thing it is an indicator that your data model is wrong and could do with some fixing.

So, I'd recommend to add a seperate table featured_albums (FK: int id_album) and use that to determine if the album is featured.

Your update becomes

DELETE FROM featured_album; INSERT INTO featured_album SET id_album = $id;

When selecting join the tables

SELECT album.id,
       album.name, 
       ( id_album IS NOT NULL ) AS isfeatured
FROM   album
LEFT JOIN featured_album ON id_album = album.id 

As requested to expand on the above basically I'm suggesting adding a table that will contain a row indicating the currently selected album. This is a 1 to 1 relationship, i.e. one record in the album table has one related record in the feature_albums table. See Types of Relationship.

Album Schema Diagram

You remove the isFeatured field from the album table and add a new table.

CREATE TABLE `featured_album` (
    `id_album` INTEGER NOT NULL,
    FOREIGN KEY (id_album) REFERENCES `album` (`id`)
);

The DELETE FROM .. INSERT INTO line sets the featured album by creating an entry in the table.

The SELECT statement with the LEFT JOIN will pull in the records from the album table and join those that match from the featured_album table, in our case only one record will match so as there is one field in the featured_album table it will return NULL for all records except the featured album.

So if we did

SELECT album.id, album.name, featured_album.id_album as isFeatured0
FROM   album
LEFT JOIN featured_album ON id_album = album.id 

We'd get something like the following:

+----+----------------+------------+
| id | name           | isFeatured |
+----+----------------+------------+
|  1 | Rumours        |       NULL |
|  2 | Snowblind      |       NULL |
|  3 | Telegraph road |          3 |
+----+----------------+------------+

i.e. a NULL for isFeatured or an ID.

By adding the ( id_album IS NOT NULL ) AS isfeatured and using the first query we get

+----+----------------+------------+
| id | name           | isfeatured |
+----+----------------+------------+
|  1 | Rumours        |          0 |
|  2 | Snowblind      |          0 |
|  3 | Telegraph road |          1 |
+----+----------------+------------+

i.e. 0/1 for isfeatured which makes things more readable, although if you're processing the results in PHP it won't make a difference to your code.

Richard Harrison
  • 19,247
  • 4
  • 40
  • 67
5

You can use CASE WHEN statement and remember to set original value where necessary (ELSE clause below) and order CASE conditions as required (in statement below isFeatured will be 0 if row having requested id also has isFeatured = 1, to change it swap WHEN clauses).

UPDATE albums 
SET isFeatured = CASE 
  WHEN isFeatured = '1' THEN '0' 
  WHEN id = '$id' THEN '1'
  ELSE isFeatured
END
too
  • 3,009
  • 4
  • 37
  • 51
0

You just can't. You can only select one group of records that should be updated and can then only perform one operation on all of them. It's not possible to do

 UPDATE x SET col1 = 1 WHERE col1 = 0 AND col1 = 0 WHERE col1 = 1;

Be careful when using functions to work around this, as they need to be evaluated for every row and this can become really expensive.

xrstf
  • 1,162
  • 1
  • 9
  • 12
  • +1: for realizing MySQL does not magically rewrite your query with an `if` to substitute a where clause. – nate c Jan 09 '11 at 23:31
0

MySQL is unable to use the index when it is inside an if function: You need an index on the function which is not possible in MySQL.

see also: How does one create an index on the date part of DATETIME field in MySql

I am using the employee test database http://dev.mysql.com/doc/employee/en/employee.html

mysql> describe employees;
+------------+---------------+------+-----+---------+-------+
| Field      | Type          | Null | Key | Default | Extra |
+------------+---------------+------+-----+---------+-------+
| emp_no     | int(11)       | NO   | PRI | NULL    |       |
| birth_date | date          | NO   |     | NULL    |       |
| first_name | varchar(14)   | NO   |     | NULL    |       |
| last_name  | varchar(16)   | NO   |     | NULL    |       |
| gender     | enum('M','F') | NO   |     | NULL    |       |
| hire_date  | date          | NO   |     | NULL    |       |
+------------+---------------+------+-----+---------+-------+
6 rows in set (0.01 sec)

mysql> select count(*) from employees;
+----------+
| count(*) |
+----------+
|   300024 |
+----------+
1 row in set (0.37 sec)

Set all genders to male so it mimics the question.

mysql> update employees set gender = 'M';
Query OK, 1 row affected (9.11 sec)
Rows matched: 300024  Changed: 1  Warnings: 0

mysql>  select emp_no, gender from employees order by emp_no limit 2;
+--------+--------+
| emp_no | gender |
+--------+--------+
|  10001 | M      |
|  10002 | M      |
+--------+--------+
2 rows in set (0.00 sec)

Set one employee to female. (Notice it uses the index and is almost instant.)

mysql> update employees set gender = 'F' where emp_no = 10001;
Query OK, 1 row affected (0.14 sec)
Rows matched: 1  Changed: 1  Warnings: 0

Now we use the suggested answer. (Notice it does not use the index and touches every row.)

mysql> update employees set gender = if(emp_no=10002, 'F', 'M');
Query OK, 2 rows affected (10.67 sec)
Rows matched: 300024  Changed: 2  Warnings: 0

Will an index help?

> mysql> create index employees_gender_idx  on employees(gender);
Query OK, 300024 rows affected (21.61 sec)
Records: 300024  Duplicates: 0  Warnings: 0

> mysql> update employees set gender = if(emp_no=10001, 'F', 'M');
Query OK, 2 rows affected (9.02 sec)
Rows matched: 300024  Changed: 2  Warnings: 0

Nope.

It was also said that MySQL is only going to look at the rows that need to be changed.

mysql> update employees set gender = 'M';
Query OK, 1 row affected (8.78 sec)
Rows matched: 300024  Changed: 1  Warnings: 0

Guess not. What if use a WHERE clause?

mysql> update employees set gender = 'M' where gender ='F';
Query OK, 0 rows affected (0.03 sec)
Rows matched: 0  Changed: 0  Warnings: 0

Gee that fast, now it used the index.

Mysql has no idea what the IF function will return and must do a full table scan. Notice that WHERE really does mean where and SET really does mean set. You can't expect the DB to just arrange all your clauses to get good performance.

The correct solution is to issue two updates (which if use indexes will be almost instant.)

Notice, it was said elsewhere that MySQL will magically know only update the rows it needs to change.

Community
  • 1
  • 1
nate c
  • 8,802
  • 2
  • 27
  • 28
  • Very thorough explanation, thank you. So, basically, trying to combine this sort of thing is actually SLOWER than just sending out two separate statements? – NightMICU Jan 09 '11 at 23:32
  • Yes. Some db's such as PostgreSQL allow you to index functions but not MySQl. Its also correct to say your table is not normalized (see Richard Harrisons answer) which would solve your problem. I just wanted to clear up the mis-information. – nate c Jan 09 '11 at 23:38
  • Just to clarify I added a little bit more about expecting the MySQL to figure out the SET clause is actually a where clause. – nate c Jan 09 '11 at 23:54
0

Adding an alternate method to the excellent answer provided by @too where instead of CASE IF statement is used -

UPDATE album
    -> SET isFeatured = IF (
    -> isFeatured = '1', '0', IF (
    -> id = '$id', '1', isFeatured
    -> ));
Payel Senapati
  • 1,134
  • 1
  • 11
  • 27
-1

I don't think you can, or at least not in a neat or practical way.

If you're wanting to do one call from php/whatever then you can seperate them with semicolons thus:

UPDATE albums SET isFeatured = '0' WHERE isFeatured = '1';UPDATE albums SET isFeatured = '1' WHERE id = '$id';
richo
  • 8,717
  • 3
  • 29
  • 47