web-dev-qa-db-fra.com

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

Je voudrais optimiser mes requêtes afin que je regarde dans mysql-slow.log.

La plupart de mes requêtes lentes contiennent ORDER BY Rand(). Je ne trouve pas de solution réelle pour résoudre ce problème. Il y a une solution possible à 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 ligne aléatoire PHP-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
87
fabrik

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 COUNT(*) est instantané), mais même dans InnoDB, il est 10 fois plus efficace que ORDER BY Rand().

L'idée principale ici est que nous ne trions pas, mais conservons deux variables et calculons le running probability d'une ligne à sélectionner sur l'étape actuelle.

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

Mettre à jour:

Si vous devez sélectionner 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 votre ac_id est distribué de manière plus ou moins égale.

64
Quassnoi

Cela dépend de la façon dont vous devez être aléatoire. La solution que vous avez associée fonctionne assez bien à l’OMI. À moins que vous n'ayez de grandes lacunes dans le champ ID, cela reste assez aléatoire.

Cependant, vous devriez pouvoir le faire dans une 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 permanent appelé random à la table et remplissez-le avec des nombres aléatoires. Vous pouvez ensuite générer un nombre aléatoire dans PHP et faire "SELECT ... WHERE rnd > $random"
  • Prenez la liste complète des identifiants et mettez-les en cache dans un fichier texte. Lisez le fichier et choisissez un identifiant aléatoire.
  • Mettez en cache les résultats de la requête au format HTML et conservez-les pendant quelques heures.
13
DisgruntledGoat

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;
1
Bill Karwin

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
)
0
Karl Mikko

(Oui, je vais me faire reprocher de ne pas avoir assez de viande ici, mais ne pouvez-vous pas être végétalien pendant une journée?)

Cas: AUTO_INCREMENT consécutif sans espaces, 1 ligne retournée
Affaire: AUTO_INCREMENT consécutif sans espaces, 10 lignes
Cas: AUTO_INCREMENT avec des espaces, 1 ligne retournée
Cas: colonne Extra FLOAT 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.

0
Rick James

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 .

0
tereško

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, il m'est difficile d'intégrer ladite solution à toutes les requêtes, en particulier pour les requêtes complexes impliquant de nombreuses sous-requêtes sur plusieurs tables volumineuses.

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 () va générer un nombre aléatoire. La ligne sera sélectionnée si Rand () est plus petit ou égal à la probabilité. Ceci effectue efficacement une sélection aléatoire pour limiter la taille de la table. Comme il y a une chance qu'il retourne moins que le nombre limite défini, nous devons augmenter la probabilité pour nous assurer de sélectionner suffisamment de lignes. Par conséquent, nous multiplions $ taille par un facteur $ (je règle généralement $ facteur = 2, cela fonctionne dans la plupart des cas). Enfin nous faisons le limit $size

Le problème est en train de résoudre le 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 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 constater, le code suivant calcule uniquement la taille approximative de la table de temp (En fait, trop approximative!): (select (SELECT count(*) FROM accomodation) * (SELECT count(*) FROM accomodation_category)) Mais vous pouvez affiner cette logique pour obtenir une approximation plus proche de la taille de la table. Notez qu'il est préférable de sur-sélectionner plutôt que de sous-sélectionner les lignes. Si la probabilité est trop basse, 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 facile à gérer. C'est un compromis entre précision + performance vs complexité de codage . Cela dit, sur les grandes tables, cela reste de loin 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.

0
lawrenceshen