Wie kann ich die ORDER BY RAND () - Funktion von MySQL optimieren?


90

Ich möchte meine Abfragen optimieren, damit ich sie mir ansehen kann mysql-slow.log.

Die meisten meiner langsamen Abfragen enthalten ORDER BY RAND(). Ich kann keine echte Lösung finden, um dieses Problem zu lösen. Theres ist eine mögliche Lösung bei MySQLPerformanceBlog, aber ich denke nicht, dass dies genug ist. Bei schlecht optimierten (oder häufig aktualisierten, vom Benutzer verwalteten) Tabellen funktioniert dies nicht oder ich muss zwei oder mehr Abfragen PHPausführen, bevor ich meine generierte Zufallszeile auswählen kann .

Gibt es eine Lösung für dieses Problem?

Ein Dummy-Beispiel:

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

Antworten:


67

Versuche dies:

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

Dies ist besonders effizient MyISAM(da dies COUNT(*)sofort geschieht), aber selbst in Zeiten, in denen InnoDBes 10effizienter ist als ORDER BY RAND().

Die Hauptidee hier ist, dass wir nicht sortieren, sondern zwei Variablen behalten und die running probabilityeiner Zeile berechnen, die im aktuellen Schritt ausgewählt werden soll.

Weitere Informationen finden Sie in diesem Artikel in meinem Blog:

Aktualisieren:

Wenn Sie nur einen einzelnen zufälligen Datensatz auswählen müssen, versuchen Sie Folgendes:

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
        )
        )

Dies setzt voraus, dass Ihre ac_idmehr oder weniger gleichmäßig verteilt sind.


Hallo Quassnoi! Zunächst einmal vielen Dank für Ihre schnelle Antwort! Vielleicht ist es meine Schuld, aber Ihre Lösung ist immer noch unklar. Ich werde meinen ursprünglichen Beitrag mit einem konkreten Beispiel aktualisieren und freue mich, wenn Sie Ihre Lösung in diesem Beispiel erläutern.
Fabrik

Es gab einen Tippfehler bei "JOIN Accomodation Aco ON Aco.id =", wo Aco.id wirklich Aco.ac_id ist. Andererseits hat die korrigierte Abfrage bei mir nicht funktioniert, da sie einen Fehler # 1241 auslöst. Der Operand sollte beim fünften SELECT (der vierten Unterauswahl) 1 Spalte (n) enthalten. Ich habe versucht, das Problem mit Klammern zu finden (wenn ich mich nicht irre), aber ich kann das Problem noch nicht finden.
Fabrik

@fabrik: Versuche es jetzt. Es wäre sehr hilfreich, wenn Sie die Tabellenskripte veröffentlichen würden, damit ich sie vor dem Posten überprüfen könnte.
Quassnoi

Danke, es funktioniert! :) Können Sie den Teil JOIN ... ON aco.id in JOIN ... ON aco.ac_id bearbeiten, damit ich Ihre Lösung akzeptieren kann? Danke noch einmal! Eine Frage: Ich frage mich, ob dies ein schlechterer Zufall wie ORDER BY RAND () ist. Nur weil diese Abfrage einige Ergebnisse oft wiederholt.
Fabrik

1
@Adam: Nein, das ist beabsichtigt, damit du die Ergebnisse reproduzieren kannst.
Quassnoi

12

Es hängt davon ab, wie zufällig Sie sein müssen. Die von Ihnen verknüpfte Lösung funktioniert IMO ziemlich gut. Sofern Sie keine großen Lücken im ID-Feld haben, ist es immer noch ziemlich zufällig.

Sie sollten dies jedoch in einer Abfrage tun können (um einen einzelnen Wert auszuwählen):

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

Andere Lösungen:

  • Fügen Sie randomder Tabelle ein permanentes Float-Feld hinzu und füllen Sie es mit Zufallszahlen. Sie können dann eine Zufallszahl in PHP generieren und tun"SELECT ... WHERE rnd > $random"
  • Holen Sie sich die gesamte Liste der IDs und speichern Sie sie in einer Textdatei. Lesen Sie die Datei und wählen Sie eine zufällige ID aus.
  • Zwischenspeichern Sie die Ergebnisse der Abfrage als HTML und bewahren Sie sie einige Stunden lang auf.

8
Ist es nur ich oder funktioniert diese Abfrage nicht? Ich habe es mit verschiedenen Variationen versucht und alle werfen "Ungültige Verwendung der Gruppenfunktion".
Sophivorus

Sie können es mit einer Unterabfrage tun, SELECT [fields] FROM [table] WHERE id >= FLOOR(RAND()*(SELECT MAX(id) FROM [table])) LIMIT 1aber dies scheint nicht richtig zu funktionieren, da es nie den letzten Datensatz zurückgibt
Mark

11
SELECT [fields] FROM [table] WHERE id >= FLOOR(1 + RAND()*(SELECT MAX(id) FROM [table])) LIMIT 1Scheint den Trick für mich zu tun
Mark

1

So würde ich es machen:

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;


Meine Tabelle ist nicht fortlaufend, da sie häufig bearbeitet wird. Zum Beispiel ist derzeit die erste ID 121.
Fabrik

3
Die obige Technik beruht nicht darauf, dass die ID-Werte kontinuierlich sind. Es wählt eine Zufallszahl zwischen 1 und COUNT (*), nicht 1 und MAX (id) wie bei einigen anderen Lösungen.
Bill Karwin

1
Durch OFFSETdie Verwendung (wofür @r) wird ein Scan nicht vermieden - bis zu einem vollständigen Tabellenscan.
Rick James

@ RickJames, das stimmt. Wenn ich diese Frage heute beantworten würde, würde ich die Abfrage nach Primärschlüssel durchführen. Die Verwendung eines Versatzes mit LIMIT scannt viele Zeilen. Das Abfragen nach dem Primärschlüssel ist zwar viel schneller, garantiert jedoch keine gleichmäßige Chance, jede Zeile auszuwählen. Es werden Zeilen bevorzugt, die Lücken folgen.
Bill Karwin

1

(Ja, ich werde mich ärgern, weil ich hier nicht genug Fleisch habe, aber kannst du nicht einen Tag lang Veganer sein?)

Fall: Aufeinanderfolgendes AUTO_INCREMENT ohne Lücken, 1 Zeile zurückgegeben
Fall: Aufeinanderfolgendes AUTO_INCREMENT ohne Lücken, 10 Zeilen
Fall: AUTO_INCREMENT mit Lücken, 1 Zeile zurückgegeben
Fall: Zusätzliche FLOAT-Spalte zum Randomisieren
Fall: UUID- oder MD5-Spalte

Diese 5 Fälle können für große Tische sehr effizient gemacht werden. Siehe meinen Blog für die Details.


0

Dadurch erhalten Sie eine einzelne Unterabfrage, die den Index verwendet, um eine zufällige ID abzurufen. Bei der anderen Abfrage wird die verknüpfte Tabelle abgerufen.

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

Die Lösung für Ihr Dummy-Beispiel wäre:

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

Um mehr über Alternativen zu zu ORDER BY RAND()erfahren, sollten Sie diesen Artikel lesen .


0

Ich optimiere viele vorhandene Abfragen in meinem Projekt. Die Lösung von Quassnoi hat mir geholfen, die Abfragen sehr zu beschleunigen! Es fällt mir jedoch schwer, diese Lösung in alle Abfragen zu integrieren, insbesondere bei komplizierten Abfragen, an denen viele Unterabfragen in mehreren großen Tabellen beteiligt sind.

Ich verwende also eine weniger optimierte Lösung. Grundsätzlich funktioniert es genauso wie die Lösung von 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]berechnet die Wahrscheinlichkeit, eine zufällige Zeile auszuwählen. Der Rand () generiert eine Zufallszahl. Die Zeile wird ausgewählt, wenn rand () kleiner ist oder der Wahrscheinlichkeit entspricht. Dies führt effektiv eine zufällige Auswahl durch, um die Tabellengröße zu begrenzen. Da die Wahrscheinlichkeit besteht, dass weniger als der definierte Grenzwert zurückgegeben wird, müssen wir die Wahrscheinlichkeit erhöhen, um sicherzustellen, dass wir genügend Zeilen auswählen. Daher multiplizieren wir $ size mit einem $ -Faktor (ich setze normalerweise $ factor = 2, funktioniert in den meisten Fällen). Endlich machen wir daslimit $size

Das Problem besteht nun darin, den accomodation_table_row_count zu ermitteln . Wenn wir die Tabellengröße kennen, KÖNNEN wir die Tabellengröße hart codieren. Dies würde am schnellsten laufen, aber offensichtlich ist dies nicht ideal. Wenn Sie Myisam verwenden, ist das Abrufen der Tabellenanzahl sehr effizient. Da ich innodb benutze, mache ich nur eine einfache Zählung + Auswahl. In Ihrem Fall würde es so aussehen:

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

Der schwierige Teil besteht darin, die richtige Wahrscheinlichkeit zu ermitteln. Wie Sie sehen können, berechnet der folgende Code tatsächlich nur die grobe temporäre Tabellengröße (tatsächlich zu grob!): (select (SELECT count(*) FROM accomodation) * (SELECT count(*) FROM accomodation_category))Sie können diese Logik jedoch verfeinern, um eine genauere Annäherung an die Tabellengröße zu erhalten. Beachten Sie, dass es besser ist, Zeilen über- als unterzuwählen. Wenn die Wahrscheinlichkeit zu niedrig eingestellt ist, besteht die Gefahr, dass nicht genügend Zeilen ausgewählt werden.

Diese Lösung läuft langsamer als die Lösung von Quassnoi, da die Tabellengröße neu berechnet werden muss. Ich finde diese Codierung jedoch viel einfacher zu handhaben. Dies ist ein Kompromiss zwischen Genauigkeit + Leistung und Codierungskomplexität . Auf großen Tischen ist dies jedoch immer noch weitaus schneller als bei Order by Rand ().

Hinweis: Wenn die Abfragelogik dies zulässt, führen Sie die zufällige Auswahl so früh wie möglich durch, bevor Verknüpfungsvorgänge ausgeführt werden.


-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; 
}
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.