web-dev-qa-db-fra.com

Pagination MySQL sans double requête?

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), d'abord je fais quelque chose comme

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

Après avoir obtenu num_rows (requête), j'ai le nombre de résultats. Mais 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: existe-t-il de toute façon de récupérer le nombre total de résultats qui seraient fournis ET de limiter les résultats renvoyés dans une seule requête? Ou tout autre moyen plus efficace de le faire. Merci!

102
atp

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

L'autre méthode consiste à utiliser la clause SQL_CALC_FOUND_ROWS, Puis à appeler SELECT FOUND_ROWS(). à part le fait que vous devez ensuite mettre l'appel FOUND_ROWS(), il y a un problème avec ceci: Il y a n bug dans MySQL que cela chatouille et qui affecte ORDER BY les requêtes, ce qui le rend beaucoup plus lent sur les grandes tables que l'approche naïve de deux requêtes.

61
staticsan

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, 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 retourner dans un ordre des plus pertinents 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 page deux, pas à neuf.

64
Derrick

Une autre approche pour éviter la double interrogation consiste à extraire toutes les lignes de la page en cours à l'aide d'une clause LIMIT d'abord, puis à ne faire une deuxième requête COUNT (*) que si le nombre maximal 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 de 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 débordent rarement sur une deuxième page. Les commentaires sur une réponse dépassent rarement la limite de 5 ou plus requise pour les montrer tous.

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 (*) - qui devrait couvrir la majorité des situations.

25
thomasrutter

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 est beaucoup plus lente, même plus lente que l'exécution de deux requêtes, la première avec COUNT (*) et la seconde avec LIMIT. La raison en est que SQL_CALC_FOUND_ROWS provoque l'application de la clause LIMIT après récupération des lignes au lieu d'avant, donc 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 des deux requêtes, la première ne récupérant que COUNT (*) et pas réellement la récupération des 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 doit uniquement regarder les premières lignes $ offset + $ limit, puis revenir.

Cet article du blog sur les performances de 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 .

15
thomasrutter

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 principal. Dans PHP par exemple, vous pourriez 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 à considérer, cela devient très rapidement inefficace. Le décompte pré-calculé peut être une bonne idée à examiner.

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

2
Kama
query = SELECT col, col2, (SELECT COUNT(*) FROM `table`) AS total FROM `table` WHERE `some_condition` LIMIT 0, 10
2
Cris McLaughlin

Vous pouvez réutiliser la majeure partie de la requête dans une sous-requête et la définir sur un identifiant. Par exemple, une requête de film qui trouve des films contenant l'ordre 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 de la base de données et j'espère que quelqu'un sera en mesure d'optimiser un peu mieux. En l'état, il est exécuté directement à partir de l'interface de ligne de commande SQL, ils prennent tous les deux ~ 0,02 seconde sur mon ordinateur portable.

0
Philip Rollins