Pagination MySQL sans double requête?

115

Je me demandais s'il y avait un moyen d'obtenir le nombre de résultats d'une requête MySQL, et en même temps de limiter les résultats.

La façon dont la pagination fonctionne (si je comprends bien), je fais d'abord quelque chose comme

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

Après avoir obtenu le nombre_rows (requête), j'ai le nombre de résultats. Mais ensuite, pour limiter mes résultats, je dois faire une deuxième requête comme:

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

Ma question: est-il possible à la fois de récupérer le nombre total de résultats qui seraient donnés ET de limiter les résultats renvoyés dans une seule requête? Ou tout autre moyen plus efficace de le faire. Merci!

atp
la source
8
Bien que vous n'ayez pas COUNT (*) dans query2
dlofrodloh

Réponses:

66

Non, c'est le nombre d'applications qui veulent paginer doivent le faire. Il est fiable et à l'épreuve des balles, bien qu'il fasse la requête deux fois. Mais vous pouvez mettre en cache le décompte pendant quelques secondes et cela aidera beaucoup.

L'autre façon consiste à utiliser une SQL_CALC_FOUND_ROWSclause puis à appeler SELECT FOUND_ROWS(). à part le fait que vous devez mettre l' FOUND_ROWS()appel après, il y a un problème avec ceci: il y a un bogue dans MySQL qui chatouille qui affecte les ORDER BYrequêtes, ce qui le rend beaucoup plus lent sur les grandes tables que l'approche naïve de deux requêtes.

statique
la source
2
Cependant, ce n'est pas tout à fait une preuve de condition de concurrence, sauf si vous effectuez les deux requêtes dans une transaction. Ce n'est généralement pas un problème, cependant.
NickZoic
Par «fiable», je voulais dire que le SQL lui-même retournera toujours le résultat souhaité, et par «à l'épreuve des balles», je voulais dire qu'il n'y a pas de bogues MySQL qui gênent le SQL que vous pouvez utiliser. Contrairement à l'utilisation de SQL_CALC_FOUND_ROWS avec ORDER BY et LIMIT, selon le bogue que j'ai mentionné.
staticsan
5
Sur les requêtes complexes, l'utilisation de SQL_CALC_FOUND_ROWS pour récupérer le nombre dans la même requête sera presque toujours plus lente que d'effectuer deux requêtes distinctes. En effet, cela signifie que toutes les lignes devront être récupérées dans leur intégralité, quelle que soit la limite, alors seules celles spécifiées dans la clause LIMIT sont renvoyées. Voir aussi ma réponse qui a des liens.
thomasrutter
Selon la raison pour laquelle vous en avez besoin, vous voudrez peut-être également penser à ne pas récupérer le total des résultats. Il est de plus en plus courant d'implémenter des méthodes de pagination automatique. Des sites comme Facebook, Twitter, Bing et Google utilisent cette méthode depuis des lustres.
Thomas B
68

Je ne fais presque jamais deux requêtes.

Renvoyez simplement une ligne de plus que nécessaire, affichez seulement 10 sur la page, et s'il y en a plus que ce qui est affiché, affichez un bouton "Suivant".

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.

Votre requête doit revenir dans l'ordre de la plus pertinente en premier. Il y a de fortes chances que la plupart des gens ne se soucient pas d'aller à la page 236 sur 412.

Lorsque vous effectuez une recherche Google et que vos résultats ne sont pas sur la première page, vous accédez probablement à la deuxième page, et non à la neuf.

Derrick
la source
42
En fait, si je ne le trouve pas sur la première page d'une requête Google, je passe généralement à la page neuf.
Phil
3
@Phil J'ai déjà entendu ça mais pourquoi faire ça?
TK123
5
Un peu tard, mais voici mon raisonnement. Certaines recherches sont dominées par des fermes de liens optimisées pour les moteurs de recherche. Ainsi, les premières pages sont les différentes fermes qui se disputent la position numéro 1, le résultat utile est probablement toujours associé à la requête, mais pas en haut.
Phil
4
COUNTest une fonction d'agrégation. Comment renvoyer le décompte et tous les résultats en une seule requête? La requête ci-dessus ne renverra qu'une seule ligne, quel que soit le LIMITparamètre défini. Si vous ajoutez GROUP BY, tous les résultats COUNTseront
renvoyés,
2
C'est l'une des approches recommandées par Percona: percona.com/blog/2008/09/24/…
techdude
27

Une autre approche pour éviter les doubles requêtes consiste à extraire d'abord toutes les lignes de la page actuelle en utilisant une clause LIMIT, puis à n'effectuer une deuxième requête COUNT (*) que si le nombre maximum de lignes a été récupéré.

Dans de nombreuses applications, le résultat le plus probable sera que tous les résultats tiennent sur une seule page, et devoir faire la pagination est l'exception plutôt que la norme. Dans ces cas, la première requête ne récupérera pas le nombre maximum de résultats.

Par exemple, les réponses à une question stackoverflow se retrouvent rarement sur une deuxième page. Les commentaires sur une réponse dépassent rarement la limite de 5 ou plus requise pour tous les montrer.

Donc, dans ces applications, vous pouvez simplement faire une requête avec une LIMIT d'abord, puis tant que cette limite n'est pas atteinte, vous savez exactement combien de lignes il y a sans avoir besoin de faire une deuxième requête COUNT (*) - ce qui devrait couvrent la majorité des situations.

thomasrutter
la source
1
@thomasrutter J'ai eu la même approche, mais j'ai découvert une faille avec elle aujourd'hui. La page finale des résultats n'aura alors pas les données de pagination. c'est-à-dire que chaque page devrait avoir 25 résultats, la dernière page n'en aura probablement pas autant, disons qu'elle en a 7 ... cela signifie que le compte (*) ne sera jamais exécuté, et donc aucune pagination ne sera affichée utilisateur.
duellsy
2
Non - si vous dites 200 résultats, vous interrogez les 25 suivants et vous n'en obtenez que 7, cela vous indique que le nombre total de résultats est de 207 et que vous n'avez donc pas besoin de faire une autre requête avec COUNT (*) parce que vous savez déjà ce que ça va dire. Vous disposez de toutes les informations nécessaires pour afficher la pagination. Si vous rencontrez un problème avec la pagination qui ne s'affiche pas à l'utilisateur, vous avez un bogue ailleurs.
thomasrutter
15

Dans la plupart des situations, il est beaucoup plus rapide et moins gourmand en ressources de le faire en deux requêtes distinctes que de le faire en une seule, même si cela semble contre-intuitif.

Si vous utilisez SQL_CALC_FOUND_ROWS, alors pour les grandes tables, votre requête sera beaucoup plus lente, beaucoup plus lente même que l'exécution de deux requêtes, la première avec un COUNT (*) et la seconde avec une LIMIT. La raison en est que SQL_CALC_FOUND_ROWS entraîne l'application de la clause LIMIT après l' extraction des lignes au lieu d'avant, de sorte qu'il récupère la ligne entière pour tous les résultats possibles avant d'appliquer les limites. Cela ne peut pas être satisfait par un index car il récupère réellement les données.

Si vous adoptez l'approche à deux requêtes, la première ne récupérant que COUNT (*) et ne récupérant pas réellement les données réelles, cela peut être satisfait beaucoup plus rapidement car il peut généralement utiliser des index et n'a pas à récupérer les données de ligne réelles pour chaque ligne qu'il regarde. Ensuite, la deuxième requête n'a besoin que de regarder les premières lignes $ offset + $ limit, puis de revenir.

Cet article du blog de performance MySQL explique cela plus en détail:

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

Pour plus d'informations sur l'optimisation de la pagination, consultez cet article et cet article .

thomasrutter
la source
2

Ma réponse est peut-être en retard, mais vous pouvez ignorer la deuxième requête (avec la limite) et simplement filtrer les informations via votre script back-end. En PHP par exemple, vous pouvez faire quelque chose comme:

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

Mais bien sûr, lorsque vous avez des milliers d'enregistrements à prendre en compte, cela devient très vite inefficace. Le décompte pré-calculé est peut-être une bonne idée à examiner.

Voici une bonne lecture sur le sujet: http://www.percona.com/ppc2009/PPC2009_mysql_pagination.pdf

Kama
la source
Le lien est mort, je suppose que c'est le bon: percona.com/files/presentations/ppc2009/… . Ne sera pas édité car je ne sais pas si c'est le cas.
hectorg87
1
query = SELECT col, col2, (SELECT COUNT(*) FROM `table`) AS total FROM `table` WHERE `some_condition` LIMIT 0, 10
Cris McLaughlin
la source
16
Cette requête renvoie simplement le nombre total d'enregistrements dans la table; pas le nombre d'enregistrements qui correspondent à la condition.
Lawrence Barsanti
1
Le nombre total d'enregistrements est ce qui est nécessaire pour la pagination (@Lawrence).
imme
Oh, eh bien, ajoutez simplement la whereclause à la requête interne et vous obtenez le bon "total" à côté des résultats limit
paginés
le nombre de sous-requêtes (*) nécessiterait la même clause where, sinon il ne retournera pas le nombre correct de résultats
AKrush95
1

Pour tous ceux qui recherchent une réponse en 2020. Selon la documentation MySQL:

"Le modificateur de requête SQL_CALC_FOUND_ROWS et la fonction FOUND_ROWS () qui l'accompagne sont obsolètes à partir de MySQL 8.0.17 et seront supprimés dans une future version de MySQL. En remplacement, envisagez d'exécuter votre requête avec LIMIT, puis une deuxième requête avec COUNT (*) et sans LIMIT pour déterminer s'il y a des lignes supplémentaires. "

Je suppose que cela règle cela.

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

Igor K
la source
0

Vous pouvez réutiliser la plupart de la requête dans une sous-requête et la définir sur un identificateur. Par exemple, une requête de film qui trouve des films contenant le tri des lettres par exécution ressemblerait à ceci sur mon site.

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;

Notez que je ne suis pas un expert en bases de données et j'espère que quelqu'un pourra optimiser cela un peu mieux. En l'état, l'exécuter directement à partir de l'interface de ligne de commande SQL, ils prennent tous deux environ 0,02 seconde sur mon ordinateur portable.

Philip Rollins
la source
-14
SELECT * 
FROM table 
WHERE some_condition 
ORDER BY RAND()
LIMIT 0, 10
John
la source
3
Cela ne répond pas à la question, et une commande par rand est une très mauvaise idée.
Dan Walmsley