Recherche
Peut être aimerez-vous...
Sections du site
Sites Neamar
Lisez ces articles !
| Sélectionner un élément au hasard dans une table MySQL |
|
| Programmation et tuning - Programmation Web | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Écrit par Neamar | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Vendredi, 14 Mai 2010 11:29 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Sur la plupart des sites dits « 2,0 », vous trouverez quelque part un petit module « Un article au hasard », et un « Dernier article ». Souvent, le programmeur derrière ne se pose malheureusement pas de questions pour récupérer cet enregistrement… et pourtant, il devrait ! Car selon les méthodes utilisées et la taille de la table, il peut améliorer sa requête de plusieurs ordres de grandeur. On est parti pour voir ça… Protocole de testTablesJ'effectue les tests sur une table moyennement chargée (10 000 enregistrements) nommée BENCH_Table dans la suite, simulant une bonne base d'articles. Requêtes SQLBENCHMARKPour tester les requêtes SQL présentées ici, je me suis servi de l'instruction BENCHMARK qui permet de calculer plusieurs fois une expression. Pour cet article, j'ai effectué les requêtes 10 000 000 de fois, sauf contre-ordre (dans certains cas vraiment peu optimisés, il aurait fallu des heures de calcul, montrant l'inadéquation de la solution). Cette valeur assez large permet d'obtenir un résultat fiable. Petit exemple de requête avec BENCHMARK : --Répéter 10 000 000 de fois la récupération du titre de l'élément 5 SELECT BENCHMARK(10000000,( SELECT Titre FROM BENCH_Table WHERE ID = 5 )) Attention ! BENCHMARK renvoie toujours 0. Pour récupérer le temps de la requête, il faut utiliser autre chose : le client MySQL en ligne de commande par exemple, qui affiche le temps passé. EXPLAINPour justifier le temps obtenu et l'expliquer, j'utiliserai EXPLAIN, une instruction indispensable pour les gestionnaires de base de données qui affiche le schéma prévu par le compilateur. Une requête EXPLAIN n'exécute pas la demande, mais la compile et affiche les plans de l'exécuteur (je simplifie). EXPLAIN est un vaste sujet, je n'entrerai pas dans le détail et décrirai uniquement les résultats obtenus au cours des tests. Pour un cours complet sur EXPLAIN, RTFM ! Petit exemple de requête avec EXPLAIN : EXPLAIN SELECT Titre FROM BENCH_Table WHERE ID = 5 Et le résultat :
Sélectionner un élément aléatoireC'est sûrement la partie la plus intéressante… mais aussi la plus délaissée par les webmasters ! Présentation de RAND()Dès que l'on parle d'aléatoire, on aura besoin d'une fonction génératrice de nombres pseudo-aléatoires. En MySQL, il s'agit de l'instruction RAND(), classée avec les fonctions mathématiques. RAND() renvoie un nombre aléatoire flottant entre 0 et 1 (1 non compris). Sélectionner un élément aléatoireSi vous vous promenez un peu sur les forums, c'est toujours la même solution qui est donnée : --Tri par rand SELECT ID FROM BENCH_Table ORDER BY RAND() LIMIT 1 C'est logique : on trie aléatoirement et on ne récupère que le premier élément. Sauf que… c'est long, très long : sur la table de tests décrite ci-dessus, il faut 7,6 secondes pour effectuer 1 000 calculs. Si j'avais pris les 100 000 000 prévus, on serait donc à 21 heures de calcul… presque une journée ! EXPLAIN SELECT ID FROM BENCH_Table ORDER BY RAND() LIMIT 1 Que voit-on dans la colonne Extra ? « Using index ; Using temporary ; Using filesort ». Ouch, expliquons un peu.
On comprend mieux : lors de l'exécution, MySQL va ajouter à chaque ligne une colonne contenant la valeur de RAND() (soit 10 000 nouvelles valeurs), puis trier sur cette colonne et ne récupérer que le plus petit résultat. <?php /** * Solution sale */ //Chargement du tableau $MonTableau = listeValeurs(); //Tri du tableau asort($MonTableau) //Récupération du premier élément $Min = $MonTableau[0]; /** * Solution optimisée, propre */ //Chargement du tableau $MonTableau = listeValeurs(); //Récupération du minimum sans tri inutile $Min = min($MonTableau); ?> Mais, comment faire alors ? Hé bien, on réfléchit. Allez allez, on se creuse un peu la tête… Par exemple : --Sélection d'un élément SELECT ID FROM BENCH_Table WHERE ID = ( SELECT CEIL( COUNT(*) * RAND( ) ) FROM BENCH_Table ) À priori, c'est bien ! On calcule la sous-requête, on obtient alors un nombre aléatoire entier, et on récupère cet ID. Comme on récupère à partir d'un index, l'opération « = » sera extrêmement rapide et tout devrait aller pour le mieux.
La deuxième ligne nous informe de notre erreur : UNCACHEABLE SUBQUERY. Autrement dit, il va falloir recalculer la sous-requête sur chaque ligne ! (Une bref lecture du manuel sur la page « How the query cache operates » aurait pu nous renseigner : la présence d'un RAND() empêche la mise en cache. ) Forcément, calculer un CEIL( COUNT( * ) * RAND( ) ) sur 10 000 lignes, c'est coûteux… J'ai un peu cherché comment forcer la mise en cache d'une requête, sans résultat. Alors j'ai arrêté de pleurnicher, et j'ai fait fonctionner à nouveau la matière grise. Donc, un JOIN : --Jointure de l'élément SELECT ID FROM BENCH_Table JOIN ( SELECT FLOOR( COUNT( * ) * RAND( ) ) AS ValeurAleatoire FROM BENCH_Table ) AS V ON BENCH_Table.ID = V.ValeurAleatoire Cette fois, les performances sont au rendez-vous : sur 10 000 000 de tests, on ne monte qu'à 1,278s (à comparer avec la presque-journée de la première solution). Un petit EXPLAIN pour la route :
Ce qui est intéressant ici, c'est la colonne type :
Par acquit de conscience, j'ai testé avec COUNT(*) et MAX(ID) (je suis en InnoDB, ça a peut-être son importance) : le résultat est mieux avec COUNT(*). À la réflexion, ça paraît logique vu que MAX(ID) est obligé de lire toutes les lignes et de comparer à chaque fois avec le maximum courant. Je pensais cependant qu'il suffisait de lire la dernière ligne du fichier d'index… à priori non donc. Note sur les tables « creuses »Les techniques exposées ici fonctionnent parfaitement si vous ne supprimez pas de lignes de votre table. --Jointure de l'élément avec gestion des index supprimés --La différence ? LA jointure se fait sur >=, et pas sur = SELECT ID FROM BENCH_Table JOIN ( SELECT FLOOR( COUNT( * ) * RAND( ) ) AS ValeurAleatoire FROM BENCH_Table ) AS V ON BENCH_Table.ID >= V.ValeurAleatoire LIMIT 1 Forcément, ça ira un peu moins vite (20 secondes, une bonne perte quand même)… Et dans le futurForcément, on se prend à rêver de pouvoir exécuter le code suivant : --Code imaginaire, NE FONCTIONNE PAS SELECT ID FROM BENCH_Table LIMIT FLOOR( COUNT( * ) * RAND( ) ),1 Mais ce n'est pas pour tout de suite… dommage ! Encore une idéeOn peut aussi coupler deux langages (on choisit un enregistrement en PHP/Whatever, puis on le récupère) ou avec des variables SQL : --Utilisation d'une variable aléatoire SELECT @ValeurAleatoire := (RAND() * COUNT(*)) FROM BENCH_Table; SELECT ID FROM BENCH_Table WHERE ID = @ValeurAleatoire; Mais là, je ne peux pas benchmarker ! En théorie, ça marche… Récupérer le dernier élémentDeuxième section donc, pour récupérer le dernier élément. C'est encore plus simple que pour le RAND(), et les trois approches décrites ci-dessus sont possibles : --Sous requête SELECT ID FROM BENCH_Table WHERE ID = ( SELECT MAX( ID ) FROM BENCH_Table ) --Tri ID DESC SELECT ID FROM BENCH_Table ORDER BY ID DESC LIMIT 1 --Jointure SELECT ID FROM BENCH_Table JOIN (SELECT MAX(ID) AS MAX FROM BENCH_Table) AS M ON M.MAX = BENCH_Table.ID Les résultats sont dans un mouchoir de poche : 356ms pour la sous-requête, 366ms pour le tri décroissant, et 441ms. Si la sous-requête fonctionne bien ici, c'est parce qu'elle peut être mise en cache et n'a pas à être recalculée à chaque ligne. Un EXPLAIN nous indique « Select tables optimized away ». Le tri décroissant lui se fait sur un index (« using index »), ce qui explique sa vélocité. La jointure est un peu défavorisée (trois jointures) et devraient donc être oubliée pour ce cas particulier. ConclusionEn conclusion : pour un élément aléatoire, ne pas utiliser ORDER BY RAND() ! En revanche, pour récupérer le dernier élément tout se vaut à peu près niveau temps de calcul. Pour la mémoire, c'est une autre paire de manches ! Et surtout, réfléchir à ce qu'on veut et ce qui est fait : la distinction est importante ! |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Mise à jour le Dimanche, 16 Mai 2010 19:06 |
