Як я можу оптимізувати функцію ORDER BY RAND () MySQL?)


90

Я хотів би оптимізувати свої запити, щоб заглянути mysql-slow.log.

Більшість моїх повільних запитів містить ORDER BY RAND(). Я не можу знайти реального рішення для вирішення цієї проблеми. Є можливе рішення на 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

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

@fabrik: Спробуємо зараз. Було б дуже корисно, якщо б ви опублікували сценарії таблиці, щоб я міг перевірити їх перед публікацією.
Quassnoi

Дякую, це працює! :) Чи можете ви відредагувати частину JOIN ... ON aco.id на JOIN ... ON aco.ac_id, щоб я міг прийняти ваше рішення. Знову дякую! Питання: цікаво, якщо це можливо, це гірший випадковий випадок, як ЗАМОВИТИ НА РЕНД ()? Просто тому, що цей запит багато разів повторює деякі результати.
fabrik

1
@Adam: ні, це навмисно, щоб ви могли відтворити результати.
Quassnoi

12

Це залежить від того, наскільки випадковим вам потрібно бути. Рішення, яке ви зв’язали, працює досить добре в IMO. Якщо у вас немає великих прогалин у полі ідентифікатора, це все одно досить випадково.

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

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

Інші рішення:

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

8
Це лише я, або цей запит не працює? Я спробував це з декількома варіаціями, і всі вони кидають "Недійсне використання групової функції" ..
Софіворус

Ви можете зробити це за допомогою підзапиту, 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, це правильно. Якби я відповів на це питання сьогодні, я б зробив запит за первинним ключем. Використання зсуву з обмеженням сканує багато рядків. Запит за первинним ключем, хоча і набагато швидший, не гарантує рівного шансу вибрати кожен рядок - він надає перевагу рядкам, які слідують за пробілами.
Білл Карвін,

1

(Так, я потерплюся за те, що тут не маю достатньо м’яса, але чи не можете ви бути веганом протягом одного дня?)

Справа: послідовний AUTO_INCREMENT без пропусків, повернутий 1 рядок
Справа: послідовний AUTO_INCREMENT без пропусків, 10 рядків
Справа: AUTO_INCREMENT з пробілами, повернутий 1 рядок
Справа: Додатковий стовпець FLOAT для рандомізації
Case: 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 допомогло мені значно пришвидшити запити! Однак мені важко включити згадане рішення у всі запити, особливо для складних запитів, що включають багато підзапитів у декількох великих таблицях.

Тому я використовую менш оптимізоване рішення. По суті, це працює так само, як рішення Квасноя.

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

Зараз проблема полягає в розробці accomodation_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))Але ви можете уточнити цю логіку, щоб наблизити приблизне розмір таблиці. Зверніть увагу, що краще НАД-вибрати, ніж недобирати рядки. тобто якщо ймовірність встановлена ​​занадто низькою, ви ризикуєте не виділити достатню кількість рядків.

Це рішення працює повільніше, ніж рішення Квасноя, оскільки нам потрібно перерахувати розмір таблиці. Однак я вважаю це кодування набагато більш керованим. Це компроміс між точністю + продуктивністю та складністю кодування . Сказавши це, на великих столах це все ще набагато швидше, ніж 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.