{"id":83,"date":"2008-11-21T23:36:00","date_gmt":"2008-11-21T23:36:00","guid":{"rendered":"https:\/\/knielsen-hq.org\/w\/?p=83"},"modified":"2021-09-01T10:11:59","modified_gmt":"2021-09-01T10:11:59","slug":"selecting-rows-holding-group-wise-maximum-of-a-field","status":"publish","type":"post","link":"https:\/\/knielsen-hq.org\/w\/selecting-rows-holding-group-wise-maximum-of-a-field\/","title":{"rendered":"Selecting rows holding group-wise maximum of a field"},"content":{"rendered":"\n<p>Today there was a question on the Freenode MySQL channel about a classical problem: <a href=\"http:\/\/dev.mysql.com\/doc\/refman\/5.0\/en\/example-maximum-column-group-row.html\">Rows holding group-wise maximum<\/a> of a column. This is a problem that I keep encountering every so often, so I thought I would write up something about it.<\/p>\n\n\n\n<p>A good example of the problem is a table like the following holding versioned objects:<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">CREATE TABLE object_versions (\n  object_id INT NOT NULL,\n  version INT NOT NULL,\n  data VARCHAR(1000),\n  PRIMARY KEY(object_id, version)\n) ENGINE=InnoDB\n<\/pre>\n\n\n\n<p>Now it is easy to get the latest version for an object:<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">SELECT data FROM object_versions WHERE object_id = ? ORDER BY version DESC LIMIT 1\n<\/pre>\n\n\n\n<p>The query will even be very fast as it can use the index to directly fetch the right row:<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">mysql&gt; EXPLAIN SELECT data FROM object_versions\nWHERE object_id = 42 ORDER BY version DESC LIMIT 1;\n+----+-------------+-----------------+------+---------------+---------+---------+-------+------+-------------+\n| id | select_type | table           | type | possible_keys | key     | key_len | ref   | rows | Extra       |\n+----+-------------+-----------------+------+---------------+---------+---------+-------+------+-------------+\n|  1 | SIMPLE      | object_versions | ref  | PRIMARY       | PRIMARY | 4       | const |    3 | Using where | \n+----+-------------+-----------------+------+---------------+---------+---------+-------+------+-------------+\n1 row in set (0.00 sec)\n<\/pre>\n\n\n\n<p>But what if we want to select the latest version of <em>all<\/em> (or some range of) objects? This is a problem that I think standard SQL (or any SQL dialect that I know of, including MySQL) has no satisfactory answer to.<\/p>\n\n\n\n<p>Intuitively, the problem should be simple for the database engine to solve. Just traverse the BTree structure of the primary key (assume InnoDB clustered index storage here), and for each value of the first part of the primary key (<code>object_id<\/code>), pick the highest value of the second part (<code>version<\/code>) and return the corresponding row (this is similar to what I believe is sometimes called <em>index skip scan<\/em>). However, this idea is surprisingly difficult to express in SQL.<\/p>\n\n\n\n<p>The first method suggested in the above link to the MySQL manual works in this case, but it is not all that great in my opinion. For example, it does not work well if the column that MAX is computed over is not unique per group (as it is in this example with versions); it will return all of the maximal rows which may or may not be what you wanted. And the query plan is not all that great either:<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">mysql&gt; EXPLAIN SELECT data FROM object_versions o1 WHERE version =\n(SELECT MAX(version) FROM object_versions o2 WHERE o1.object_id = o2.object_id);\n+----+--------------------+-------+------+---------------+---------+---------+-----------------------+------+-------------+\n| id | select_type        | table | type | possible_keys | key     | key_len | ref                   | rows | Extra       |\n+----+--------------------+-------+------+---------------+---------+---------+-----------------------+------+-------------+\n|  1 | PRIMARY            | o1    | ALL  | NULL          | NULL    | NULL    | NULL                  |    6 | Using where | \n|  2 | DEPENDENT SUBQUERY | o2    | ref  | PRIMARY       | PRIMARY | 4       | einstein.o1.object_id |    1 | Using index | \n+----+--------------------+-------+------+---------------+---------+---------+-----------------------+------+-------------+\n2 rows in set (0.00 sec)\n<\/pre>\n\n\n\n<p>It is apparently doing a full table scan with an index lookup for every row in the table, which is not <em>that<\/em> bad, but certainly more expensive than necessary, especially if there are many versions per object. Still, it is probably the best method in most cases (or so I thought first, but see benchmarks below!).<\/p>\n\n\n\n<p>The two other suggestions from the MySQL manual are not perfect either (though the first one is blazingly fast, see benchmarks below). One is to use an uncorrelated subquery with a join:<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">mysql&gt; EXPLAIN SELECT o1.data FROM object_versions o1\nINNER JOIN (SELECT object_id, MAX(version) AS version FROM object_versions GROUP BY object_id) o2\nON (o1.object_id = o2.object_id AND o1.version = o2.version);\n+----+-------------+-----------------+--------+---------------+---------+---------+-------------------------+------+-------------+\n| id | select_type | table           | type   | possible_keys | key     | key_len | ref                     | rows | Extra       |\n+----+-------------+-----------------+--------+---------------+---------+---------+-------------------------+------+-------------+\n|  1 | PRIMARY     | &lt;derived2&gt;      | ALL    | NULL          | NULL    | NULL    | NULL                    |    3 |             | \n|  1 | PRIMARY     | o1              | eq_ref | PRIMARY       | PRIMARY | 8       | o2.object_id,o2.version |    1 |             | \n|  2 | DERIVED     | object_versions | index  | NULL          | PRIMARY | 8       | NULL                    |    6 | Using index | \n+----+-------------+-----------------+--------+---------------+---------+---------+-------------------------+------+-------------+\n<\/pre>\n\n\n\n<p>At first, I actually did not know exactly how to interpret this plan output. After the benchmarks given below, I now think this plan is actually very good, apparently it is first using something like an index skip scan to compute the <code>MAX()<\/code> in the uncorrelated subquery, and then looking up each row using the primary key. It still has the issue with multiple rows if version was not unique per object.<\/p>\n\n\n\n<p>The other suggestion uses an outer self-join:<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">mysql&gt; EXPLAIN SELECT o1.data FROM object_versions o1\nLEFT JOIN object_versions o2 ON o1.object_id = o2.object_id AND o1.version &lt; o2.version\nWHERE o2.object_id IS NULL;\n+----+-------------+-------+------+---------------+---------+---------+-----------------------+------+--------------------------------------+\n| id | select_type | table | type | possible_keys | key     | key_len | ref                   | rows | Extra                                |\n+----+-------------+-------+------+---------------+---------+---------+-----------------------+------+--------------------------------------+\n|  1 | SIMPLE      | o1    | ALL  | NULL          | NULL    | NULL    | NULL                  |    6 |                                      | \n|  1 | SIMPLE      | o2    | ref  | PRIMARY       | PRIMARY | 4       | einstein.o1.object_id |    1 | Using where; Using index; Not exists | \n+----+-------------+-------+------+---------------+---------+---------+-----------------------+------+--------------------------------------+\n2 rows in set (0.00 sec)\n<\/pre>\n\n\n\n<p>The plan again looks reasonable, but not optimal. And somehow, all three methods feel unnatural for something that ought to be simple to express.<\/p>\n\n\n\n<p>And in fact, there is a nice way to express this in SQL, except that it does not work (at least not in MySQL):<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">SELECT MAX(version, data) FROM object_versions GROUP BY object_id;\n<\/pre>\n\n\n\n<p>If there was just support for computing <code>MAX()<\/code> over multiple columns like this, this query would be a nice, natural, and simple way to express our problem. And it would be relatively easy for database engines to create the optimal plan, I think <em>index skip scan<\/em> is fairly standard already for single-column <code>MAX()<\/code> with <code>GROUP BY<\/code>. And the syntax feels very natural, even though it does bend the rules somehow by a single expression (<code>MAX(version, data)<\/code>) returning multiple columns. I have half a mind to try to implement it myself in MySQL or Drizzle one day &#8230;<\/p>\n\n\n\n<p>In fact, one can <em>almost<\/em> use this technique by an old trick-of-the-trade:<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">mysql&gt; SELECT MAX(CONCAT(version, \":\", data)) FROM object_versions GROUP BY object_id;\n+---------------------------------+\n| max(concat(version, \":\", data)) |\n+---------------------------------+\n| 2:foo2                          | \n| 1:bar                           | \n| 3:baz2                          | \n+---------------------------------+\n3 rows in set (0.00 sec)\n\nmysql&gt; EXPLAIN SELECT MAX(CONCAT(version, \":\", data)) FROM object_versions GROUP BY object_id;\n+----+-------------+-----------------+-------+---------------+---------+---------+------+------+-------+\n| id | select_type | table           | type  | possible_keys | key     | key_len | ref  | rows | Extra |\n+----+-------------+-----------------+-------+---------------+---------+---------+------+------+-------+\n|  1 | SIMPLE      | object_versions | index | NULL          | PRIMARY | 8       | NULL |    6 |       | \n+----+-------------+-----------------+-------+---------------+---------+---------+------+------+-------+\n1 row in set (0.00 sec)\n<\/pre>\n\n\n\n<p>Though I consider this (and variations thereof) a hack with limited practical usage.<\/p>\n\n\n\n<p>And speaking of hacks, there is actually <em>another<\/em> way to solve the problem, one which I learned about recently at a customer:<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">mysql&gt; EXPLAIN SELECT data FROM\n(SELECT * FROM object_versions ORDER BY object_id DESC, version DESC) t GROUP BY object_id;\n+----+-------------+-----------------+-------+---------------+---------+---------+------+------+---------------------------------+\n| id | select_type | table           | type  | possible_keys | key     | key_len | ref  | rows | Extra                           |\n+----+-------------+-----------------+-------+---------------+---------+---------+------+------+---------------------------------+\n|  1 | PRIMARY     | &lt;derived2&gt;      | ALL   | NULL          | NULL    | NULL    | NULL |    6 | Using temporary; Using filesort | \n|  2 | DERIVED     | object_versions | index | NULL          | PRIMARY | 8       | NULL |    6 |                                 | \n+----+-------------+-----------------+-------+---------------+---------+---------+------+------+---------------------------------+\n<\/pre>\n\n\n\n<p>Get it? This cleverly\/evilly (ab)uses the MySQL non-standard extension which allows SELECT of columns not in the GROUP BY clause even without using aggregate functions. MySQL will return a value for the column from an &#8220;arbitrary&#8221; row in the group. In practise, it chooses it deterministically from the first row in the group, which is why this trick seems to work well in practise. But it is clearly documented to <em>not<\/em> be supported, so not really something to recommend, though interesting to see.<\/p>\n\n\n\n<h3>Bonus benchmark<\/h3>\n\n\n\n<p>As a free bonus, I decided to run some quick benchmarks. As it turns out, the results are quite surprising!<\/p>\n\n\n\n<p><br>So I filled the table above with 1,000,000 rows, 1000 objects each with 1000<br>versions. Total table size is about 50Mb or so. I then ran each of the five<br>above queries:<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">mysql&gt; SELECT data FROM object_versions o1\nWHERE version = (SELECT MAX(version) FROM object_versions o2 WHERE o1.object_id = o2.object_id);\n1000 rows in set (4 min 22.86 sec)\n\nmysql&gt; SELECT o1.data FROM object_versions o1\nINNER JOIN (SELECT object_id, MAX(version) AS version FROM object_versions GROUP BY object_id) o2\nON (o1.object_id = o2.object_id AND o1.version = o2.version);\n1000 rows in set (0.01 sec)\n\nmysql&gt; SELECT o1.data FROM object_versions o1\nLEFT JOIN object_versions o2 ON o1.object_id = o2.object_id AND o1.version &lt; o2.version\nWHERE o2.object_id IS NULL;\n1000 rows in set (2 min 42.72 sec)\n\nmysql&gt; SELECT MAX(CONCAT(version, \":\", data)) FROM object_versions GROUP BY object_id;\n1000 rows in set (0.63 sec)\n\nmysql&gt; SELECT data FROM\n(SELECT * FROM object_versions ORDER BY object_id DESC, version DESC) t GROUP BY object_id;\n1000 rows in set (15.61 sec)\n<\/pre>\n\n\n\n<p>The differences are <em>huge<\/em>!<\/p>\n\n\n\n<p>The clear winner is query 2, the uncorrelated subquery. Apparently it can do an index skip scan for the inner MAX\/GROUP BY query, followed with a primary key join, so it only ever has to touch 1000 rows. An almost optimal plan.<\/p>\n\n\n\n<p>Query 1 and 3 (correlated subquery and outer join) are spectacularly bad. It looks as if they are doing something like for each 1,000,000 rows in the full table scan, do a 1000 row index range scan in the subquery\/join, for a total of 1 billion rows examined. Or something. Not good.<\/p>\n\n\n\n<p>Query 4 and 5, the trick queries, are doing so-so, probably they get away with a sort of a full table scan of 1,000,000 rows.<\/p>\n\n\n\n<p>Conclusions: Uncorrelated subquery is the undisputed winner!<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Today there was a question on the Freenode MySQL channel about a classical problem: Rows holding group-wise maximum of a column. This is a problem that I keep encountering every so often, so I thought I would write up something about it. A good example of the problem is a table like the following holding&hellip; <a class=\"more-link\" href=\"https:\/\/knielsen-hq.org\/w\/selecting-rows-holding-group-wise-maximum-of-a-field\/\">Continue reading <span class=\"screen-reader-text\">Selecting rows holding group-wise maximum of a field<\/span><\/a><\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":[],"categories":[1],"tags":[4,27],"_links":{"self":[{"href":"https:\/\/knielsen-hq.org\/w\/wp-json\/wp\/v2\/posts\/83"}],"collection":[{"href":"https:\/\/knielsen-hq.org\/w\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/knielsen-hq.org\/w\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/knielsen-hq.org\/w\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/knielsen-hq.org\/w\/wp-json\/wp\/v2\/comments?post=83"}],"version-history":[{"count":1,"href":"https:\/\/knielsen-hq.org\/w\/wp-json\/wp\/v2\/posts\/83\/revisions"}],"predecessor-version":[{"id":84,"href":"https:\/\/knielsen-hq.org\/w\/wp-json\/wp\/v2\/posts\/83\/revisions\/84"}],"wp:attachment":[{"href":"https:\/\/knielsen-hq.org\/w\/wp-json\/wp\/v2\/media?parent=83"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/knielsen-hq.org\/w\/wp-json\/wp\/v2\/categories?post=83"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/knielsen-hq.org\/w\/wp-json\/wp\/v2\/tags?post=83"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}