Простые случайные выборки из базы данных Sql


Как я могу взять эффективную простую случайную выборку в SQL? База данных, о которой идет речь, работает под управлением MySQL; моя таблица составляет не менее 200 000 строк, и мне нужна простая случайная выборка около 10 000.

"очевидный" ответ:

SELECT * FROM table ORDER BY RAND() LIMIT 10000

для больших таблиц это слишком медленно: он вызывает RAND() для каждой строки (которая уже помещает ее в O(n)) и сортирует их, делая ее o(n lg n) в лучшем случае. Есть ли способ сделать это быстрее, чем O(n)?

Примечание: как Эндрю Мао указывает в комментариях, если вы используете этот подход на SQL Server, вы должны использовать функцию T-SQL NEWID (), потому что RAND ()может возвращать одно и то же значение для всех строк.

РЕДАКТИРОВАТЬ: 5 ЛЕТ СПУСТЯ

Я снова столкнулся с этой проблемой с большей таблицей и в итоге использовал версию решения @ignorant с двумя настройками:

  • образец строк до 2-5x мой желаемый размер выборки, чтобы дешево заказать Рэнд ()
  • сохранить результат RAND () в индексированный столбец при каждой вставке/обновлении. (Если ваш набор данных не очень сильно обновлен, вам может потребоваться найти другой способ сохранить этот столбец свежим.)

чтобы взять образец 1000 элементов таблицы, я подсчитываю строки и пробую результат в среднем до 10 000 строк со столбцом frozen_rand:

SELECT COUNT(*) FROM table; -- Use this to determine rand_low and rand_high

  SELECT *
    FROM table
   WHERE frozen_rand BETWEEN %(rand_low)s AND %(rand_high)s
ORDER BY RAND() LIMIT 1000

(моя фактическая реализация включает в себя больше работы, чтобы убедиться, что я не занижаю пример, и вручную обернуть rand_high вокруг, но основная идея заключается в том, чтобы "случайно сократить N до нескольких тысяч.")

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

9 62

9 ответов:

есть очень интересное обсуждение этого типа вопроса здесь: http://www.titov.net/2005/09/21/do-not-use-order-by-rand-or-how-to-get-random-rows-from-table/

Я думаю, что абсолютно без предположений о таблице, что ваше решение O(n lg n) является лучшим. Хотя на самом деле с хорошим оптимизатором или немного другой техникой запрос, который вы перечисляете, может быть немного лучше, O(m*n) где m-количество случайных строк, так как это не так необходимо отсортировать весь большой массив, он может просто искать наименьшие m раз. Но для тех номеров, которые вы опубликовали, m все равно больше, чем lg n.

три предположения, которые мы могли бы попробовать:

  1. в таблице есть уникальный индексированный первичный ключ

  2. количество случайных строк, которые вы хотите выбрать (m), намного меньше, чем количество строк в таблице (n)

  3. уникальный первичный ключ является целым числом в диапазоне от 1 до n без пробелов

только с предположениями 1 и 2 я думаю, что это можно сделать в O(n), хотя вам нужно будет написать целый индекс в таблицу, чтобы соответствовать предположению 3, поэтому это не обязательно быстрый O(n). Если мы можем дополнительно предположить что-то еще хорошее о таблице, мы можем выполнить задачу в O(M log m). Предположение 3 было бы легким приятным дополнительным свойством для работы. С хорошим генератором случайных чисел, который гарантированное отсутствие дубликатов при генерации m чисел подряд, решение O(m) было бы возможно.

учитывая три предположения, основная идея состоит в том, чтобы генерировать m уникальных случайных чисел между 1 и n, а затем выбрать строки с этими ключами из таблицы. У меня нет mysql или что-то передо мной прямо сейчас, поэтому в слегка псевдокоде это будет выглядеть примерно так:


create table RandomKeys (RandomKey int)
create table RandomKeysAttempt (RandomKey int)

-- generate m random keys between 1 and n
for i = 1 to m
  insert RandomKeysAttempt select rand()*n + 1

-- eliminate duplicates
insert RandomKeys select distinct RandomKey from RandomKeysAttempt

-- as long as we don't have enough, keep generating new keys,
-- with luck (and m much less than n), this won't be necessary
while count(RandomKeys) &lt m
  NextAttempt = rand()*n + 1
  if not exists (select * from RandomKeys where RandomKey = NextAttempt)
    insert RandomKeys select NextAttempt

-- get our random rows
select *
from RandomKeys r
join table t ON r.RandomKey = t.UniqueKey

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

Я думаю, что самое быстрое решение

select * from table where rand() <= .3

вот почему я думаю, что это должно сделать работу.

  • он создаст случайное число для каждой строки. Число между 0 и 1
  • он оценивает, следует ли отображать эту строку, если сгенерированное число от 0 до .3 (30%).

это предполагает, что rand() генерирует числа в равномерном распределении. Это самый быстрый способ сделать это.

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

  • Это O(n), но сортировка не требуется, поэтому она быстрее, чем O (n lg n)
  • mysql очень способен генерировать случайные числа для каждой строки. Попробуйте это -

    выберите rand () из INFORMATION_SCHEMA.Таблица предел 10;

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

быстрее, чем заказ от RAND ()

Я проверил этот метод, чтобы быть намного быстрее, чем ORDER BY RAND(), следовательно, он работает в O (n) времени, и делает это впечатляюще быстро.

от http://technet.microsoft.com/en-us/library/ms189108%28v=sql.105%29.aspx:

Не-MSSQL версия -- Я не проверял это

SELECT * FROM Sales.SalesOrderDetail
WHERE 0.01 >= RAND()

версия MSSQL:

SELECT * FROM Sales.SalesOrderDetail
WHERE 0.01 >= CAST(CHECKSUM(NEWID(), SalesOrderID) & 0x7fffffff AS float) / CAST (0x7fffffff AS int)

это позволит выбрать ~1% записей. Так что если вы нужно точное количество процентов или записей, которые будут выбраны, оценить свой процент с некоторым запасом прочности, а затем случайным образом вырвать лишние записи из результирующего набора, используя более дорогой ORDER BY RAND() метод.

Еще Быстрее

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

например, если у вас есть индексированный столбец с равномерно распределенные целые числа [0..max], вы можете использовать это для случайного выбора N небольшой интервал. Сделать это динамически в вашей программе, чтобы получить другой набор для каждого выполнения запроса. Этот выбор подмножества будет O (N), который может на много порядков меньше, чем ваш полный набор данных.

в моем тесте я сократил время, необходимое для получения 20 (из 20 мил) образцов записей из 3 минуты используя ORDER BY RAND () вплоть до 0.0 секунд!

видимо, в некоторых версиях SQL есть TABLESAMPLE команда, но это не во всех реализациях SQL (в частности, Redshift).

http://technet.microsoft.com/en-us/library/ms189108 (v=sql. 105). aspx

просто использовать

WHERE RAND() < 0.1 

чтобы получить 10% записей или

WHERE RAND() < 0.01 

чтобы получить 1% записей и т. д.

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

select *
from table_name
where _id in (4, 1, 2, 5, 3)

мы можем прийти к результату, что если бы мы могли генерировать строку "(4, 1, 2, 5, 3)", тогда у нас был бы более эффективный способ, чем RAND().

например, в Java:

ArrayList<Integer> indices = new ArrayList<Integer>(rowsCount);
for (int i = 0; i < rowsCount; i++) {
    indices.add(i);
}
Collections.shuffle(indices);
String inClause = indices.toString().replace('[', '(').replace(']', ')');

если идентификаторы имеют пробелы, то начальный arraylist indices является результатом sql-запроса на идентификаторы.

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

Если вы хотите, чтобы ваш образец был независимым, вам нужно будет попробовать с заменой. Смотрите вопрос 25451034 для одного примера того, как это сделать, используя соединение таким же образом, как решение user12861. Этот решение написано для T-SQL, но концепция работает в любой SQL-БД.

Если вам нужно именно m строки, реально вы будете генерировать подмножество идентификаторов за пределами SQL. Большинство методов требуют в какой-то момент выбрать запись "nth", а таблицы SQL на самом деле не являются массивами вообще. Предположение, что ключи являются последовательными, чтобы просто присоединиться к случайным входам между 1 и количеством, также трудно удовлетворить - MySQL, например, не поддерживает его изначально, и условия блокировки... хитрый.

вот O(max(n, m lg n))времени O(n)-пространственного решения и при условии, просто используя кнопки:

  1. извлеките все значения ключевого столбца таблицы данных в любом порядке в массив на вашем любимом языке сценариев в O(n)
  2. выполнить Фишер-Йейтс перемешать остановка после m свопы, и извлечь подмассив [0:m-1] на ϴ(m)
  3. "присоединиться" подмассив с исходным набором данных (например,SELECT ... WHERE id IN (<subarray>)) in O(m lg n)

любой метод, который генерирует случайное подмножество вне SQL должен иметь по крайней мере эту сложность. Соединение не может быть быстрее, чем O(m lg n) С BTREE (так O(m) претензии являются фантазией для большинства двигателей) и перетасовка ограничена ниже n и m lg n и не влияет на асимптотическое поведение.

в обновления псевдокод:

ids = sql.query('SELECT id FROM t')
for i in range(m):
  r = int(random() * (len(ids) - i))
  ids[i], ids[i + r] = ids[i + r], ids[i]

results = sql.query('SELECT * FROM t WHERE id IN (%s)' % ', '.join(ids[0:m-1])

может быть, вы могли бы сделать

SELECT * FROM table LIMIT 10000 OFFSET FLOOR(RAND() * 190000)