Пагинація 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 (*) у запиті2
dlofrodloh

Відповіді:


66

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

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


2
Однак це не зовсім доказовий стан, якщо ви не зробите два запити в рамках транзакції. Однак це, як правило, не є проблемою.
NickZoic

Під "надійним" я мав на увазі, що сам SQL завжди повертає потрібний результат, а під "куленепроникним" я мав на увазі, що немає помилок MySQL, що перешкоджають використанню SQL. На відміну від використання SQL_CALC_FOUND_ROWS з ORDER BY і LIMIT, відповідно до помилки, яку я згадав.
статикан

5
У складних запитах використання SQL_CALC_FOUND_ROWS для отримання кількості в одному запиті майже завжди буде повільніше, ніж виконання двох окремих запитів. Це тому, що це означає, що всі рядки потрібно буде отримати повністю, незалежно від межі, тоді повертаються лише ті, що вказані в пункті LIMIT. Дивіться також мою відповідь, яка містить посилання.
thomasrutter

Залежно від причини, яка вам потрібна, ви також можете подумати про те, щоб не отримати загальних результатів. Це стає більш поширеною практикою впровадження методів автоматичного підключення до сторінки. Такі сайти, як Facebook, Twitter, Bing та Google, використовують цей метод вже віками.
Томас Б

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: percona.com/blog/2008/09/24/…
techdude

27

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

У багатьох програмах найімовірнішим результатом буде те, що всі результати вміщуються на одній сторінці, а необхідність робити пагинацію - це виняток, а не норма. У цих випадках перший запит не отримає максимальну кількість результатів.

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

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


1
@thomasrutter У мене був такий самий підхід, проте я виявив у цьому недолік. Тоді на кінцевій сторінці результатів не буде даних про сторінки. наприклад, скажімо, що кожна сторінка повинна мати 25 результатів, остання сторінка, ймовірно, не буде такою кількістю, скажімо, вона має 7 ... це означає, що кількість (*) ніколи не буде запущена, і тому жодна сторінка не відображатиметься до користувач.
Duellsy

2
Ні - якщо у вас, наприклад, 200 результатів, ви запитуєте наступні 25, і ви отримуєте лише 7 назад, що говорить про те, що загальна кількість результатів - 207, і тому вам не потрібно робити ще один запит із COUNT (*) тому що ви вже знаєте, що це буде говорити. Ви маєте всю інформацію, необхідну для показу сторінки. Якщо у вас виникають проблеми з тим, що сторінка не відображається користувачеві, ви маєте помилку десь в іншому місці.
thomasrutter

15

У більшості ситуацій набагато швидше і менш ресурсомістко це зробити за двома окремими запитами, ніж зробити це в одному, хоча це здається контрінтуїтивним.

Якщо ви використовуєте SQL_CALC_FOUND_ROWS, то для великих таблиць ваш запит набагато повільніше, значно повільніше, ніж виконання двох запитів, перший з COUNT (*), а другий з обмеженням. Причиною цього є те, що 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/

Щоб отримати додаткові відомості про оптимізацію сторінки, перевірте цю публікацію та цю публікацію .


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пунктом
Erenor Paz

кількість запитів під запитом (*) вимагатиме те саме, де пункт, інакше він не поверне правильну кількість результатів
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
Це не дає відповіді на питання, і замовлення на рейд - це дійсно погана ідея.
Dan Walmsley
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.