Как быстро выбрать 3 случайные записи из таблицы 30k MySQL с фильтром где-то одним запросом?
Ну, это очень старый вопрос, который никогда не получал реального решения. Мы хотим, чтобы 3 случайные строки из таблицы содержали около 30 тыс. Записей. Таблица не такая большая, с точки зрения MySQL, но если она представляет продукты магазина, она является представительной. Случайный выбор полезен, когда вы представляете 3 случайных продукта на веб-странице, например. Мы хотели бы, чтобы единственное SQL-строковое решение отвечало следующим условиям:
- В PHP набор записей с помощью PDO или MySQLi должен иметь ровно 3 строки.
- Они должны быть получены одним запросом MySQL без использования хранимой процедуры.
- Решение должно быть быстрым, например, как оживленный сервер apache2, MySQL-запрос во многих ситуациях является узким местом. Поэтому он должен избегать создания временных таблиц и т.д.
- 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.
Поиск совершенства, сначала отбросим существующие решения, у которых есть недостатки:
слишком медленный, но гарантирует 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(),
from 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;
* ЦП около 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;
* CPU около 0.01 - 0.03s, сканирование 9690 строк, Использование где.
Здесь 3 - количество желаемых строк, 30000 - RecordCount таблицы Products, 16 - экспериментальный коэффициент, чтобы увеличить выбор, чтобы гарантировать выбор трех записей. Я не знаю, на каком основании коэффициент 16 является приемлемым приближением.
В большинстве случаев мы получаем 3 случайные записи, и это очень быстро, но это не оправдано: иногда запрос возвращает только 2 строки, иногда даже никакой записи.
Три вышеуказанных метода проверяют все записи таблицы, следующей за предложением WHERE, здесь 9690 строк.
Лучшая строка SQL?
Ответы
Ответ 1
Уродливый, но быстрый и случайный. Может стать очень уродливым очень быстро, особенно с настройкой, описанной ниже, поэтому убедитесь, что вы действительно этого хотите.
(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 и первым существующим идентификатором.
Вместо подзапроса (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 строки будут извлечены. Это просто делает его более вероятным.
Ответ 2
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
Ответ 3
Этот оператор выполняется очень быстро (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.
Ответ 4
Я тестировал следующую группу 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, если вы хотите выполнить их за один раз. И единственное их отличие состоит в том, что для более поздней версии требуется вызвать метод MySQLi next_result
для получения всех трех результатов.
Мое личное убеждение в том, что это самый быстрый способ сделать это.
Ответ 5
Как создать другую таблицу, содержащую только элементы с изображением? Эта таблица будет намного легче, так как она будет содержать только одну треть предметов, которые есть в оригинальной таблице!
------------------------------------------
|ID | Item ID (on the original table)|
------------------------------------------
|0 | 0 |
------------------------------------------
|1 | 123 |
------------------------------------------
.
.
.
------------------------------------------
|10 000 | 30 000 |
------------------------------------------
Затем вы можете генерировать три случайных идентификатора в части кода PHP и просто извлекать из базы данных.
Ответ 6
В том случае, если вы готовы принять ответ типа "вне поля", я повторю то, что я сказал в некоторых комментариях.
Лучший способ подойти к вашей проблеме - заранее кэшировать свои данные (будь то внешний файл JSON или XML или в отдельной таблице базы данных, возможно, даже в таблице в памяти).
Таким образом, вы можете запланировать поражение производительности в таблице продуктов до тех пор, пока вы не узнаете, что сервер будет тихим, и уменьшите беспокойство о создании удара производительности в "случайные" моменты, когда посетитель прибудет на ваш сайт.
Я не собираюсь предлагать явное решение, потому что слишком много возможностей для построения решения. Однако ответ, предложенный @ahmed, не является глупым. Если вы не хотите создавать соединение в своем запросе, просто загрузите больше данных, которые вам нужны, в новую таблицу.