Quel est le plus rapide? SELECT SQL_CALC_FOUND_ROWS FROM `table`, ou SELECT COUNT (*)

176

Lorsque vous limitez le nombre de lignes à renvoyer par une requête SQL, généralement utilisée dans la pagination, il existe deux méthodes pour déterminer le nombre total d'enregistrements:

Méthode 1

Incluez l' SQL_CALC_FOUND_ROWSoption dans l'original SELECT, puis obtenez le nombre total de lignes en exécutant SELECT FOUND_ROWS():

SELECT SQL_CALC_FOUND_ROWS * FROM table WHERE id > 100 LIMIT 10;
SELECT FOUND_ROWS();  

Méthode 2

Exécutez la requête normalement, puis obtenez le nombre total de lignes en exécutant SELECT COUNT(*)

SELECT * FROM table WHERE id > 100 LIMIT 10;
SELECT COUNT(*) FROM table WHERE id > 100;  

Quelle méthode est la meilleure / la plus rapide?

Jrgns
la source

Réponses:

120

Ça dépend. Voir l'article du blog MySQL Performance sur ce sujet: http://www.mysqlperformanceblog.com/2007/08/28/to-sql_calc_found_rows-or-not-to-sql_calc_found_rows/

Juste un bref résumé: Peter dit que cela dépend de vos index et d'autres facteurs. De nombreux commentaires sur le post semblent indiquer que SQL_CALC_FOUND_ROWS est presque toujours plus lent - parfois jusqu'à 10 fois plus lent - que d'exécuter deux requêtes.

Nathan
la source
27
Je peux le confirmer - je viens de mettre à jour une requête avec 4 jointures sur une base de données de 168 000 lignes. La sélection des 100 premières lignes avec un a SQL_CALC_FOUND_ROWSpris plus de 20 secondes; l'utilisation d'une COUNT(*)requête distincte a pris moins de 5 secondes (pour les requêtes de nombre + résultats)
Sam Dufel
9
Des résultats très intéressants. Étant donné que la documentation de MySQL suggère explicitement que ce SQL_CALC_FOUND_ROWSsera plus rapide, je me demande dans quelles situations (le cas échéant) il est en fait plus rapide!
svidgen
12
vieux sujet, mais pour ceux qui sont toujours intéressants! Je viens de terminer ma vérification sur INNODB à partir de 10 vérifications, je peux dire que c'est 26 (2query) contre 9.2 (1 requête) SELECT SQL_CALC_FOUND_ROWS tblA. *, TblB.id AS 'b_id', tblB.city AS 'b_city', tblC.id AS 'C_Id', tblC.type AS 'C_Type', tblD.id AS 'D_ID', tblD.extype AS 'd_extype', tblY.id AS 'y_id', tblY.ydt AS y_ydt à partir de tblA, tblB, tblC, tblD, tblY OÙ tblA.b = tblC.id AND tblA.c = tblB.id AND tblA.d = tblD.id AND tblA.y = tblY.id
Al Po
4
Je viens de lancer cette expérience et SQLC_CALC_FOUND_ROWS était beaucoup plus rapide que deux requêtes. Maintenant, ma table principale ne fait que 65k et deux jointures de quelques centaines, mais la requête principale prend 0,18 seconde avec ou sans SQLC_CALC_FOUND_ROWS, mais lorsque j'ai exécuté une deuxième requête avec COUNT ( id), cela a pris 0,25 seul.
transilvlad
1
En plus des problèmes de performances possibles, considérez que cela FOUND_ROWS()est obsolète dans MySQL 8.0.17. Voir aussi la réponse de @ madhur-bhaiya.
arueckauer
19

Lors du choix de la «meilleure» approche, une considération plus importante que la vitesse peut être la maintenabilité et l'exactitude de votre code. Si tel est le cas, SQL_CALC_FOUND_ROWS est préférable car vous n'avez besoin de gérer qu'une seule requête. L'utilisation d'une seule requête exclut complètement la possibilité d'une différence subtile entre les requêtes principale et count, ce qui peut conduire à un COUNT inexact.

Jeff Clemens
la source
11
Cela dépend de votre configuration. Si vous utilisez une sorte d'ORM ou de générateur de requêtes, il est très facile d'utiliser les mêmes critères where pour les deux requêtes, d'échanger les champs de sélection contre un nombre et de supprimer la limite. Vous ne devriez jamais écrire les critères deux fois.
mpen
Je tiens à souligner que je préfère conserver le code en utilisant deux requêtes SQL simples, assez standard et faciles à comprendre, plutôt qu'une qui utilise une fonctionnalité MySQL propriétaire - ce qui est à noter est obsolète dans les nouvelles versions de MySQL.
thomasrutter
15

MySQL a commencé à désapprouver les SQL_CALC_FOUND_ROWSfonctionnalités à partir de la version 8.0.17.

Par conséquent, il est toujours préférable d'envisager d'exécuter votre requête avec LIMIT, puis une deuxième requête avec COUNT(*)et sans LIMITpour déterminer s'il existe des lignes supplémentaires.

À partir de la documentation :

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.

COUNT (*) est soumis à certaines optimisations. SQL_CALC_FOUND_ROWS entraîne la désactivation de certaines optimisations.

Utilisez plutôt ces requêtes:

SELECT * FROM tbl_name WHERE id > 100 LIMIT 10;
SELECT COUNT(*) WHERE id > 100;

En outre, il SQL_CALC_FOUND_ROWSa été observé avoir plus de problèmes en général, comme expliqué dans le MySQL WL # 12615 :

SQL_CALC_FOUND_ROWS a un certain nombre de problèmes. Tout d'abord, c'est lent. Souvent, il serait moins coûteux d'exécuter la requête avec LIMIT puis un SELECT COUNT ( ) séparé pour la même requête, car COUNT ( ) peut utiliser des optimisations qui ne peuvent pas être effectuées lors de la recherche de l'ensemble de résultats (par exemple, filesort peut être ignoré pour COUNT (*), alors qu'avec CALC_FOUND_ROWS, nous devons désactiver certaines optimisations de tri de fichiers pour garantir le bon résultat)

Plus important encore, sa sémantique est très floue dans un certain nombre de situations. En particulier, lorsqu'une requête a plusieurs blocs de requête (par exemple avec UNION), il n'y a tout simplement aucun moyen de calculer le nombre de lignes «aurait-il été» en même temps que de produire une requête valide. Au fur et à mesure que l'exécuteur de l'itérateur progresse vers ce type de requêtes, il est vraiment difficile d'essayer de conserver la même sémantique. De plus, s'il y a plusieurs LIMITs dans la requête (par exemple pour les tables dérivées), il n'est pas nécessairement clair à laquelle d'entre elles SQL_CALC_FOUND_ROWS doit se référer. Ainsi, de telles requêtes non triviales auront nécessairement une sémantique différente dans l'exécuteur de l'itérateur par rapport à ce qu'elles avaient auparavant.

Enfin, la plupart des cas d'utilisation où SQL_CALC_FOUND_ROWS semblerait utile devraient simplement être résolus par d'autres mécanismes que LIMIT / OFFSET. Par exemple, un annuaire téléphonique doit être paginé par lettre (à la fois en termes d'UX et en termes d'utilisation d'index), et non par numéro d'enregistrement. Les discussions sont de plus en plus organisées par défilement infini par date (permettant à nouveau l'utilisation de l'index), et non par pagination par numéro de poste. Etc.

Madhur Bhaiya
la source
Comment effectuer ces deux sélections en tant qu'opération atomique? Que faire si quelqu'un insère une ligne avant la requête SELECT COUNT (*)? Merci.
Dom
@Dom si vous avez MySQL8 +, vous pouvez exécuter à la fois la requête en une seule requête en utilisant les fonctions Window; mais ce ne sera pas une solution optimale car les index ne seront pas utilisés correctement. Une autre option consiste à entourer ces deux requêtes avec LOCK TABLES <tablename>et UNLOCK TABLES. La troisième option et (meilleure IMHO) est de repenser la pagination. Veuillez lire: mariadb.com/kb/en/library/pagination-optimization
Madhur Bhaiya
14

Selon l'article suivant: https://www.percona.com/blog/2007/08/28/to-sql_calc_found_rows-or-not-to-sql_calc_found_rows/

Si vous avez un INDEX sur votre clause where (si id est indexé dans votre cas), il est préférable de ne pas utiliser SQL_CALC_FOUND_ROWS et d'utiliser 2 requêtes à la place, mais si vous n'avez pas d'index sur ce que vous avez mis dans votre clause where (id dans votre cas) alors utiliser SQL_CALC_FOUND_ROWS est plus efficace.

patapouf_ai
la source
8

IMHO, la raison pour laquelle 2 requêtes

SELECT * FROM count_test WHERE b = 666 ORDER BY c LIMIT 5;
SELECT count(*) FROM count_test WHERE b = 666;

sont plus rapides que d'utiliser SQL_CALC_FOUND_ROWS

SELECT SQL_CALC_FOUND_ROWS * FROM count_test WHERE b = 555 ORDER BY c LIMIT 5;

doit être considéré comme un cas particulier.

Elle dépend en fait de la sélectivité de la clause WHERE par rapport à la sélectivité de la clause implicite équivalente à ORDER + LIMIT.

Comme Arvids l'a dit en commentaire ( http://www.mysqlperformanceblog.com/2007/08/28/to-sql_calc_found_rows-or-not-to-sql_calc_found_rows/#comment-1174394 ), le fait que l'EXPLAIN utilise, ou non, une table temporelle, devrait être une bonne base pour savoir si SCFR sera plus rapide ou non.

Mais, comme je l'ai ajouté ( http://www.mysqlperformanceblog.com/2007/08/28/to-sql_calc_found_rows-or-not-to-sql_calc_found_rows/#comment-8166482 ), le résultat dépend vraiment, vraiment du cas. Pour un paginateur particulier, vous pourriez arriver à la conclusion que «pour les 3 premières pages, utilisez 2 requêtes; pour les pages suivantes, utilisez un SCFR »!

Pierre-Olivier Vares
la source
6

Suppression de certains SQL inutiles, puis COUNT(*)sera plus rapide que SQL_CALC_FOUND_ROWS. Exemple:

SELECT Person.Id, Person.Name, Job.Description, Card.Number
FROM Person
JOIN Job ON Job.Id = Person.Job_Id
LEFT JOIN Card ON Card.Person_Id = Person.Id
WHERE Job.Name = 'WEB Developer'
ORDER BY Person.Name

Alors comptez sans partie inutile:

SELECT COUNT(*)
FROM Person
JOIN Job ON Job.Id = Person.Job_Id
WHERE Job.Name = 'WEB Developer'
Jessé Catrinck
la source
3

Il existe d'autres options pour vous de comparer:

1.) Une fonction de fenêtre renverra directement la taille réelle (testée dans MariaDB):

SELECT 
  `mytable`.*,
  COUNT(*) OVER() AS `total_count`
FROM `mytable`
ORDER BY `mycol`
LIMIT 10, 20

2.) En sortant de la boîte, la plupart du temps, les utilisateurs n'ont pas besoin de connaître la taille EXACTE de la table, une approximation est souvent suffisante.

SELECT `TABLE_ROWS` AS `rows_approx`
FROM `INFORMATION_SCHEMA`.`TABLES`
WHERE `TABLE_SCHEMA` = DATABASE()
  AND `TABLE_TYPE` = "BASE TABLE"
  AND `TABLE_NAME` = ?
Code4R7
la source