Question

I have table with items. Each item has id, item_type (let's say - book, audio, video, ebook), time of insertion and sign:

id|  item_type | created (timestamp) | sign
1 |    book    | 2014-01-14 15:40:24 | NULL
2 |    book    | 2014-01-15 15:40:24 | NULL
3 |   audio    | 2014-01-16 15:40:24 | NULL
4 |   audio    | 2014-01-17 15:40:24 | NULL
5 |   ebook    | 2014-01-18 15:40:24 | NULL
6 |   video    | 2014-01-19 15:40:24 | NULL

I would like to update column sign for 3 oldest rows with different item_type, for data above, these rows are with ID 1,3,5.

But I have no idea how to express the condition that item_type must be different for updated rows. Thank you for your advices. I appreciate your help. Richard

EDIT: apologies if description is confusing: I would like to update always 3 rows, each row with different item_type. Not 3 books, 3 videos, ...


Ad sorting - it's true I could sort them by ID, not timestamp here. This demo is simplification, in real application, I will use use sorting by modification timestamp.

Was it helpful?

Solution

Here's one way to meet the specified requirement.

(Note that we weren't given a guarantee about uniqueness of the created column per item_type, and given a requirement that the query update AT MOST three rows, one row of each item_type. This query assumes that the 'id' column is the primary key, or at least a unique key.)

UPDATE thetable t
  JOIN ( SELECT r.item_type
              , MIN(r.id) AS id
           FROM thetable r
           JOIN ( SELECT p.item_type
                       , MIN(p.created) AS oldest
                    FROM thetable p
                   GROUP BY p.item_type 
                   ORDER BY oldest
                   LIMIT 3
                ) q
             ON q.item_type = r.item_type
            AND q.oldest = r.created
          GROUP BY r.item_type
       ) s
    ON s.id = t.id
   SET t.sign = 0

Yeah, that looks kind of nasty. Let's unpack it a little.

The inline view aliased as 'q' gets a maximum of three rows, with three different item_type, along with the oldest 'created' value for each item_type. That's half the battle right there.

The inline view aliased as 's' gets an 'id' value for a single row that matches a row from 'q'. (We need to do this, since we weren't given a guaranteed that there wouldn't be two or more rows with matching 'item_type' and 'created', that leaves a potential to update more than three rows.)


-- test case
CREATE TABLE thetable (id INT, item_type VARCHAR(9), created TIMESTAMP, `sign` INT);
INSERT INTO thetable VALUES
('1','book','2014-01-14 15:40:24',NULL)
,('2','book','2014-01-15 15:40:24',NULL)
,('3','audio','2014-01-16 15:40:24',NULL)
,('4','audio','2014-01-17 15:40:24',NULL)
,('5','ebook','2014-01-18 15:40:24',NULL)
,('6','video','2014-01-19 15:40:24',NULL);

SELECT * FROM thetable;

FOLLOWUP

Would it be difficult to modify this UPDATE query so it updates signs for rows which are oldest AND their sign is null?

If what you mean is that you want the statement work like it does, but only operate on rows in the table that have a NULL in the sign column, as if it's pretending that rows with a non-NULL value in the sign column don't exist, that's a pretty simple change.

We can add WHERE clauses to the inline views, something like this:

UPDATE thetable t
  JOIN ( SELECT r.item_type
              , MIN(r.id) AS id
           FROM thetable r
           JOIN ( SELECT p.item_type
                       , MIN(p.created) AS oldest
                    FROM thetable p
                   WHERE p.sign IS NULL        -- only consider rows with NULLs
                   GROUP BY p.item_type 
                   ORDER BY oldest
                   LIMIT 3
                ) q
             ON q.item_type = r.item_type
            AND q.oldest = r.created
          WHERE r.sign IS NULL                 -- only consider rows with NULLs
          GROUP BY r.item_type
       ) s
    ON s.id = t.id
   SET t.sign = 0

If you mean something else, the modification would be different.

OTHER TIPS

This query will get you the earliest record for each item type:

select item_type, min(created) as MinCreated
from MyTable
group by item_type

You can then do something like:

update MyTable a
inner join (
    select item_type, min(created) as MinCreated
    from MyTable
    group by item_type
    ) b on a.item_type = b.item_type
    and a.created = b.MinCreated
set sign = 1

You could use something like:

   SELECT * FROM MyTable GROUP BY item_type ORDER BY created DESC LIMIT 3

Using group by gives you unique item_type, and order by created in descendant order along with limit 3, will give you the oldest three

I tested this solution:

SET @t := NULL;
SET @c := 0;

UPDATE n 
SET sign = IF(@t = item_type, sign, IF(@c < 3, SIGN(@c:=@c+1), sign)),
    item_type = IF(@t:=item_type, item_type, item_type)
ORDER BY item_type, created;

Here's the resulting change to data:

+----+-----------+---------------------+------+
| id | item_type | created             | sign |
+----+-----------+---------------------+------+
|  1 | book      | 2014-01-20 08:54:00 |    1 |
|  2 | book      | 2014-01-15 15:40:24 | NULL |
|  3 | audio     | 2014-01-20 08:54:00 |    1 |
|  4 | audio     | 2014-01-17 15:40:24 | NULL |
|  5 | ebook     | 2014-01-20 08:54:00 |    1 |
|  6 | video     | 2014-01-19 15:40:24 | NULL |
+----+-----------+---------------------+------+

After thinking about this further and viewing other people's solutions, I think this would be far easier if we don't restrict to doing it in a single SQL statement.

Pick the three earliest entries with distinct item_types, first by created timestamp, then just in case there are ties, of the entries that match that timestamp, pick the item with the lowest id.

SELECT item_type, MIN(id) AS id
FROM n
JOIN (
    SELECT item_type, MIN(created) AS created
    FROM n 
    GROUP BY item_type 
    ORDER BY created ASC LIMIT 3
) AS t USING (item_type, created)
GROUP BY item_type;

This gives you the id of the three earliest entries, choosing just one per item_type.

+-----------+------+
| item_type | id   |
+-----------+------+
| audio     |    3 |
| book      |    1 |
| ebook     |    5 |
+-----------+------+

Then you have to fetch the id's and form a new query:

UPDATE n SET sign = 1 WHERE id IN (3,1,5);

I tried to combine the two into one UPDATE statement, but I got the following error:

ERROR 1093 (HY000): You can't specify target table 'n' for update in FROM clause

Thought I also tested some of the other solutions given by other users, and they seem to work. So there must be a pretty subtle rule for when UPDATE allows subqueries.

I'd recommend that a simpler solution is better, because it's easier to understand, code, debug, and maintain. So do it in two statements and call it a victory.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top