Rick Irwin
Rick Irwin

Reputation: 13

MySQL updating all records having max value within group

Apologies if this has been answered elsewhere and I'm just not seeing it; this is the closest I've found, but it isn't quite what I'm trying to do.

MySQL - updating all records to match max value in group

I have a table on a production web server with about 15,000 rows. There are many records sharing an item_name with another record, but item_meters is (normally, but not always) a unique value for each row. item_id is always unique. Currently every record has a value of "0" in the item_flag column.

I would like to update all records with the largest item_meters value within each item_name group to have an item_flag value of "1".

Here is a simplified version of the table ordered by item_id ASC:

----------------------------------------------
                  mytable
----------------------------------------------
item_id | item_name | item_meters | item_flag
--------+-----------+-------------+-----------
001     | aaa       | 224         | 0
002     | aaa       | 359         | 0
003     | aaa       | 456         | 0
004     | bbb       | 489         | 0
005     | bbb       | 327         | 0
006     | bbb       | 215         | 0
007     | ccc       | 208         | 0
008     | ccc       | 756         | 0
009     | ccc       | 756         | 0
--------+-----------+-------------+-----------

The desired result would be a table with "1" in the item_flag column for each "aaa" having the largest item_meters, each "bbb" having the largest item_meters, each "ccc" having the largest item_meters, etc. like this:

----------------------------------------------
                  mytable
----------------------------------------------
item_id | item_name | item_meters | item_flag
--------+-----------+-------------+-----------
001     | aaa       | 224         | 0
002     | aaa       | 359         | 0
003     | aaa       | 456         | 1
004     | bbb       | 489         | 1
005     | bbb       | 327         | 0
006     | bbb       | 215         | 0
007     | ccc       | 208         | 0
008     | ccc       | 756         | 1
009     | ccc       | 756         | 0
--------+-----------+-------------+-----------

(In case there are 2 or more records having the same item_name and the same item_meters (e.g. item_id 008 and 009 above), the desired result would be for the record with the numerically lower item_id (item_id is always unique), to have an item_flag value of "1" while the row with a numerically higher item_id would still have an item_flag value of "0")

Also of note, even though this database is running behind a production web server with new rows added every day, there will be no need to update the table every time a new row is added. It is something that will only be required once, regardless of whether new rows are later added outside of the parameters. The reason I mention this, is because execution speed is not a big concern since the query will only be executed once.

Thank you in advance! Please let me know if I can provide more info or clarify my question in any way.

Upvotes: 1

Views: 1392

Answers (1)

spencer7593
spencer7593

Reputation: 108480

The approach I take is to first write a query (SELECT statement) that will return the item_id values of the rows we want to update.

As a starting point, get the maximum value for item_meters, a simple query like this:

  SELECT m.item_name
       , MAX(m.item_meters) AS max_item_meters
    FROM my_table m
   GROUP BY m.item_name

We can use that query as an inline view in another query, to get the lowest item_id for each of those item_name

  SELECT MIN(o.item_id) AS min_item_id
    FROM ( SELECT m.item_name
                , MAX(m.item_meters) AS max_item_meters
            FROM my_table m
           GROUP BY m.item_name
         ) n
    JOIN my_table o
      ON o.item_name = n.item_name
     AND o.item_meters = n.max_item_meters
   GROUP BY o.item_name, o.item_meters

And we can use that query as an inline view that gets the whole row associated with the item_id values we returned...

  SELECT t.item_id
       , t.item_name
       , t.item_meters
       , t.item_flag
    FROM my_table t
    JOIN ( SELECT p.min_item_id
             FROM ( SELECT MIN(o.item_id) AS min_item_id
                      FROM ( SELECT m.item_name
                                  , MAX(m.item_meters) AS max_item_meters
                               FROM my_table m
                              GROUP BY m.item_name
                           ) n
                      JOIN my_table o
                        ON o.item_name = n.item_name
                       AND o.item_meters = n.max_item_meters
                     GROUP BY o.item_name, o.item_meters
                  ) p
         ) q
      ON q.min_item_id = t.item_id

Once the SELECT query is working, convert that to an UPDATE statement... replace the SELECT ... FROM with UPDATE, and add a SET clause. (Sometimes, it's necessary to wrap the inline view in yet another SELECT, to avoid MySQL error about disallowing references to the table we are updating.)

  UPDATE my_table t
    JOIN ( SELECT p.min_item_id
             FROM ( SELECT MIN(o.item_id) AS min_item_id
                      FROM ( SELECT m.item_name
                                  , MAX(m.item_meters) AS max_item_meters
                               FROM my_table m
                              GROUP BY m.item_name
                           ) n
                      JOIN my_table o
                        ON o.item_name = n.item_name
                       AND o.item_meters = n.max_item_meters
                     GROUP BY o.item_name, o.item_meters
                  ) p
         ) q
      ON q.min_item_id = t.item_id
     SET t.item_flag = '1' 

If the intent is not to UPDATE the existing table, but to return a resultset, we can write a query and do an outer join to that same inline view, and return either a 0 or a 1 for item_flag, testing whether the item_id matches one we want flagged as 1...

  SELECT t.item_id
       , t.item_name
       , t.item_meters
       , IF(q.min_item_id IS NULL,0,1) AS `item_flag`
    FROM my_table t
    LEFT
    JOIN ( SELECT p.min_item_id
             FROM ( SELECT MIN(o.item_id) AS min_item_id
                      FROM ( SELECT m.item_name
                                  , MAX(m.item_meters) AS max_item_meters
                               FROM my_table m
                              GROUP BY m.item_name
                           ) n
                      JOIN my_table o
                        ON o.item_name = n.item_name
                       AND o.item_meters = n.max_item_meters
                     GROUP BY o.item_name, o.item_meters
                  ) p
         ) q
      ON q.min_item_id = t.item_id

Upvotes: 2

Related Questions