Pourquoi le décalage LIMIT supérieur de MYSQL ralentit-il la requête?

173

Scénario en bref: une table avec plus de 16 millions d'enregistrements [2 Go de taille]. Plus le décalage LIMIT est élevé avec SELECT, plus la requête est lente lorsque vous utilisez ORDER BY * primary_key *

Alors

SELECT * FROM large ORDER BY `id`  LIMIT 0, 30 

prend bien moins que

SELECT * FROM large ORDER BY `id` LIMIT 10000, 30 

Cela ne commande que 30 disques et de toute façon. Ce n'est donc pas la surcharge de ORDER BY.
Désormais, lors de la récupération des 30 dernières lignes, cela prend environ 180 secondes. Comment puis-je optimiser cette simple requête?

Rahman
la source
REMARQUE: je suis l'auteur. MySQL ne fait pas référence à l'index (PRIMARY) dans les cas ci-dessus. voir le lien ci-dessous par l'utilisateur "Quassnoi" pour l'explication.
Rahman

Réponses:

197

Il est normal que des décalages plus élevés ralentissent la requête, car la requête doit compter les premiers OFFSET + LIMITenregistrements (et n'en prendre que LIMIT). Plus cette valeur est élevée, plus la requête s'exécute longtemps.

La requête ne peut pas aller directement à OFFSET car, premièrement, les enregistrements peuvent être de longueur différente et, deuxièmement, il peut y avoir des espaces entre les enregistrements supprimés. Il doit vérifier et compter chaque enregistrement sur son chemin.

En supposant qu'il ids'agit PRIMARY KEYd'une MyISAMtable, vous pouvez l'accélérer en utilisant cette astuce:

SELECT  t.*
FROM    (
        SELECT  id
        FROM    mytable
        ORDER BY
                id
        LIMIT 10000, 30
        ) q
JOIN    mytable t
ON      t.id = q.id

Consultez cet article:

Quassnoi
la source
7
Le comportement de «recherche précoce de ligne» de MySQL était la réponse pour laquelle il parle si longtemps. Par l'astuce que vous avez fournie, seuls les identifiants correspondants (par l'index directement) sont liés, ce qui économise les recherches de lignes inutiles d'un trop grand nombre d'enregistrements. Cela a fait l'affaire, hourra!
Rahman
4
@harald: qu'entendez-vous exactement par «ne fonctionne pas»? Il s'agit d'une pure amélioration des performances. S'il n'y a pas d'index utilisable par ORDER BYou si l'index couvre tous les champs dont vous avez besoin, vous n'avez pas besoin de cette solution de contournement.
Quassnoi
6
@ f055: la réponse dit "accélérer", pas "rendre instantané". Avez-vous lu la toute première phrase de la réponse?
Quassnoi
3
Est-il possible d'exécuter quelque chose comme ça pour InnoDB?
NeverEndingQueue
3
@Lanti: postez-la en tant que question séparée et n'oubliez pas de la taguer postgresql. C'est une réponse spécifique à MySQL.
Quassnoi
220

J'ai eu exactement le même problème moi-même. Étant donné que vous souhaitez collecter une grande quantité de ces données et non un ensemble spécifique de 30, vous exécuterez probablement une boucle et incrémenterez le décalage de 30.

Donc, ce que vous pouvez faire à la place est:

  1. Conserver le dernier identifiant d'un ensemble de données (30) (par exemple, lastId = 530)
  2. Ajouter la condition WHERE id > lastId limit 0,30

Ainsi, vous pouvez toujours avoir un offset ZERO. Vous serez surpris par l'amélioration des performances.

Nikos Kyr
la source
Cela fonctionne-t-il s'il y a des lacunes? Que faire si vous n'avez pas une seule clé unique (une clé composite par exemple)?
xaisoft
8
Il n'est peut-être pas évident pour tous que cela ne fonctionne que si votre jeu de résultats est trié par cette clé, dans l'ordre croissant (pour l'ordre décroissant, la même idée fonctionne, mais remplacez> lastid par <lastid.) Peu importe si c'est le clé primaire, ou un autre champ (ou groupe de champs.)
Eloff
Bien joué cet homme! Une solution très simple qui a résolu mon problème :-)
oodavid
30
Juste une note que limite / offset est souvent utilisé dans les résultats paginés, et tenir lastId n'est tout simplement pas possible parce que l'utilisateur peut sauter à n'importe quelle page, pas toujours la page suivante. En d'autres termes, le décalage doit souvent être calculé dynamiquement en fonction de la page et de la limite, au lieu de suivre un modèle continu.
Tom
3
Je parle plus longuement de "se souvenir où vous vous êtes arrêté" dans mysql.rjweb.org/doc.php/pagination
Rick James
17

MySQL ne peut pas aller directement au 10000ème enregistrement (ou au 80000ème octet comme vous le suggérez) car il ne peut pas supposer qu'il est emballé / ordonné comme ça (ou qu'il a des valeurs continues de 1 à 10000). Bien qu'il puisse en être ainsi en réalité, MySQL ne peut pas supposer qu'il n'y a pas de trous / lacunes / identifiants supprimés.

Ainsi, comme bobs l'a noté, MySQL devra récupérer 10000 lignes (ou parcourir les 10000ème entrées de l'index id) avant de trouver les 30 à renvoyer.

EDIT : pour illustrer mon propos

Notez que bien que

SELECT * FROM large ORDER BY id LIMIT 10000, 30 

serait lent (euh) ,

SELECT * FROM large WHERE id >  10000 ORDER BY id LIMIT 30 

serait rapide (heu) , et renverrait les mêmes résultats à condition qu'il n'y ait pas de ids manquants (c.-à-d. lacunes).

Riedsio
la source
2
C'est correct. Mais comme il est limité par "id", pourquoi cela prend-il autant de temps lorsque cet identifiant est dans un index (clé primaire)? L'optimiseur doit se référer directement à cet index, puis récupérer les lignes avec les identifiants correspondants (qui proviennent de cet index)
Rahman
1
Si vous avez utilisé une clause WHERE sur id, elle pourrait aller directement à cette marque. Cependant, si vous y mettez une limite, triée par id, c'est juste un contre-sens relatif au début, donc il doit traverser tout le chemin.
Riedsio
Très bon article eversql.com
Pažout
A travaillé pour moi @Riedsio Merci.
mahesh kajale
8

J'ai trouvé un exemple intéressant pour optimiser les requêtes SELECT ORDER BY id LIMIT X, Y. J'ai 35 millions de lignes, il a donc fallu environ 2 minutes pour trouver une plage de lignes.

Voici l'astuce:

select id, name, address, phone
FROM customers
WHERE id > 990
ORDER BY id LIMIT 1000;

Il suffit de mettre le WHERE avec le dernier identifiant pour augmenter considérablement les performances. Pour moi, c'était de 2 minutes à 1 seconde :)

Autres astuces intéressantes ici: http://www.iheavy.com/2013/06/19/3-ways-to-optimize-for-paging-in-mysql/

Cela fonctionne aussi avec des cordes

sym
la source
1
cela ne fonctionne que pour les tables, où aucune donnée n'est supprimée
miro
1
@miro Ce n'est vrai que si vous travaillez sous l'hypothèse que votre requête peut effectuer des recherches sur des pages aléatoires, ce que je ne pense pas que cette affiche suppose. Bien que je n'aime pas cette méthode pour la plupart des cas du monde réel, cela fonctionnera avec des lacunes tant que vous la basez toujours sur le dernier identifiant obtenu.
Gremio
5

La partie chronophage des deux requêtes consiste à récupérer les lignes de la table. Logiquement parlant, dans la LIMIT 0, 30version, seules 30 lignes doivent être récupérées. Dans la LIMIT 10000, 30version, 10000 lignes sont évaluées et 30 lignes sont renvoyées. Il peut y avoir une optimisation du processus de lecture des données, mais considérez ce qui suit:

Et si vous aviez une clause WHERE dans les requêtes? Le moteur doit renvoyer toutes les lignes qualifiées, puis trier les données et enfin obtenir les 30 lignes.

Considérez également le cas où les lignes ne sont pas traitées dans la séquence ORDER BY. Toutes les lignes éligibles doivent être triées pour déterminer les lignes à renvoyer.

bobs
la source
1
se demandant simplement pourquoi il faut du temps pour récupérer ces 10000 lignes. L'index utilisé sur ce champ (id, qui est une clé primaire) devrait rendre la récupération de ces lignes aussi rapide que la recherche de cet index PK pour l'enregistrement no. 10000, ce qui à son tour est censé être rapide en recherchant le fichier vers ce décalage multiplié par la longueur de l'enregistrement d'index, (c'est-à-dire en recherchant 10000 * 8 = octet no 80000 - étant donné que 8 est la longueur de l'enregistrement d'index)
Rahman
@Rahman - La seule façon de compter au-delà des 10000 lignes est de les parcourir une par une. Cela peut simplement impliquer un index, mais les lignes d'index prennent encore du temps à parcourir. Il n'y a pas de structure MyISAM ou InnoDB qui peut correctement (dans tous les cas) "chercher" pour enregistrer 10000. La suggestion 10000 * 8 suppose (1) MyISAM, (2) enregistrement de longueur FIXED, et (3) jamais aucune suppression de la table . Quoi qu'il en soit, les index MyISAM sont des BTrees, donc cela ne fonctionnerait pas.
Rick James
Comme cette réponse l'a indiqué, je pense que la partie la plus lente est la recherche de lignes, ne parcourant pas les index (ce qui, bien sûr, s'additionnera également, mais loin d'être autant que les recherches de lignes sur le disque). Sur la base des requêtes de solution de contournement fournies pour ce problème, je pense que les recherches de lignes ont tendance à se produire si vous sélectionnez des colonnes en dehors de l'index - même si elles ne font pas partie de la clause order by ou where. Je n'ai pas trouvé de raison pour laquelle cela est nécessaire, mais il semble que certaines des solutions de contournement aident.
Gremio
1

Pour ceux qui sont intéressés par une comparaison et des chiffres :)

Expérience 1: l'ensemble de données contient environ 100 millions de lignes. Chaque ligne contient plusieurs BIGINT, TINYINT, ainsi que deux champs TEXT (délibérément) contenant environ 1k caractères.

  • Bleu: = SELECT * FROM post ORDER BY id LIMIT {offset}, 5
  • Orange: = méthode de @ Quassnoi. SELECT t.* FROM (SELECT id FROM post ORDER BY id LIMIT {offset}, 5) AS q JOIN post t ON t.id = q.id
  • Bien entendu, la troisième méthode ... WHERE id>xxx LIMIT 0,5n'apparaît pas ici car elle doit être en temps constant.

Expérience 2: chose similaire, sauf qu'une ligne n'a que 3 BIGINTs.

  • vert: = le bleu avant
  • rouge: = l'orange avant

entrez la description de l'image ici

ch271828n
la source