Как я могу оптимизировать MySQL функцию ORDER BY RAND ()?


90

Я хотел бы оптимизировать свои запросы, чтобы я изучил 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

Ответы:


67

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

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файлы распределены более или менее равномерно.


Привет, Квассной! Прежде всего, спасибо за быстрый ответ! Может это моя вина, но твое решение до сих пор непонятно. Я обновлю свой исходный пост конкретным примером, и буду рад, если вы объясните свое решение на этом примере.
fabrik 07

была опечатка в "ПРИСОЕДИНЯЙТЕСЬ к размещению aco ON aco.id =", где aco.id на самом деле является aco.ac_id. с другой стороны, исправленный запрос не сработал для меня, потому что он выдает ошибку # 1241 - операнд должен содержать 1 столбец (столбцы) в пятом SELECT (четвертом подвыборе). Я попытался найти проблему со скобками (если я не ошибаюсь), но пока не могу найти проблему.
fabrik

@fabrik: попробуй сейчас. Было бы очень полезно, если бы вы разместили скрипты таблиц, чтобы я мог проверить их перед публикацией.
Quassnoi

Спасибо, это работает! :) Можете ли вы отредактировать часть JOIN ... ON aco.id для JOIN ... ON aco.ac_id, чтобы я мог принять ваше решение. Еще раз спасибо! Вопрос: интересно, возможно ли это худшее случайное число, такое как ORDER BY RAND ()? Просто потому, что этот запрос многократно повторяет некоторые результаты.
fabrik

1
@ Адам: нет, это сделано намеренно, чтобы вы могли воспроизвести результаты.
Quassnoi 05

12

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

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

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

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

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

8
Это только у меня или этот запрос не работает? Я пробовал это с несколькими вариантами, и все они бросают "Недопустимое использование групповой функции" ..
Sophivorus

Вы можете сделать это с помощью подзапроса, SELECT [fields] FROM [table] WHERE id >= FLOOR(RAND()*(SELECT MAX(id) FROM [table])) LIMIT 1но, похоже, это не работает должным образом, поскольку он никогда не возвращает последнюю запись
Марк

11
SELECT [fields] FROM [table] WHERE id >= FLOOR(1 + RAND()*(SELECT MAX(id) FROM [table])) LIMIT 1Кажется, это помогает мне
Марк

1

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

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;


моя таблица не непрерывна, потому что она часто редактируется. например, в настоящее время первый идентификатор - 121.
fabrik

3
Приведенный выше метод не полагается на непрерывность значений id. Он выбирает случайное число от 1 до COUNT (*), а не от 1 до MAX (id), как некоторые другие решения.
Билл Карвин,

1
Использование OFFSET(то @rесть для чего) не предотвращает сканирования - вплоть до полного сканирования таблицы.
Рик Джеймс

@RickJames, верно. Если бы я сегодня ответил на этот вопрос, я бы сделал запрос по первичному ключу. Использование смещения с LIMIT действительно сканирует много строк. Запрос по первичному ключу, хотя и намного быстрее, не гарантирует равных возможностей выбора каждой строки - он предпочитает строки, следующие за пробелами.
Билл Карвин,

1

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

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

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


0

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

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

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

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

Я оптимизирую множество существующих запросов в своем проекте. Решение Quassnoi помогло мне значительно ускорить запросы! Однако мне сложно включить указанное решение во все запросы, особенно для сложных запросов, включающих множество подзапросов в нескольких больших таблицах.

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

Теперь проблема заключается в том, чтобы разобраться с encodation_table_row_count . Если мы знаем размер таблицы, мы МОЖЕМ жестко закодировать размер таблицы. Это будет работать быстрее всего, но, очевидно, это не идеально. Если вы используете 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))Но вы можете уточнить эту логику, чтобы дать более точное приближение к размеру таблицы. Обратите внимание, что лучше выбрать ПЕРЕБРАТЬ, чем выделить строки ниже. то есть, если вероятность слишком низкая, вы рискуете не выбрать достаточно строк.

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

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


-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; 
}
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.