Подтвердить что ты не робот

Как быстро выбрать 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.

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


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

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?

4b9b3361

Ответ 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, не является глупым. Если вы не хотите создавать соединение в своем запросе, просто загрузите больше данных, которые вам нужны, в новую таблицу.