There's a pretty common MySQL recipe for performance that if you want to efficiently scan through lots of rows in small chunks that LIMIT with OFFSET is right out. Using OFFSET, MySQL will have to scan all the rows until it finds the starting position before it starts reading results to return. Just to be clear, these statements look like:
If you were trying to read all rows in table then this would be a very slow and expensive way to do that (in terms of MySQL resources.) The most common optimization is to switch to an algorithm where you remember the last highest id value for each chunk of rows, and then add that to the WHERE clause.
This would be a much more efficient way select all values from a table in small chunks because it will use the PRIMARY KEY and be able to quickly find the first row to read with no extra reads. But what if we add a twist: you only want to scan through some part of your table in this way, something like:
In this case, we want to select all rows where b=1. Now if we try to use the primary key we have to skip over rows that don't match the b=1 condition. If those rows are not close together in the table, then this can be a very inefficient method. You may have to read millions of rows just to find even the next 1 row to return.
The natural conclusion is that you would apply an index to column b. Since indexes in innodb are clustered, MySQL can use the PK value hidden at the end to order the rows and scan them in order. Problem solved, right?
Not so fast.
There's a hidden gotcha here, so lets take a look at the MySQL status counters to reveal what's going on internally.
Do you see the problem yet? It's subtle. Let's look at the handler status variables after we run the query.
The value or the Handler_read_next show it had to read 911 rows before it found the 1 row requested and could stop executing and return the result. That's not good. In fact, it's going to get worse as you progress through the table and the queries will become progressively slower.
What's going on is that MySQL apparently cannot use the hidden value in the clustered index to resolve the "id > 1000" part of the WHERE clause. This is quite disappointing. It seems like it should be filed as a bug, but so far I couldn't find any existing bug that describes this behavior. If anyone knows of one please let me know. Otherwise I'll probably file something.
The workaround for now is to add an index like the following:
I can feel all of you out there cringing right now. Seems gross doesn't it? Now we have the primary key value on the end of each index record *twice*! However, it's hard to argue with results:
This result shows that key_len is now 6 instead of 2 -- it's using both key parts, the Handler_read_next is zero, which shows it's scanning no extra rows. There is just one key read for this query.
I've tested this result in both 5.5 and 5.6; I was hoping that this would be among the many great query optimizer enhancements in the new version. Sadly, that's not the case. If anyone wants to reproduce this, here's the table structure and data:
Edit: Several readers have pointed out that this has been fixed in current versions of 5.6 -- I was testing on one that was released several months ago (5.6.5) Things can change fast! MariaDB also supports this properly as well. Please read the comments for the full picture.
SELECT id FROM foo ORDER BY id LIMIT 10 OFFSET 1000;
If you were trying to read all rows in table then this would be a very slow and expensive way to do that (in terms of MySQL resources.) The most common optimization is to switch to an algorithm where you remember the last highest id value for each chunk of rows, and then add that to the WHERE clause.
SELECT id FROM foo WHERE id > 100000 ORDER BY id LIMIT 10;
This would be a much more efficient way select all values from a table in small chunks because it will use the PRIMARY KEY and be able to quickly find the first row to read with no extra reads. But what if we add a twist: you only want to scan through some part of your table in this way, something like:
SELECT id FROM foo WHERE b=1 AND id > 100000 ORDER BY id LIMIT 10;
In this case, we want to select all rows where b=1. Now if we try to use the primary key we have to skip over rows that don't match the b=1 condition. If those rows are not close together in the table, then this can be a very inefficient method. You may have to read millions of rows just to find even the next 1 row to return.
The natural conclusion is that you would apply an index to column b. Since indexes in innodb are clustered, MySQL can use the PK value hidden at the end to order the rows and scan them in order. Problem solved, right?
Not so fast.
There's a hidden gotcha here, so lets take a look at the MySQL status counters to reveal what's going on internally.
mysql > explain SELECT id FROM foo WHERE b=1 AND id > 100000 ORDER BY ID LIMIT 1; +----+-------------+-------+------+---------------+------+---------+-------+------+--------------------------+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | +----+-------------+-------+------+---------------+------+---------+-------+------+--------------------------+ | 1 | SIMPLE | foo | ref | PRIMARY,b | b | 2 | const | 4729 | Using where; Using index | +----+-------------+-------+------+---------------+------+---------+-------+------+--------------------------+
Do you see the problem yet? It's subtle. Let's look at the handler status variables after we run the query.
mysql> SELECT id FROM foo WHERE b=1 AND id > 100000 ORDER BY ID LIMIT 1; +--------+ | id | +--------+ | 100136 | +--------+ 1 row in set (0.01 sec) mysql> show status like 'Handler_read_next'; +-------------------+-------+ | Variable_name | Value | +-------------------+-------+ | Handler_read_next | 911 | +-------------------+-------+
The value or the Handler_read_next show it had to read 911 rows before it found the 1 row requested and could stop executing and return the result. That's not good. In fact, it's going to get worse as you progress through the table and the queries will become progressively slower.
What's going on is that MySQL apparently cannot use the hidden value in the clustered index to resolve the "id > 1000" part of the WHERE clause. This is quite disappointing. It seems like it should be filed as a bug, but so far I couldn't find any existing bug that describes this behavior. If anyone knows of one please let me know. Otherwise I'll probably file something.
The workaround for now is to add an index like the following:
ALTER TABLE foo ADD INDEX bplus (b,id);
I can feel all of you out there cringing right now. Seems gross doesn't it? Now we have the primary key value on the end of each index record *twice*! However, it's hard to argue with results:
mysql> explain SELECT id FROM foo WHERE b=1 AND id > 100000 ORDER BY ID LIMIT 1; +----+-------------+-------+-------+-----------------+-------+---------+------+------+--------------------------+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | +----+-------------+-------+-------+-----------------+-------+---------+------+------+--------------------------+ | 1 | SIMPLE | foo | range | PRIMARY,b,bplus | bplus | 6 | NULL | 3818 | Using where; Using index | +----+-------------+-------+-------+-----------------+-------+---------+------+------+--------------------------+ 1 row in set (0.00 sec) mysql> SELECT id FROM foo WHERE b=1 AND id > 100000 ORDER BY ID LIMIT 1; +--------+ | id | +--------+ | 100136 | +--------+ 1 row in set (0.00 sec) mysql> show status like 'Handler_read_next'; +-------------------+-------+ | Variable_name | Value | +-------------------+-------+ | Handler_read_next | 0 | +-------------------+-------+ 1 row in set (0.00 sec) mysql> show status like 'Handler_read%'; +-----------------------+-------+ | Variable_name | Value | +-----------------------+-------+ | Handler_read_first | 0 | | Handler_read_key | 1 | | Handler_read_last | 0 | | Handler_read_next | 0 | | Handler_read_prev | 0 | | Handler_read_rnd | 0 | | Handler_read_rnd_next | 0 | +-----------------------+-------+ 7 rows in set (0.01 sec)
This result shows that key_len is now 6 instead of 2 -- it's using both key parts, the Handler_read_next is zero, which shows it's scanning no extra rows. There is just one key read for this query.
I've tested this result in both 5.5 and 5.6; I was hoping that this would be among the many great query optimizer enhancements in the new version. Sadly, that's not the case. If anyone wants to reproduce this, here's the table structure and data:
CREATE TABLE `foo` ( `id` int(10) unsigned NOT NULL AUTO_INCREMENT, `b` tinyint(4) DEFAULT NULL, PRIMARY KEY (`id`), KEY `b` (`b`) ) ENGINE=InnoDB insert into foo values (NULL, RAND()*100); insert into foo select NULL, RAND()*100 FROM foo; -- repeat until approx 200k rows are inserted.
Edit: Several readers have pointed out that this has been fixed in current versions of 5.6 -- I was testing on one that was released several months ago (5.6.5) Things can change fast! MariaDB also supports this properly as well. Please read the comments for the full picture.