Как я могу оптимизировать функцию ORDER BY RAND () MySQL? - PullRequest
88 голосов
/ 07 августа 2009

Я бы хотел оптимизировать свои запросы, чтобы я посмотрел на mysql-slow.log.

Большинство моих медленных запросов содержит ORDER BY RAND(). Я не могу найти реальное решение для решения этой проблемы. Theres - возможное решение в MySQLPerformanceBlog , но я не думаю, что этого достаточно. В плохо оптимизированных (или часто обновляемых, управляемых пользователем) таблицах это не работает, или мне нужно выполнить два или более запросов, прежде чем я смогу выбрать свою PHP генерируемую случайную строку.

Есть ли решение этой проблемы?

Дурацкий пример:

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

Ответы [ 8 ]

67 голосов
/ 07 августа 2009

Попробуйте это:

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

Это особенно эффективно на MyISAM (поскольку COUNT(*) мгновенно), но даже в InnoDB оно в 10 раз эффективнее, чем ORDER BY RAND().

Основная идея здесь заключается в том, что мы не сортируем, а вместо этого сохраняем две переменные и вычисляем running probability строки, которая будет выбрана на текущем шаге.

См. Эту статью в моем блоге для более подробной информации:

Обновление:

Если вам нужно выбрать только одну случайную запись, попробуйте это:

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

Предполагается, что ваши ac_id распределены более или менее равномерно.

13 голосов
/ 09 августа 2009

Это зависит от того, насколько случайным вам нужно быть. Решение, которое вы связали, работает очень хорошо IMO. Если у вас нет больших пробелов в поле идентификатора, это все еще довольно случайно.

Однако вы должны быть в состоянии сделать это в одном запросе, используя это (для выбора одного значения):

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

Другие решения:

  • Добавьте постоянное поле с плавающей точкой с именем random в таблицу и заполните его случайными числами. Затем вы можете сгенерировать случайное число в PHP и сделать "SELECT ... WHERE rnd > $random"
  • Соберите весь список идентификаторов и поместите их в текстовый файл. Прочитайте файл и выберите случайный идентификатор из него.
  • Кэшируйте результаты запроса в виде HTML и сохраняйте его в течение нескольких часов.
1 голос
/ 08 августа 2009

Вот как бы я это сделал:

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;
0 голосов
/ 06 ноября 2017
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; 
}
0 голосов
/ 01 февраля 2016

(Да, я буду страдать от недостатка мяса здесь, но разве вы не можете быть веганом на один день?)

Случай: последовательный AUTO_INCREMENT без пробелов, 1 строка возвращена
Случай: последовательный AUTO_INCREMENT без пробелов, 10 строк
Случай: AUTO_INCREMENT с пробелами, возвращается 1 строка
Случай: дополнительный столбец FLOAT для рандомизации
Случай: UUID или столбец MD5

Эти 5 случаев могут быть очень эффективными для больших столов. Подробнее см. мой блог .

0 голосов
/ 17 сентября 2014

Я оптимизирую множество существующих запросов в моем проекте. Решение 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] определяет вероятность выбора случайной строки. Rand () сгенерирует случайное число. Строка будет выбрана, если rand () меньше или равна вероятности. Это эффективно выполняет случайный выбор, чтобы ограничить размер таблицы. Так как есть вероятность, что он вернет меньше заданного предельного числа, нам нужно увеличить вероятность, чтобы убедиться, что мы выбираем достаточно строк. Следовательно, мы умножаем размер $ на $ фактор (обычно я устанавливаю $ factor = 2, в большинстве случаев работает). Наконец мы делаем limit $size

Проблема сейчас в том, чтобы решить гостиничный_стол_в__счету . Если мы знаем размер таблицы, мы можем жестко закодировать размер таблицы. Это будет работать быстрее всего, но, очевидно, это не идеально. Если вы используете Myisam, получение количества таблиц очень эффективно. Поскольку я использую innodb, я просто делаю простой подсчет + выбор. В вашем случае это будет выглядеть так:

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

Сложная задача - определить правильную вероятность. Как вы можете видеть, следующий код фактически рассчитывает только приблизительный размер временной таблицы (на самом деле слишком грубый!): (select (SELECT count(*) FROM accomodation) * (SELECT count(*) FROM accomodation_category)) Но вы можете уточнить эту логику, чтобы приблизить размер таблицы. Обратите внимание, что лучше OVER-select, чем under-select строк. то есть, если вероятность установлена ​​слишком низкой, вы рискуете не выбрать достаточно строк.

Это решение работает медленнее, чем решение Quassnoi, так как нам нужно пересчитать размер таблицы. Тем не менее, я считаю, что это кодирование гораздо более управляемым. Это компромисс между точность + производительность против сложность кодирования . Тем не менее, на больших столах это все еще намного быстрее, чем Order by Rand ().

Примечание. Если логика запроса позволяет, выполнить произвольный выбор как можно раньше, прежде чем выполнять какие-либо операции соединения.

0 голосов
/ 20 марта 2012

Решение для вашего фиктивного примера:

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

Чтобы узнать больше об альтернативах ORDER BY RAND(), прочитайте эту статью .

0 голосов
/ 12 октября 2011

Это даст вам один подзапрос, который будет использовать индекс для получения случайного идентификатора, тогда как другой запрос вызовет получение вашей объединенной таблицы.

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