Как быстро выбрать 3 случайные записи из таблицы 30k MySQL с фильтром где-то одним запросом?

Ну, это очень старый вопрос, который никогда не получал реального решения. Мы хотим, чтобы 3 случайные строки из таблицы содержали около 30 тыс. Записей. Таблица не такая большая, с точки зрения MySQL, но если она представляет продукты магазина, она является представительной. Случайный выбор полезен, когда вы представляете 3 случайных продукта на веб-странице, например. Мы хотели бы, чтобы одно стандартное SQL-решение отвечало следующим условиям:

  1. В PHP набор записей с помощью PDO или MySQLi должен иметь ровно 3 строки.
  2. Они должны быть получены одним запросом MySQL без использования хранимой процедуры.
  3. Решение должно быть быстрым, например, занятым сервером apache2, MySQL-запрос во многих ситуациях является узким местом. Поэтому он должен избегать создания временных таблиц и т. Д.
  4. 3 записи должны быть не смежными, т. Е. Они не должны находиться рядом друг с другом.

Таблица имеет следующие поля:

CREATE TABLE Products ( ID INT(8) NOT NULL AUTO_INCREMENT, Name VARCHAR(255) default NULL, HasImages INT default 0, ... ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 

Ограничение WHERE – это Products.HasImages = 1, позволяющее извлекать только записи, на которых есть изображения, доступные для показа на веб-странице. Примерно одна треть записей соответствует условию HasImages = 1.

В поисках совершенства мы сначала отбросили существующие решения, у которых есть недостатки:


I. Это основное решение, использующее ORDER BY RAND (),

слишком медленный, но гарантирует 3 действительно случайных записи в каждом запросе:

 SELECT ID, Name FROM Products WHERE HasImages=1 ORDER BY RAND() LIMIT 3; 

* CPU около 0,10 с, сканирование 9690 строк из-за предложения WHERE, использование где; Использование временных; Использование filesort , на Debian Squeeze Double-Core Linux, не так плохо, но

не настолько масштабируемым для большей таблицы, поскольку временная таблица и filesort используются, и берут меня 8.52 для первого запроса в тестовой системе Windows7 :: MySQL. С такой плохой производительностью, чтобы избежать веб-страницы, это не так?


II. Яркое решение riedsio с использованием JOIN … RAND (),

из MySQL выберете 10 случайных строк из 600K строк быстро , адаптировано здесь только для одной случайной записи, так как следующий запрос приводит к почти всегда непрерывным записям. Фактически он получает только случайный набор из 3 непрерывных записей в идентификаторах:

 SELECT Products.ID, Products.Name FROM Products INNER JOIN (SELECT (RAND() * (SELECT MAX(ID) FROM Products)) AS ID) AS t ON Products.ID >= t.ID WHERE (Products.HasImages=1) ORDER BY Products.ID ASC LIMIT 3; 

* CPU около 0,01 – 0,19 с, сканирование 3200, 9690, 12000 строк или около того случайным образом, но в основном 9690 записей, с использованием где.


III. Лучшее решение выглядит следующим образом: WHERE … RAND (),

замеченный в MySQL, выберите 10 случайных строк из 600K строк, предложенных bernardo-siu :

 SELECT Products.ID, Products.Name FROM Products WHERE ((Products.Hasimages=1) AND RAND() < 16 * 3/30000) LIMIT 3; 

* ЦП около 0,01 – 0,03 с, сканирование 9690 строк, Использование где.

Здесь 3 – количество желаемых строк, 30000 – RecordCount таблицы Products, 16 – экспериментальный коэффициент, чтобы увеличить выбор, чтобы гарантировать выбор трех записей. Я не знаю, на каком основании коэффициент 16 является приемлемым приближением.

В большинстве случаев мы получаем 3 случайные записи, и это очень быстро, но это не оправдано: иногда запрос возвращает только 2 строки, а иногда даже вообще никакой записи.

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

Лучшая строка SQL?

Уродливые, но быстрые и случайные. Может стать очень уродливым очень быстро, особенно с настройкой, описанной ниже, поэтому убедитесь, что вы действительно этого хотите.

 (SELECT Products.ID, Products.Name FROM Products INNER JOIN (SELECT RAND()*(SELECT MAX(ID) FROM Products) AS ID) AS t ON Products.ID >= t.ID WHERE Products.HasImages=1 ORDER BY Products.ID LIMIT 1) UNION ALL (SELECT Products.ID, Products.Name FROM Products INNER JOIN (SELECT RAND()*(SELECT MAX(ID) FROM Products) AS ID) AS t ON Products.ID >= t.ID WHERE Products.HasImages=1 ORDER BY Products.ID LIMIT 1) UNION ALL (SELECT Products.ID, Products.Name FROM Products INNER JOIN (SELECT RAND()*(SELECT MAX(ID) FROM Products) AS ID) AS t ON Products.ID >= t.ID WHERE Products.HasImages=1 ORDER BY Products.ID LIMIT 1) 

Первая строка появляется чаще, чем должна

Если у вас большие пробелы между идентификаторами в таблице, строки сразу после таких пробелов будут иметь больше шансов получить этот запрос. В некоторых случаях они появятся значительно чаще, чем должны. Это не может быть решено в целом, но есть исправление для общего частного случая: когда между 0 и первым существующим ID в таблице существует разрыв.

Вместо подзапроса (SELECT RAND()*<max_id> AS ID) используйте что-то вроде (SELECT <min_id> + RAND()*(<max_id> - <min_id>) AS ID)

Удалить дубликаты

Запрос, если он используется как есть, может возвращать повторяющиеся строки. Этого можно избежать, используя UNION вместо UNION ALL . Таким образом, дубликаты будут объединены, но запрос больше не гарантирует возврата ровно 3 строки. Вы можете обойти это тоже, извлекая больше строк, чем вам нужно, и ограничивая внешний результат следующим образом:

 (SELECT ... LIMIT 1) UNION (SELECT ... LIMIT 1) UNION (SELECT ... LIMIT 1) ... UNION (SELECT ... LIMIT 1) LIMIT 3 

Тем не менее, по-прежнему нет никакой гарантии, что 3 строки будут извлечены. Это просто делает его более вероятным.

 SELECT Products.ID, Products.Name FROM Products INNER JOIN (SELECT (RAND() * (SELECT MAX(ID) FROM Products)) AS ID) AS t ON Products.ID >= t.ID WHERE (Products.HasImages=1) ORDER BY Products.ID ASC LIMIT 3; 

Разумеется, вышеприведенные «близкие» непрерывные записи вы каждый раз кормите его одним и тем же ID не обращая особого внимания на seed функции rand .

Это должно дать больше «случайности»,

 SELECT Products.ID, Products.Name FROM Products INNER JOIN (SELECT (ROUND((RAND() * (max-min))+min)) AS ID) AS t ON Products.ID >= t.ID WHERE (Products.HasImages=1) ORDER BY Products.ID ASC LIMIT 3; 

Где max и min – два значения, которые вы выберете, скажем, например, сакэ:

 max = select max(id) min = 225 

Этот оператор выполняется очень быстро (19 мс в таблице 30 тыс. Записей):

 $db = new PDO('mysql:host=localhost;dbname=database;charset=utf8', 'username', 'password'); $stmt = $db->query("SELECT p.ID, p.Name, p.HasImages FROM (SELECT @count := COUNT(*) + 1, @limit := 3 FROM Products WHERE HasImages = 1) vars STRAIGHT_JOIN (SELECT t.*, @limit := @limit - 1 FROM Products t WHERE t.HasImages = 1 AND (@count := @count -1) AND RAND() < @limit / @count) p"); $products = $stmt->fetchAll(PDO::FETCH_ASSOC); 

Идея состоит в том, чтобы «ввести» новый столбец со случайными значениями, а затем отсортировать по этому столбцу. Генерация и сортировка по этому введенному столбцу выполняется быстрее, чем команда «ORDER BY RAND ()».

Там может быть одно предостережение: вы должны дважды включить запрос WHERE.

Как создать другую таблицу, содержащую только элементы с изображением? Эта таблица будет намного легче, так как она будет содержать только одну треть предметов, которые есть в оригинальной таблице!

 ------------------------------------------ |ID | Item ID (on the original table)| ------------------------------------------ |0 | 0 | ------------------------------------------ |1 | 123 | ------------------------------------------ . . . ------------------------------------------ |10 000 | 30 000 | ------------------------------------------ 

Затем вы можете сгенерировать три случайных идентификатора в части кода PHP и просто получить из базы данных.

Я тестировал следующую группу SQL-запросов в 10-мегапиксельной, плохо разработанной базе данных.

 SELECT COUNT(ID) INTO @count FROM Products WHERE HasImages = 1; PREPARE random_records FROM '( SELECT * FROM Products WHERE HasImages = 1 LIMIT ?, 1 ) UNION ( SELECT * FROM Products WHERE HasImages = 1 LIMIT ?, 1 ) UNION ( SELECT * FROM Products WHERE HasImages = 1 LIMIT ?, 1 )'; SET @l1 = ROUND(RAND() * @count); SET @l2 = ROUND(RAND() * @count); SET @l3 = ROUND(RAND() * @count); EXECUTE random_records USING @l1 , @l2 , @l3; DEALLOCATE PREPARE random_records; 

Чтобы получить три результата, потребовалось почти 7 минут. Но я уверен, что его производительность будет намного лучше в вашем случае. Тем не менее, если вы ищете лучшую производительность, я предлагаю следующие, так как мне потребовалось менее 30 секунд для выполнения этой работы (в одной базе данных).

 SELECT COUNT(ID) INTO @count FROM Products WHERE HasImages = 1; PREPARE random_records FROM 'SELECT * FROM Products WHERE HasImages = 1 LIMIT ?, 1'; SET @l1 = ROUND(RAND() * @count); SET @l2 = ROUND(RAND() * @count); SET @l3 = ROUND(RAND() * @count); EXECUTE random_records USING @l1; EXECUTE random_records USING @l2; EXECUTE random_records USING @l3; DEALLOCATE PREPARE random_records; 

Имейте в виду, что обе эти команды требуют MySQLi-драйвера в PHP, если вы хотите выполнить их за один раз. И их единственное отличие состоит в том, что для более позднего требуется вызвать метод next_result MySQLi для извлечения всех трех результатов.

Мое личное убеждение в том, что это самый быстрый способ сделать это.

Если вы не согласны с тем, что хотите принять ответ «вне поля», я повторю то, что я сказал в некоторых комментариях.

Лучшим способом подойти к вашей проблеме является кеширование ваших данных заранее (будь то во внешнем JSON или XML-файле или в отдельной таблице базы данных, возможно, даже в таблице в памяти).

Таким образом, вы можете запланировать поражение производительности в таблице продуктов до тех пор, пока вы не узнаете, что сервер будет тихим, и уменьшите беспокойство о создании удара производительности в «случайные» моменты, когда посетитель прибудет на ваш сайт.

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