MySQL быстро выбирает 10 случайных строк из 600К строк - PullRequest
429 голосов
/ 02 декабря 2010

Как лучше всего написать запрос, который выбирает 10 строк случайным образом из общего числа 600 КБ?

Ответы [ 24 ]

2 голосов
/ 15 мая 2015

Вот изменитель игры, который может быть полезным для многих;

У меня есть таблица с 200k строк, с последовательными идентификаторами , мне нужно было выбрать N случайных строк, поэтому я предпочитаю генерировать случайные значения на основе наибольшего идентификатора в таблице Я создал этот скрипт, чтобы выяснить, какая операция самая быстрая:

logTime();
query("SELECT COUNT(id) FROM tbl");
logTime();
query("SELECT MAX(id) FROM tbl");
logTime();
query("SELECT id FROM tbl ORDER BY id DESC LIMIT 1");
logTime();

Результаты:

  • Количество: 36.8418693542479 мс
  • Макс .: 0.241041183472 мс
  • Заказ: 0.216960906982 мс

Исходя из этих результатов, Order Desc является самой быстрой операцией для получения максимального идентификатора,
Вот мой ответ на вопрос:

SELECT GROUP_CONCAT(n SEPARATOR ',') g FROM (
    SELECT FLOOR(RAND() * (
        SELECT id FROM tbl ORDER BY id DESC LIMIT 1
    )) n FROM tbl LIMIT 10) a

...
SELECT * FROM tbl WHERE id IN ($result);

К вашему сведению: чтобы получить 10 случайных строк из таблицы 200К, мне понадобилось 1,78 мс (включая все операции на стороне php)

2 голосов
/ 21 февраля 2019

Это очень быстро и на 100% случайно, даже если у вас есть пробелы.

  1. Подсчитайте количество x доступных вам строк SELECT COUNT(*) as rows FROM TABLE
  2. Pick 10различные случайные числа a_1,a_2,...,a_10 между 0 и x
  3. Запросите ваши строки следующим образом: SELECT * FROM TABLE LIMIT 1 offset a_i для i = 1, ..., 10

Я нашел этот хакв книге Антипаттерны SQL из Билл Карвин .

2 голосов
/ 07 июля 2015

Все лучшие ответы уже опубликованы (в основном те, которые ссылаются на ссылку http://jan.kneschke.de/projects/mysql/order-by-rand/).

Я хочу указать еще одну возможность ускорения - кеширование . Подумайте, почему вам нужно получить случайные строки. Вероятно, вы хотите разместить на сайте какой-нибудь случайный пост или случайную рекламу. Если вы получаете 100 запросов в секунду, действительно ли нужно, чтобы каждый посетитель получал случайные строки? Обычно вполне нормально кэшировать эти X случайных строк в течение 1 секунды (или даже 10 секунд). Не имеет значения, если 100 уникальных посетителей в одну и ту же секунду получат одинаковые случайные записи, потому что в следующую секунду еще 100 посетителей получат другой набор сообщений.

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

2 голосов
/ 29 марта 2017

Я улучшил ответ @Riedsio. Это самый эффективный запрос, который я могу найти в большой, равномерно распределенной таблице с пробелами (проверено на получение 1000 случайных строк из таблицы, в которой> 2.6Б строк).

(SELECT id FROM table INNER JOIN (SELECT FLOOR(RAND() * @max := (SELECT MAX(id) FROM table)) + 1 as rand) r on id > rand LIMIT 1) UNION
(SELECT id FROM table INNER JOIN (SELECT FLOOR(RAND() * @max) + 1 as rand) r on id > rand LIMIT 1) UNION
(SELECT id FROM table INNER JOIN (SELECT FLOOR(RAND() * @max) + 1 as rand) r on id > rand LIMIT 1) UNION
(SELECT id FROM table INNER JOIN (SELECT FLOOR(RAND() * @max) + 1 as rand) r on id > rand LIMIT 1) UNION
(SELECT id FROM table INNER JOIN (SELECT FLOOR(RAND() * @max) + 1 as rand) r on id > rand LIMIT 1) UNION
(SELECT id FROM table INNER JOIN (SELECT FLOOR(RAND() * @max) + 1 as rand) r on id > rand LIMIT 1) UNION
(SELECT id FROM table INNER JOIN (SELECT FLOOR(RAND() * @max) + 1 as rand) r on id > rand LIMIT 1) UNION
(SELECT id FROM table INNER JOIN (SELECT FLOOR(RAND() * @max) + 1 as rand) r on id > rand LIMIT 1) UNION
(SELECT id FROM table INNER JOIN (SELECT FLOOR(RAND() * @max) + 1 as rand) r on id > rand LIMIT 1) UNION
(SELECT id FROM table INNER JOIN (SELECT FLOOR(RAND() * @max) + 1 as rand) r on id > rand LIMIT 1)

Позвольте мне распаковать, что происходит.

  1. @max := (SELECT MAX(id) FROM table)
    • Я рассчитываю и сохраняю макс. Для очень больших таблиц есть небольшие издержки для вычисления MAX(id) каждый раз, когда вам нужна строка
  2. SELECT FLOOR(rand() * @max) + 1 as rand)
    • Получает случайный идентификатор
  3. SELECT id FROM table INNER JOIN (...) on id > rand LIMIT 1
    • Это заполняет пробелы. В основном, если вы случайно выберете число в промежутках, он просто выберет следующий идентификатор. Предполагая, что промежутки равномерно распределены, это не должно быть проблемой.

Выполнение объединения поможет вам вписать все в один запрос, чтобы вы могли избежать выполнения нескольких запросов. Это также позволяет вам сэкономить на вычислении MAX(id). В зависимости от вашего приложения это может иметь большое или очень важное значение.

Обратите внимание, что это получает только идентификаторы и получает их в случайном порядке. Если вы хотите сделать что-то более сложное, я рекомендую вам сделать это:

SELECT t.id, t.name -- etc, etc
FROM table t
INNER JOIN (
    (SELECT id FROM table INNER JOIN (SELECT FLOOR(RAND() * @max := (SELECT MAX(id) FROM table)) + 1 as rand) r on id > rand LIMIT 1) UNION
    (SELECT id FROM table INNER JOIN (SELECT FLOOR(RAND() * @max) + 1 as rand) r on id > rand LIMIT 1) UNION
    (SELECT id FROM table INNER JOIN (SELECT FLOOR(RAND() * @max) + 1 as rand) r on id > rand LIMIT 1) UNION
    (SELECT id FROM table INNER JOIN (SELECT FLOOR(RAND() * @max) + 1 as rand) r on id > rand LIMIT 1) UNION
    (SELECT id FROM table INNER JOIN (SELECT FLOOR(RAND() * @max) + 1 as rand) r on id > rand LIMIT 1) UNION
    (SELECT id FROM table INNER JOIN (SELECT FLOOR(RAND() * @max) + 1 as rand) r on id > rand LIMIT 1) UNION
    (SELECT id FROM table INNER JOIN (SELECT FLOOR(RAND() * @max) + 1 as rand) r on id > rand LIMIT 1) UNION
    (SELECT id FROM table INNER JOIN (SELECT FLOOR(RAND() * @max) + 1 as rand) r on id > rand LIMIT 1) UNION
    (SELECT id FROM table INNER JOIN (SELECT FLOOR(RAND() * @max) + 1 as rand) r on id > rand LIMIT 1) UNION
    (SELECT id FROM table INNER JOIN (SELECT FLOOR(RAND() * @max) + 1 as rand) r on id > rand LIMIT 1)
) x ON x.id = t.id
ORDER BY t.id
1 голос
/ 13 июля 2017

Если вы хотите одну случайную запись (независимо от наличия пробелов между идентификаторами):

PREPARE stmt FROM 'SELECT * FROM `table_name` LIMIT 1 OFFSET ?';
SET @count = (SELECT
        FLOOR(RAND() * COUNT(*))
    FROM `table_name`);

EXECUTE stmt USING @count;

Источник: https://www.warpconduit.net/2011/03/23/selecting-a-random-record-using-mysql-benchmark-results/#comment-1266

1 голос
/ 22 июня 2016

Один способ, который я нахожу довольно хорошим, если есть автоматически сгенерированный идентификатор, это использовать оператор по модулю "%". Например, если вам нужно 10 000 случайных записей из 70 000, вы можете упростить это, сказав, что вам нужна 1 из каждых 7 строк. Это может быть упрощено в этом запросе:

SELECT * FROM 
    table 
WHERE 
    id % 
    FLOOR(
        (SELECT count(1) FROM table) 
        / 10000
    ) = 0;

Если результат деления целевых строк на общее количество не является целым числом, у вас будет несколько дополнительных строк, чем вы запрашивали, поэтому вы должны добавить предложение LIMIT, чтобы помочь вам обрезать результирующий набор следующим образом:

SELECT * FROM 
    table 
WHERE 
    id % 
    FLOOR(
        (SELECT count(1) FROM table) 
        / 10000
    ) = 0
LIMIT 10000;

Это требует полного сканирования, но это быстрее, чем ORDER BY RAND, и, на мой взгляд, проще для понимания, чем другие опции, упомянутые в этой теме. Кроме того, если система, выполняющая запись в БД, создает наборы строк в пакетном режиме, вы можете не получить такого случайного результата, как ожидали.

1 голос
/ 09 ноября 2015

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

SELECT d.* FROM (
SELECT  t.*,  @rownum := @rownum + 1 AS rank
FROM mytable AS t,
    (SELECT @rownum := 0) AS r,
    (SELECT @cnt := (SELECT RAND() * (SELECT COUNT(*) FROM mytable))) AS n
) d WHERE rank >= @cnt LIMIT 10;

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

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

SELECT * FROM (
SELECT d.* FROM (
    SELECT  c.*,  @rownum := @rownum + 1 AS rank
    FROM buildbrain.`commits` AS c,
        (SELECT @rownum := 0) AS r,
        (SELECT @cnt := (SELECT RAND() * (SELECT COUNT(*) FROM buildbrain.`commits`))) AS rnd
) d 
WHERE rank >= @cnt LIMIT 10000 
) t ORDER BY RAND() LIMIT 10;
1 голос
/ 07 мая 2014

Если у вас есть только один запрос на чтение

Объедините ответ @redsio с таблицей темпов (600K не так уж много):

DROP TEMPORARY TABLE IF EXISTS tmp_randorder;
CREATE TABLE tmp_randorder (id int(11) not null auto_increment primary key, data_id int(11));
INSERT INTO tmp_randorder (data_id) select id from datatable;

А затем возьмите версиюof @redsios Ответ:

SELECT dt.*
FROM
       (SELECT (RAND() *
                     (SELECT MAX(id)
                        FROM tmp_randorder)) AS id)
        AS rnd
 INNER JOIN tmp_randorder rndo on rndo.id between rnd.id - 10 and rnd.id + 10
 INNER JOIN datatable AS dt on dt.id = rndo.data_id
 ORDER BY abs(rndo.id - rnd.id)
 LIMIT 1;

Если таблица большая, вы можете просеять первую часть:

INSERT INTO tmp_randorder (data_id) select id from datatable where rand() < 0.01;

Если у вас много запросов на чтение

  1. Версия: Вы можете сохранить таблицу tmp_randorder постоянной, назовите ее datatable_idlist.Повторно создавайте эту таблицу через определенные промежутки времени (день, час), так как она также будет иметь дыры.Если ваша таблица становится действительно большой, вы также можете заполнить дыры

    выбрать l.data_id как целое из списка данных didatable_idlist l оставить соединение с данными dt на dt.id = l.data_id, где dt.id равен нулю;

  2. Версия: Дайте вашему набору данных столбец random_sortorder либо непосредственно в таблице данных, либо в постоянной дополнительной таблице datatable_sortorder.Индексируйте этот столбец.Сгенерируйте случайное значение в вашем приложении (я назову его $rand).

    select l.*
    from datatable l 
    order by abs(random_sortorder - $rand) desc 
    limit 1;
    

Это решение различает «крайние строки» с самым высоким и самым низким random_sortorder,поэтому переставляйте их с интервалами (один раз в день).

0 голосов
/ 30 апреля 2019

Следующее должно быть быстрым, беспристрастным и независимым от столбца id. Однако это не гарантирует, что количество возвращаемых строк будет соответствовать количеству запрошенных строк.

SELECT *
FROM t
WHERE RAND() < (SELECT 10 / COUNT(*) FROM t)

Объяснение: если вы хотите, чтобы 10 строк из 100, то каждая строка имела 1/10 вероятности получения SELECT, чего можно достичь с помощью WHERE RAND() < 0.1. Этот подход не гарантирует 10 строк; но если запрос выполняется достаточно раз, среднее число строк на выполнение будет около 10, и каждая строка в таблице будет выбрана равномерно.

0 голосов
/ 25 июня 2018

Я просмотрел все ответы, и я не думаю, что кто-то вообще упоминает эту возможность, и я не уверен, почему.

Если вам нужна предельная простота и скорость при минимальных затратах, то, мне кажется, имеет смысл хранить случайное число для каждой строки в БД. Просто создайте дополнительный столбец random_number и установите для него значение по умолчанию RAND(). Создайте индекс для этого столбца.

Затем, когда вы хотите извлечь строку, сгенерируйте случайное число в вашем коде (PHP, Perl и т. Д.) И сравните его со столбцом.

SELECT FROM tbl WHERE random_number >= :random LIMIT 1

Полагаю, хотя для одной строки она очень аккуратна, для десяти строк, например, ОП спрашивал, что вам придется вызывать его десять раз (или придумать хитрый твик, который сразу ускользает от меня)

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...