MySQL: Pagination - SQL_CALC_FOUND_ROWS vs COUNT()-Query
When using pagination for let's say a list of offers that where never clicked, you have to know the exact amount of offers (which were never clicked) in order to know how many pages you have. Now there are 2 possible ways of calculating the exact amount of offers: You can use either SQL_CALC_FOUND_ROWS or you can setup a second query with a COUNT() in it. I did the tests with SQL_NO_CACHE in order to get the best results possible. The clicks table has about 18.000.000 rows, the offer table about 800.000. Let's start with some time results.
Using SQL_CALC_FOUND_ROWS:
mysql> SELECT SQL_NO_CACHE SQL_CALC_FOUND_ROWS o.offer_id
-> FROM offer AS o
-> LEFT JOIN clicks AS c
-> ON (o.offer_id = c.offer_id)
-> WHERE c.offer_id IS NULL
-> LIMIT 50,50;
50 rows in set (30.84 sec)
mysql> SELECT FOUND_ROWS();
1 row in set (0.00 sec)
Using a second query with COUNT():
mysql> SELECT SQL_NO_CACHE o.offer_id
-> FROM offer AS o
-> LEFT JOIN clicks AS c
-> ON (o.offer_id = c.offer_id)
-> WHERE c.offer_id IS NULL
-> LIMIT 50,50;
50 rows in set (0.03 sec)
mysql> SELECT SQL_NO_CACHE COUNT(o.offer_id)
-> FROM offer AS o
-> LEFT JOIN clicks AS c
-> ON (o.offer_id = c.offer_id)
-> WHERE c.offer_id IS NULL;
1 row in set (30.97 sec)
At a first glance they look equally fast, both taking about 30 seconds. But: They are only equally fast, when query caching is turned off. Let's assume we're on a high-traffic website where performance matters, so we turn the query cache on. MySQL Query caching is like a key-value cache with the key being the EXACT query and the resultset being the value. Once we turn on the cache, the pagination is way faster with the second query using COUNT().
Why?
When using SQL_CALC_FOUND_ROWS the application has to calculate the found rows every single time we turn the page, because the query changes, while the COUNT()-Query always remains the same, meaning that its result comes from the query cache from the second time on. Let's emulate:
Using SQL_CALC_FOUND_ROWS:
mysql> SELECT SQL_CALC_FOUND_ROWS o.offer_id
-> FROM offer AS o
-> LEFT JOIN clicks AS c
-> ON (o.offer_id = c.offer_id)
-> WHERE c.offer_id IS NULL
-> LIMIT 50,50;
50 rows in set (31.13 sec)
mysql> SELECT FOUND_ROWS();
1 row in set (0.00 sec)
mysql> SELECT SQL_CALC_FOUND_ROWS o.offer_id
-> FROM offer AS o
-> LEFT JOIN clicks AS c ON (o.offer_id = c.offer_id)
-> WHERE c.offer_id IS NULL
-> LIMIT 100,50;
50 rows in set (30.71 sec)
mysql> SELECT FOUND_ROWS();
1 row in set (0.00 sec)
Using a second query with COUNT():
mysql> SELECT o.offer_id
-> FROM offer AS o
-> LEFT JOIN clicks AS c
-> ON (o.offer_id = c.offer_id)
-> WHERE c.offer_id IS NULL
-> LIMIT 50,50;
50 rows in set (0.03 sec)
mysql> SELECT COUNT(o.offer_id)
-> FROM offer AS o
-> LEFT JOIN clicks AS c
-> ON (o.offer_id = c.offer_id)
-> WHERE c.offer_id IS NULL;
1 row in set (31.11 sec)
mysql> SELECT o.offer_id
-> FROM offer AS o
-> LEFT JOIN clicks AS c
-> ON (o.offer_id = c.offer_id)
-> WHERE c.offer_id IS NULL
-> LIMIT 100,50;
50 rows in set (0.04 sec)
mysql> SELECT COUNT(o.offer_id)
-> FROM offer AS o
-> LEFT JOIN clicks AS c
-> ON (o.offer_id = c.offer_id)
-> WHERE c.offer_id IS NULL;
1 row in set (0.00 sec)
Et Voilà! While the SQL_CALC_FOUND_ROWS Queries took more than one minute together, the queries with the second COUNT() query only took a bit more than 30 seconds together, meaning they are twice as fast.
I'm pretty sure there are situations where SQL_CALC_FOUND_ROWS is the way to go, but in cases like this one you definately wanna go for the COUNT()-Query.
10 comments
Ronald H.
02.07.2009, 03:30 o'clock
Thanks buddy. I'll be using this code in the future. I also thought you'd like to know there is a great domain name at Godaddy.com that you may be interested in. It's call PHPDEVELOPING.COM and I think its a good fit for you because your a great PHP programmer. You can contact me at my email address and I'll help you get to it if you want. Again, just thought you'd like to know.
Dominik Jungowski
02.07.2009, 11:21 o'clock
Just keep in mind that it's not always THE solution. When reading from a table that is very frequently updated (more than once a minute), you won't get too much speed benefits, as with each table update the cache query for the respective table runs out.
Thanks for the address tip, I'll think about it
wasimasif
15.07.2009, 15:48 o'clock
In my experience sometimes its good to use SQL_CALC_FOUND_ROWS instead of running separate queries for count.
If you are running full-text query on table with 2M+ rows then its good idea to use it. When we tried the query with SQL_CALC_FOUND_ROWS, performed better than two separate queries.
I also agree to prefer SQL_CALC_FOUND_ROWS on frequently updated table.
Vlad
30.11.2009, 20:36 o'clock
Бывает нужно выбирать записи не только в прямом поряде но и обратном. Здесь представлены эксерименты по измерению времени выборок (сек). Время выборки в прямом и обратном порядке в начале, в середине и конце таблицы.
Выбираются 100 строк из таблицы в 15198 строк . Таблица вида:
1;aaaaaaaaaaaaaaaaaaaaaaaaaaaaa;999;999,9;aaaaaaaaaa;aaaaaaaaaaaaa;aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa;32
Первое и последнее поля - индексы.
# Общий вывод
- Как при прямой выборке, так и при обратной, подсчет строк вести с помощью COUNT, а не SQL_CALC_FOUND_ROWS.
- В обратном порядке никогда не извлекать. Правда придется подсчитать строки и обратить массив, но это работает всегда быстрее, чем даже обратная выборка без подсчета.
#1
# Реверс с помощью array_reverse() не влияет на время, то есть лучше извлекать в прямом порядке и обращать массив в PHP
В начале
SELECT * FROM `alifpress_tab_drugs–` WHERE blockId=86 LIMIT 0, 100
0,002529 | rev: 0,002408
В середине
SELECT * FROM `alifpress_tab_drugs–` WHERE blockId=86 LIMIT 7000, 100
0,033052 | rev: 0,033089
В конце
SELECT * FROM `alifpress_tab_drugs–` WHERE blockId=86 LIMIT 15097, 100
0,068655 | rev: 0,068438
#2
# Последние 100 строк в обратном порядке (ORDER BY и DESC) извлекаются на 21% дольше, чем подсчет строк (COUNT), извлечение последних 100 строк в прямом порядке и обращение массива! И это было лучшее проявление обратной выборки, то есть выборка в конце таблицы (ORDER BY recId DESC LIMIT 0, 100). Применять же обратную выборку в середине или начале таблицы вообще не стоит - это время больше в 12 раз, чем при выборке в конце таблицы!
SELECT COUNT(*) AS count FROM `alifpress_tab_drugs–`
SELECT * FROM `alifpress_tab_drugs–` WHERE blockId=86 LIMIT 15097, 100
array_reverse()
0,096872
0,091073
0,088885
SELECT * FROM `alifpress_tab_drugs–` WHERE blockId=86 ORDER BY recId DESC LIMIT 0, 100
0,112414
0,111679
0,109608
#3.
# Подсчет с помощью COUNT и выборка 100 строк в прямом порядке работает 3,5 раз быстрее, чем с помощью SQL_CALC_FOUND_ROWS.
SELECT COUNT(*) AS count FROM `alifpress_tab_drugs–`
SELECT * FROM `alifpress_tab_drugs–` WHERE blockId=86 LIMIT 0, 100
0,020637
0,020737
0,020941
SELECT SQL_CALC_FOUND_ROWS * FROM `alifpress_tab_drugs–` WHERE blockId=86 LIMIT 0, 100
0,073836
0,072075
0,071991
#4.
# Для подсчета строк при обратной выборке SQL_CALC_FOUND_ROWS ни в коем случае не использовать. Этот запрос как бы делает свою работу за одно с выборкой, но лучше подсчитать число строк отдельно с помощью COUNT.
При обратной выборке на всех диапазонах
SELECT SQL_CALC_FOUND_ROWS * FROM `alifpress_tab_drugs–` WHERE blockId=86 ORDER BY recId DESC LIMIT 0, 100
1,23811
1,265026
1,199161
Gendalf
25.12.2009, 21:04 o'clock
В моем случае, есть очень много сложнейших запросов с результатами более чем по 1000 страниц по сотне записей на каждой… и время ожидания должно быть не более 0.1 сек.. вообщем вышел из положения следующей прозрачно оберткой для своего класса в пхп.
Суть в том, чтобы прозрачно выбирать "селект" запросы с "SQL_CALC_FOUND_ROWS", при первом запросе выбираем список всех таблиц из запроса, фиксируем максимальную дату изменения, и ложим кол-во записей в кеш. Если одна из таблиц меняется, кеш обновляется. Кусочек кода прилагаю. С С первого взгляда сильно много букв и движений, но в результате о долгих запросах изза пересчета всех записей можно забыть.
private function smart_count_cache() {
if(!stripos($this->sql, 'SQL_CALC_FOUND_ROWS') && !$this->smart_cache_init) return;
// Удаляем из запроса операторы Limit и Order
$sql = preg_replace(array('/Limit\s[0-9\s,]+/si',"/Order By[^\n]+/si"), '', $this->sql);
// Если кеш не инициализирован, создаем таблицу
if (!$this->smart_cache_init)
mysql_query("CREATE TABLE IF NOT EXISTS `@smart_cache` (`query_hash` varchar(64) NOT NULL, `cache_timepoint` int(11) NOT NULL, `query_rows` int(11) DEFAULT 0, PRIMARY KEY (`query_hash`)) ENGINE=HEAP;", $this->DB);
else {
// Если нет, получаем информацию о кол-во строк
$q = mysql_query("Select FOUND_ROWS() as recount", $this->DB); $r = mysql_fetch_array($q); mysql_free_result($q);
// Сохраняем ее в кеше информацию
mysql_query("Replace `@smart_cache` Set query_hash = '".md5($sql)."', cache_timepoint = UNIX_TIMESTAMP(), query_rows = {$r['recount']};", $this->DB);
// И возвращаем
return $r['recount'];
}
$this->smart_cache_init = true;
// Выбираем название всех таблиц, которые участвуют в запросе
if(preg_match_all('/(from\s|join\s)([\w_]+)/si', $sql, $found)) $tables = implode(',',$found[2]);
// Выбираем самую последнюю дату изменения
$q = mysql_query("show table status Where FIND_IN_SET(name, '{$tables}')>0", $this->DB); $LastUpdate = 1000;
while($r = mysql_fetch_array($q)) if(($n = strtotime($r['Update_time'])) > $LastUpdate) $LastUpdate = $n; mysql_free_result($q);
// Проверяем есть ли свежая информация в кеше для этого запроса
$q = mysql_query("Select query_rows From `@smart_cache` Where query_hash = '".md5($sql)."' AND cache_timepoint >= $LastUpdate");
$r = mysql_fetch_array($q); mysql_free_result($q);
if($r) {
// Если есть возращаем информацию
$this->info['count'] = $r['query_rows'];
$this->sql = str_ireplace('SQL_CALC_FOUND_ROWS', '', $this->sql);
}
}
17. Juni 2009
comments feed
recent posts
Srini
30.06.2009, 19:10 o'clock
Cool! Its 100% True!