Comment puis-je optimiser la fonction ORDER BY RAND () de MySQL?

90

J'aimerais optimiser mes requêtes afin que j'examine mysql-slow.log.

La plupart de mes requêtes lentes contiennent ORDER BY RAND(). Je ne trouve pas de vraie solution pour résoudre ce problème. Il existe une solution possible chez MySQLPerformanceBlog mais je ne pense pas que cela soit suffisant. Sur les tables mal optimisées (ou fréquemment mises à jour, gérées par l'utilisateur), cela ne fonctionne pas ou je dois exécuter deux requêtes ou plus avant de pouvoir sélectionner ma PHPligne aléatoire générée.

Existe-t-il une solution à ce problème?

Un exemple factice:

SELECT  accomodation.ac_id,
        accomodation.ac_status,
        accomodation.ac_name,
        accomodation.ac_status,
        accomodation.ac_images
FROM    accomodation, accomodation_category
WHERE   accomodation.ac_status != 'draft'
        AND accomodation.ac_category = accomodation_category.acat_id
        AND accomodation_category.acat_slug != 'vendeglatohely'
        AND ac_images != 'b:0;'
ORDER BY
        RAND()
LIMIT 1
fabrik
la source
Une duplication possible de MySQL sélectionne rapidement 10 lignes aléatoires à partir de 600K lignes
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功

Réponses:

67

Essaye ça:

SELECT  *
FROM    (
        SELECT  @cnt := COUNT(*) + 1,
                @lim := 10
        FROM    t_random
        ) vars
STRAIGHT_JOIN
        (
        SELECT  r.*,
                @lim := @lim - 1
        FROM    t_random r
        WHERE   (@cnt := @cnt - 1)
                AND RAND(20090301) < @lim / @cnt
        ) i

Ceci est particulièrement efficace sur MyISAM(puisque le COUNT(*)est instantané), mais même en InnoDBson 10temps plus efficace que ORDER BY RAND().

L'idée principale ici est de ne pas trier, mais plutôt de conserver deux variables et de calculer la valeur running probabilityd'une ligne à sélectionner à l'étape en cours.

Voir cet article dans mon blog pour plus de détails:

Mettre à jour:

Si vous ne devez sélectionner qu'un seul enregistrement aléatoire, essayez ceci:

SELECT  aco.*
FROM    (
        SELECT  minid + FLOOR((maxid - minid) * RAND()) AS randid
        FROM    (
                SELECT  MAX(ac_id) AS maxid, MIN(ac_id) AS minid
                FROM    accomodation
                ) q
        ) q2
JOIN    accomodation aco
ON      aco.ac_id =
        COALESCE
        (
        (
        SELECT  accomodation.ac_id
        FROM    accomodation
        WHERE   ac_id > randid
                AND ac_status != 'draft'
                AND ac_images != 'b:0;'
                AND NOT EXISTS
                (
                SELECT  NULL
                FROM    accomodation_category
                WHERE   acat_id = ac_category
                        AND acat_slug = 'vendeglatohely'
                )
        ORDER BY
                ac_id
        LIMIT   1
        ),
        (
        SELECT  accomodation.ac_id
        FROM    accomodation
        WHERE   ac_status != 'draft'
                AND ac_images != 'b:0;'
                AND NOT EXISTS
                (
                SELECT  NULL
                FROM    accomodation_category
                WHERE   acat_id = ac_category
                        AND acat_slug = 'vendeglatohely'
                )
        ORDER BY
                ac_id
        LIMIT   1
        )
        )

Cela suppose que les vôtres ac_idsont distribués plus ou moins uniformément.

Quassnoi
la source
Bonjour Quassnoi! Tout d'abord, merci pour votre réponse rapide! C'est peut-être ma faute, mais votre solution n'est toujours pas claire. Je mettrai à jour mon message d'origine avec un exemple concret et je serai heureux si vous expliquez votre solution sur cet exemple.
fabrik
il y avait une faute de frappe à "JOIN accomodation aco ON aco.id =" où aco.id est vraiment aco.ac_id. Par contre, la requête corrigée n'a pas fonctionné pour moi car elle génère une erreur # 1241 - L'opérande doit contenir 1 colonne (s) au cinquième SELECT (le quatrième sous-sélection). J'ai essayé de trouver le problème avec les parenthèses (si je ne me trompe pas) mais je ne trouve pas encore le problème.
fabrik
@fabrik: Essayez maintenant. Ce serait vraiment utile si vous postiez les scripts de table afin que je puisse les vérifier avant de poster.
Quassnoi
Merci, ça marche! :) Pouvez-vous modifier la partie JOIN ... ON aco.id pour JOIN ... ON aco.ac_id afin que je puisse accepter votre solution. Merci encore! Une question: je me demande si c'est possible c'est un pire aléatoire comme ORDER BY RAND ()? Tout simplement parce que cette requête répète un ou plusieurs résultats plusieurs fois.
fabrik
1
@Adam: non, c'est intentionnel, pour que vous puissiez reproduire les résultats.
Quassnoi
12

Cela dépend de la façon dont vous devez être aléatoire. La solution que vous avez liée fonctionne plutôt bien IMO. Sauf si vous avez de grandes lacunes dans le champ ID, c'est toujours assez aléatoire.

Cependant, vous devriez pouvoir le faire en une seule requête en utilisant ceci (pour sélectionner une seule valeur):

SELECT [fields] FROM [table] WHERE id >= FLOOR(RAND()*MAX(id)) LIMIT 1

Autres solutions:

  • Ajoutez un champ flottant permanent appelé randomà la table et remplissez-le avec des nombres aléatoires. Vous pouvez ensuite générer un nombre aléatoire en PHP et faire"SELECT ... WHERE rnd > $random"
  • Saisissez la liste complète des ID et mettez-les en cache dans un fichier texte. Lisez le fichier et choisissez-y un ID aléatoire.
  • Mettez en cache les résultats de la requête au format HTML et conservez-les pendant quelques heures.
Chèvre mécontente
la source
8
Est-ce juste moi ou cette requête ne fonctionne pas? Je l'ai essayé avec plusieurs variantes et elles lancent toutes "Utilisation non valide de la fonction de groupe" ..
Sophivorus
Vous pouvez le faire avec une sous-requête, SELECT [fields] FROM [table] WHERE id >= FLOOR(RAND()*(SELECT MAX(id) FROM [table])) LIMIT 1mais cela ne semble pas fonctionner correctement car il ne renvoie jamais le dernier enregistrement
Mark
11
SELECT [fields] FROM [table] WHERE id >= FLOOR(1 + RAND()*(SELECT MAX(id) FROM [table])) LIMIT 1Semble faire l'affaire pour moi
Mark
1

Voici comment je le ferais:

SET @r := (SELECT ROUND(RAND() * (SELECT COUNT(*)
  FROM    accomodation a
  JOIN    accomodation_category c
    ON (a.ac_category = c.acat_id)
  WHERE   a.ac_status != 'draft'
        AND c.acat_slug != 'vendeglatohely'
        AND a.ac_images != 'b:0;';

SET @sql := CONCAT('
  SELECT  a.ac_id,
        a.ac_status,
        a.ac_name,
        a.ac_status,
        a.ac_images
  FROM    accomodation a
  JOIN    accomodation_category c
    ON (a.ac_category = c.acat_id)
  WHERE   a.ac_status != ''draft''
        AND c.acat_slug != ''vendeglatohely''
        AND a.ac_images != ''b:0;''
  LIMIT ', @r, ', 1');

PREPARE stmt1 FROM @sql;

EXECUTE stmt1;
Bill Karwin
la source
mon tableau n'est pas continu car il est souvent édité. par exemple, actuellement, le premier identifiant est 121.
fabrik
3
La technique ci-dessus ne repose pas sur le fait que les valeurs id sont continues. Il choisit un nombre aléatoire entre 1 et COUNT (*), pas 1 et MAX (id) comme certaines autres solutions.
Bill Karwin
1
Utiliser OFFSET(ce qui est à quoi @rsert) n'évite pas une analyse - jusqu'à une analyse complète de la table.
Rick James
@RickJames, c'est vrai. Si je devais répondre à cette question aujourd'hui, je ferais la requête par clé primaire. L'utilisation d'un décalage avec LIMIT scanne beaucoup de lignes. L'interrogation par clé primaire, bien que beaucoup plus rapide, ne garantit pas une chance égale de choisir chaque ligne - elle favorise les lignes qui suivent les espaces.
Bill Karwin
1

(Ouais, je serai grogné de ne pas avoir assez de viande ici, mais ne peux-tu pas être végétalien pendant un jour?)

Cas: AUTO_INCREMENT consécutif sans espaces, 1 ligne renvoyée
Cas: AUTO_INCREMENT consécutif sans espaces, 10 lignes
Case: AUTO_INCREMENT avec espaces, 1 ligne renvoyée
Case: Colonne FLOAT supplémentaire pour la randomisation
Cas: colonne UUID ou MD5

Ces 5 cas peuvent être rendus très efficaces pour les grandes tables. Voir mon blog pour les détails.

Rick James
la source
0

Cela vous donnera une seule sous-requête qui utilisera l'index pour obtenir un identifiant aléatoire, puis l'autre requête se déclenchera pour obtenir votre table jointe.

SELECT  accomodation.ac_id,
        accomodation.ac_status,
        accomodation.ac_name,
        accomodation.ac_status,
        accomodation.ac_images
FROM    accomodation, accomodation_category
WHERE   accomodation.ac_status != 'draft'
        AND accomodation.ac_category = accomodation_category.acat_id
        AND accomodation_category.acat_slug != 'vendeglatohely'
        AND ac_images != 'b:0;'
AND accomodation.ac_id IS IN (
        SELECT accomodation.ac_id FROM accomodation ORDER BY RAND() LIMIT 1
)
Karl Mikko
la source
0

La solution pour votre exemple factice serait:

SELECT  accomodation.ac_id,
        accomodation.ac_status,
        accomodation.ac_name,
        accomodation.ac_status,
        accomodation.ac_images
FROM    accomodation,
        JOIN 
            accomodation_category 
            ON accomodation.ac_category = accomodation_category.acat_id
        JOIN 
            ( 
               SELECT CEIL(RAND()*(SELECT MAX(ac_id) FROM accomodation)) AS ac_id
            ) AS Choices 
            USING (ac_id)
WHERE   accomodation.ac_id >= Choices.ac_id 
        AND accomodation.ac_status != 'draft'
        AND accomodation_category.acat_slug != 'vendeglatohely'
        AND ac_images != 'b:0;'
LIMIT 1

Pour en savoir plus sur les alternatives à ORDER BY RAND(), vous devriez lire cet article .

tereško
la source
0

J'optimise beaucoup de requêtes existantes dans mon projet. La solution de Quassnoi m'a beaucoup aidé à accélérer les requêtes! Cependant, j'ai du mal à incorporer ladite solution dans toutes les requêtes, en particulier pour les requêtes compliquées impliquant de nombreuses sous-requêtes sur plusieurs grandes tables.

J'utilise donc une solution moins optimisée. Fondamentalement, cela fonctionne de la même manière que la solution de Quassnoi.

SELECT  accomodation.ac_id,
        accomodation.ac_status,
        accomodation.ac_name,
        accomodation.ac_status,
        accomodation.ac_images
FROM    accomodation, accomodation_category
WHERE   accomodation.ac_status != 'draft'
        AND accomodation.ac_category = accomodation_category.acat_id
        AND accomodation_category.acat_slug != 'vendeglatohely'
        AND ac_images != 'b:0;'
        AND rand() <= $size * $factor / [accomodation_table_row_count]
LIMIT $size

$size * $factor / [accomodation_table_row_count]calcule la probabilité de choisir une ligne aléatoire. Le rand () générera un nombre aléatoire. La ligne sera sélectionnée si rand () est plus petit ou égal à la probabilité. Cela effectue effectivement une sélection aléatoire pour limiter la taille de la table. Puisqu'il y a une chance qu'il renvoie moins que le nombre limite défini, nous devons augmenter la probabilité pour nous assurer que nous sélectionnons suffisamment de lignes. Par conséquent, nous multiplions $ size par un $ factor (je fixe généralement $ factor = 2, fonctionne dans la plupart des cas). Enfin nous faisons lelimit $size

Le problème est maintenant de travailler sur l' accomodation_table_row_count . Si nous connaissons la taille de la table, nous pourrions coder en dur la taille de la table. Ce serait le plus rapide, mais ce n'est évidemment pas l'idéal. Si vous utilisez Myisam, obtenir le nombre de tables est très efficace. Depuis que j'utilise innodb, je ne fais qu'un simple comptage + sélection. Dans votre cas, cela ressemblerait à ceci:

SELECT  accomodation.ac_id,
        accomodation.ac_status,
        accomodation.ac_name,
        accomodation.ac_status,
        accomodation.ac_images
FROM    accomodation, accomodation_category
WHERE   accomodation.ac_status != 'draft'
        AND accomodation.ac_category = accomodation_category.acat_id
        AND accomodation_category.acat_slug != 'vendeglatohely'
        AND ac_images != 'b:0;'
        AND rand() <= $size * $factor / (select (SELECT count(*) FROM `accomodation`) * (SELECT count(*) FROM `accomodation_category`))
LIMIT $size

La partie la plus délicate consiste à déterminer la bonne probabilité. Comme vous pouvez le voir, le code suivant ne calcule en fait que la taille approximative de la table temporaire (en fait, trop approximative!): (select (SELECT count(*) FROM accomodation) * (SELECT count(*) FROM accomodation_category))Mais vous pouvez affiner cette logique pour donner une approximation plus proche de la taille de la table. Notez qu'il vaut mieux sur-sélectionner que sous-sélectionner des lignes. c'est-à-dire que si la probabilité est trop faible, vous risquez de ne pas sélectionner suffisamment de lignes.

Cette solution fonctionne plus lentement que la solution de Quassnoi car nous devons recalculer la taille de la table. Cependant, je trouve ce codage beaucoup plus gérable. Il s'agit d'un compromis entre précision + performances et complexité de codage . Cela dit, sur les grandes tables, c'est encore beaucoup plus rapide que Order by Rand ().

Remarque: Si la logique de requête le permet, effectuez la sélection aléatoire le plus tôt possible avant toute opération de jointure.

lawrenceshen
la source
-1
function getRandomRow(){
    $id = rand(0,NUM_OF_ROWS_OR_CLOSE_TO_IT);
    $res = getRowById($id);
    if(!empty($res))
    return $res;
    return getRandomRow();
}

//rowid is a key on table
function getRowById($rowid=false){

   return db select from table where rowid = $rowid; 
}
Rokhayakebe
la source