Разбиение на страницы MySQL без двойных запросов?

115

Мне было интересно, есть ли способ получить количество результатов из запроса MySQL и в то же время ограничить результаты.

Как работает пагинация (насколько я понимаю), сначала делаю что-то вроде

query = SELECT COUNT(*) FROM `table` WHERE `some_condition`

После того, как я получу num_rows (запрос), у меня будет количество результатов. Но затем, чтобы фактически ограничить мои результаты, я должен выполнить второй запрос, например:

query2 = SELECT COUNT(*) FROM `table` WHERE `some_condition` LIMIT 0, 10

Мой вопрос: есть ли способ получить общее количество результатов, которые будут выданы, И ограничить результаты, возвращаемые в одном запросе? Или любой более эффективный способ сделать это. Спасибо!

ассоциация теннисистов-профессионалов
источник
8
Хотя у вас не было бы COUNT (*) в query2
dlofrodloh

Ответы:

66

Нет, столько приложений, которые хотят разбить на страницы, должны это делать. Он надежный и пуленепробиваемый, хотя и делает запрос дважды. Но вы можете кэшировать счетчик на несколько секунд, и это очень поможет.

Другой способ - использовать SQL_CALC_FOUND_ROWSпредложение, а затем позвонить SELECT FOUND_ROWS(). Помимо того факта, что вы должны выполнить FOUND_ROWS()вызов после этого, существует проблема с этим: в MySQL есть ошибка , которая влияет на ORDER BYзапросы, делая их намного медленнее в больших таблицах, чем наивный подход с двумя запросами.

staticsan
источник
2
Однако это не совсем доказательство состояния гонки, если только вы не выполните два запроса в транзакции. Однако обычно это не проблема.
NickZoic
Под «надежным» я имел в виду, что сам SQL всегда будет возвращать желаемый результат, а под «пуленепробиваемым» я имел в виду, что нет ошибок MySQL, мешающих тому, какой SQL вы можете использовать. В отличие от использования SQL_CALC_FOUND_ROWS с ORDER BY и LIMIT, согласно упомянутой мной ошибке.
staticsan
5
В сложных запросах использование SQL_CALC_FOUND_ROWS для получения счетчика в одном и том же запросе почти всегда будет медленнее, чем выполнение двух отдельных запросов. Это потому, что это означает, что все строки необходимо будет получить полностью, независимо от лимита, тогда будут возвращены только те, которые указаны в предложении LIMIT. См. Также мой ответ, в котором есть ссылки.
томасруттер
В зависимости от причины, по которой вам это нужно, вы также можете подумать о том, чтобы просто не получать полные результаты. Все более распространенной практикой становится реализация методов автоматической подкачки страниц. Такие сайты, как Facebook, Twitter, Bing и Google, уже много лет используют этот метод.
Thomas B
68

Я почти никогда не делаю двух запросов.

Просто верните на одну строку больше, чем необходимо, отобразите на странице только 10, а если их больше, чем отображается, отобразите кнопку «Далее».

SELECT x, y, z FROM `table` WHERE `some_condition` LIMIT 0, 11
// iterate through and display 10 rows.

// if there were 11 rows, display a "Next" button.

Ваш запрос должен возвращаться в порядке наиболее релевантного. Скорее всего, большинству людей наплевать на страницу 236 из 412.

Когда вы выполняете поиск в Google и ваши результаты не отображаются на первой странице, вы, скорее всего, перейдете на вторую, а не на девятую страницу.

вышка
источник
42
На самом деле, если я не нахожу его на первой странице запроса Google, обычно я перехожу на девятую страницу.
Фил
3
@Phil Я слышал это раньше, но почему?
TK123
5
Немного поздно, но вот мои рассуждения. В некоторых поисках преобладают фермы ссылок, оптимизированные для поисковых систем. Итак, первые несколько страниц - это разные фермы, борющиеся за позицию номер 1, полезный результат, вероятно, все еще связан с запросом, только не наверху.
Фил
4
COUNT- агрегатная функция. Как вернуть счет и все результаты одним запросом? Вышеупомянутый запрос вернет только 1 строку, независимо от того, какое значение LIMITустановлено. Если вы добавите GROUP BY, он вернет все результаты, но COUNTбудут неточными
pixelfreak
2
Это один из подходов, рекомендованных Percona
techdude
27

Другой подход к предотвращению двойных запросов состоит в том, чтобы сначала получить все строки для текущей страницы, используя предложение LIMIT, а затем выполнить второй запрос COUNT (*) только в том случае, если было получено максимальное количество строк.

Во многих приложениях наиболее вероятным результатом будет то, что все результаты умещаются на одной странице, и необходимость разбиения на страницы является скорее исключением, чем нормой. В этих случаях первый запрос не вернет максимальное количество результатов.

Например, ответы на вопрос о stackoverflow редко попадают на вторую страницу. Комментарии к ответу редко выходят за рамки 5 или около того, необходимых для того, чтобы показать их все.

Таким образом, в этих приложениях вы можете просто сначала выполнить запрос с LIMIT, а затем, пока этот предел не достигнут, вы точно знаете, сколько строк там, без необходимости выполнять второй запрос COUNT (*), который должен охватывают большинство ситуаций.

thomasrutter
источник
1
@thomasrutter У меня был такой же подход, но сегодня я обнаружил в нем недостаток. На последней странице результатов не будет данных разбивки на страницы. т.е. предположим, что на каждой странице должно быть 25 результатов, на последней странице, вероятно, будет не так много, скажем, у нее 7 ... это означает, что счетчик (*) никогда не будет запущен, и поэтому разбивка на страницы не будет отображаться для пользователь.
duellsy
2
Нет - если вы говорите, что 200 результатов, вы запрашиваете следующие 25 и получаете обратно только 7, это означает, что общее количество результатов составляет 207, и поэтому вам не нужно выполнять еще один запрос с COUNT (*) потому что вы уже знаете, что он собирается сказать. У вас есть вся необходимая информация для разбивки на страницы. Если у вас возникла проблема с тем, что пагинация не отображается пользователю, значит, у вас есть ошибка где-то еще.
thomasrutter
15

В большинстве случаев гораздо быстрее и менее ресурсоемко выполнить это в двух отдельных запросах, чем в одном, хотя это кажется нелогичным.

Если вы используете SQL_CALC_FOUND_ROWS, то для больших таблиц он делает ваш запрос намного медленнее, значительно медленнее, чем выполнение двух запросов, первый с COUNT (*), а второй с LIMIT. Причина этого в том, что SQL_CALC_FOUND_ROWS приводит к применению предложения LIMIT после выборки строк, а не до, поэтому он выбирает всю строку для всех возможных результатов перед применением ограничений. Индекс не может удовлетворить это, потому что он фактически извлекает данные.

Если вы воспользуетесь подходом с двумя запросами, первый из которых извлекает только COUNT (*), а не фактически извлекает и фактические данные, это может быть выполнено гораздо быстрее, потому что он обычно может использовать индексы и не должен извлекать фактические данные строки для каждую строку, на которую он смотрит. Затем второму запросу нужно только просмотреть первые строки $ offset + $ limit, а затем вернуться.

Этот пост из блога о производительности MySQL объясняет это дополнительно:

http://www.mysqlperformanceblog.com/2007/08/28/to-sql_calc_found_rows-or-not-to-sql_calc_found_rows/

Для получения дополнительной информации об оптимизации разбивки на страницы проверьте этот пост и этот пост .

thomasrutter
источник
2

Мой ответ может быть запоздалым, но вы можете пропустить второй запрос (с ограничением) и просто отфильтровать информацию через свой серверный скрипт. Например, в PHP вы можете сделать что-то вроде:

if($queryResult > 0) {
   $counter = 0;
   foreach($queryResult AS $result) {
       if($counter >= $startAt AND $counter < $numOfRows) {
            //do what you want here
       }
   $counter++;
   }
}

Но, конечно, когда вам нужно рассмотреть тысячи записей, это очень быстро становится неэффективным. Предварительно рассчитанный подсчет, возможно, хорошая идея.

Вот хорошее прочтение по этой теме: http://www.percona.com/ppc2009/PPC2009_mysql_pagination.pdf

Кама
источник
Линк мертв, думаю, это правильный: percona.com/files/presentations/ppc2009/… . Не буду редактировать, потому что не уверен, что это так.
hectorg87
1
query = SELECT col, col2, (SELECT COUNT(*) FROM `table`) AS total FROM `table` WHERE `some_condition` LIMIT 0, 10
Крис Маклафлин
источник
16
Этот запрос просто возвращает общее количество записей в таблице; а не количество записей, соответствующих условию.
Лоуренс Барсанти
1
Общее количество записей - это то, что нужно для разбивки на страницы (@Lawrence).
imme
О, ну, просто добавьте whereпредложение во внутренний запрос, и вы получите правильную «сумму» вместе с выгружаемыми результатами (страница выбирается с помощью limitпредложения
Эренор Паз,
счетчик подзапросов (*) потребует того же предложения where, иначе он не вернет правильное количество результатов
AKrush95
1

Для тех, кто ищет ответ в 2020 году. Согласно документации MySQL:

"Модификатор запроса SQL_CALC_FOUND_ROWS и соответствующая функция FOUND_ROWS () устарели в MySQL 8.0.17 и будут удалены в будущей версии MySQL. В качестве замены рассмотрим возможность выполнения вашего запроса с помощью LIMIT, а затем второго запроса с COUNT (*) и без LIMIT, чтобы определить, есть ли дополнительные строки. "

Думаю, это решает.

https://dev.mysql.com/doc/refman/8.0/en/information-functions.html#function_found-rows

Игорь К
источник
0

Вы можете повторно использовать большую часть запроса в подзапросе и установить для него идентификатор. Например, запрос фильма, который находит фильмы, содержащие буквенное упорядочение по времени выполнения, на моем сайте будет выглядеть так.

SELECT Movie.*, (
    SELECT Count(1) FROM Movie
        INNER JOIN MovieGenre 
        ON MovieGenre.MovieId = Movie.Id AND MovieGenre.GenreId = 11
    WHERE Title LIKE '%s%'
) AS Count FROM Movie 
    INNER JOIN MovieGenre 
    ON MovieGenre.MovieId = Movie.Id AND MovieGenre.GenreId = 11
WHERE Title LIKE '%s%' LIMIT 8;

Обратите внимание, что я не эксперт по базам данных и надеюсь, что кто-то сможет оптимизировать это немного лучше. Поскольку он работает прямо из интерфейса командной строки SQL, они оба занимают ~ 0,02 секунды на моем ноутбуке.

Филип Роллинз
источник
-14
SELECT * 
FROM table 
WHERE some_condition 
ORDER BY RAND()
LIMIT 0, 10
Джон
источник
3
Это не отвечает на вопрос, и заказ по ранду - действительно плохая идея.
Дэн Уолмсли