Простые случайные образцы из базы данных 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, вам следует использовать функцию NEWID(), поскольку RAND() может возвращать одинаковое значение для всех строк.

ИЗМЕНИТЬ: 5 ЛЕТ ПОЗЖЕ

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

  • Попробуйте строки для 2-5x желаемого размера выборки, чтобы дешево ORDER BY RAND()
  • Сохраните результат 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 до нескольких тысяч".)

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

Ответ 1

Здесь очень интересное обсуждение этого типа проблемы: 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.

Три вопроса, которые мы можем опробовать:

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

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

  • уникальный первичный ключ представляет собой целое число, которое варьируется от 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, вероятно, будет лучше в виде циклов и случайных требуется число поколений.

Ответ 2

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

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

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

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

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

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

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

    выберите rand() из INFORMATION_SCHEMA.TABLES limit 10;

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

Ответ 3

Быстрее, чем ORDER BY 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 секунд !

Ответ 5

Просто используйте

WHERE RAND() < 0.1 

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

WHERE RAND() < 0.01 

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

Ответ 6

Начиная с наблюдения, что мы можем получить идентификаторы таблицы (например, счет 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 на идентификаторы.

Ответ 7

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

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

Ответ 8

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

Здесь O(max(n, m lg n)) -time, O(n) -пространственное решение, предполагающее простые клавиши BTREE:

  • Получить все значения столбца ключа таблицы данных в любом порядке в массив на вашем любимом языке сценариев в O(n)
  • Выполните Fisher-Yates shuffle, остановившись после m свопов и извлеките подмассив [0:m-1] в ϴ(m)
  • "Присоединиться" к подрамнику с исходным набором данных (например, SELECT ... WHERE id IN (<subarray>)) в O(m lg n)

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

В Python псевдокоде:

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])

Ответ 9

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

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