Прості випадкові зразки з бази даних SQL


93

Як взяти ефективну просту випадкову вибірку в 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-5 разів за моїм бажаним розміром вибірки, щоб дешево ORDER BY RAND()
  • Зберігайте результат RAND()в індексованому стовпці при кожному вставці / оновленні. (Якщо ваш набір даних не дуже важкий для оновлення, можливо, вам доведеться знайти інший спосіб зберегти цей стовпець свіжим.)

Щоб взяти зразок таблиці з 1000 елементів, я підраховую рядки і вибираю результат до, в середньому, 10000 рядків із стовпцем 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()знову.


3
Це навіть не працює на сервері SQL, оскільки RAND()повертає одне і те ж значення при кожному наступному виклику.
Ендрю Мао,

1
Хороший момент - я додаю примітку, що користувачі SQL Server повинні замість цього використовувати ORDER BY NEWID ().
ojrac

Це все ще надзвичайно неефективно, тому що воно має сортувати всі дані. Метод довільної вибірки для певного відсотка є кращим, але я навіть прочитавши купу публікацій тут, я не знайшов прийнятного рішення, яке було б досить випадковим.
Ендрю Мао,

Якщо ви читаєте питання, я запитую конкретно, оскільки ЗАМОВИТИ ЗА РАНДОМ () - це O (n lg n).
ojrac

Відповідь muposat нижче чудова, якщо ви не надто одержимі статистичною випадковістю RAND ().
Джош Грейфер,

Відповіді:


25

Тут ведеться дуже цікаве обговорення цього типу питань: 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) < 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, мабуть, було б кращим у певному циклі та генерації випадкових чисел .


Я б порадив додати унікальний індекс до випадкового вибору ключів і, можливо, ігноруючи дублікати на вставці, тоді ви зможете позбутися різних матеріалів, і приєднання буде швидшим.
Сем Сафрон

Я думаю, що алгоритм випадкових чисел може використовувати деякі налаштування - або UNIQUE обмеження, як згадувалося, або просто генерувати 2 * m числа, і SELECT DISTINCT, ORDER BY id (first-come-first-serve, тому це зводиться до UNIQUE обмеження ) ОБМЕЖЕННЯ m. Мені це подобається.
ojrac

Щодо додавання унікального індексу до випадкового вибору ключа, а потім ігнорування дублікатів на вставці, я думав, що це може повернути вас до поведінки O (m ^ 2) замість O (m lg m) для сортування. Не впевнені, наскільки ефективно сервер підтримує індекс, вставляючи випадкові рядки по одному.
user12861

Щодо пропозицій генерувати 2 * м числа чи щось інше, я хотів, щоб алгоритм гарантував роботу незалежно від того, що. Завжди є (невеликий) шанс, що ваші випадкові числа 2 * м матимуть більше m дублікатів, тому вам не вистачить для вашого запиту.
user12861

1
Як отримати кількість рядків у таблиці?
Awesome-o

54

Я думаю, що найшвидше рішення

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

Ось чому я думаю, що це повинно зробити роботу.

  • Це створить випадкове число для кожного рядка. Цифра становить від 0 до 1
  • Він оцінює, чи відображати цей рядок, якщо згенероване число становить від 0 до .3 (30%).

Це передбачає, що rand () генерує числа в рівномірному розподілі. Це найшвидший спосіб зробити це.

Я побачив, що хтось рекомендував це рішення, і їх збили без доказів ... ось що я на це сказав би -

  • Це O (n), але сортування не потрібно, тому воно швидше, ніж O (n lg n)
  • mysql дуже здатний генерувати випадкові числа для кожного рядка. Спробуйте це -

    виберіть rand () з обмеження INFORMATION_SCHEMA.TABLES 10;

Оскільки мова йде про базу даних mySQL, це правильне рішення.


1
По-перше, у вас проблема, що це насправді не відповідає на запитання, оскільки воно отримує напіввипадкову кількість повернутих результатів, близьких до бажаного числа, але не обов’язково саме це число, замість точної бажаної кількості результатів.
user12861 07

1
Далі, щодо ефективності, вашим є O (n), де n - кількість рядків у таблиці. Це майже не так добре, як O (m log m), де m - кількість результатів, яку ви хочете, і m << n. Ви все ще можете мати рацію, що це буде швидше на практиці, тому що, як ви говорите, генерація rand () і порівняння їх з постійною МОЖЕ бути дуже швидкою. Вам доведеться протестувати це, щоб це з’ясувати. За меншими столами ви можете виграти. Маючи величезні таблиці та значно меншу кількість бажаних результатів, я сумніваюся.
user12861 07

1
Незважаючи на те, що @ user12861 має рацію щодо цього, не отримуючи точного правильного числа, це хороший спосіб скоротити встановлені дані до потрібного грубого розміру.
ojrac

1
Як база даних обслуговує такий запит - SELECT * FROM table ORDER BY RAND() LIMIT 10000 ? Спочатку потрібно створити випадкове число для кожного рядка (те саме, що рішення, яке я описав), а потім замовити його .. сорти дорогі! Ось чому це рішення буде повільнішим, ніж те, що я описав, оскільки ніяких видів не потрібно. Ви можете додати обмеження до описаного мною рішення, і воно не дасть вам більше, ніж ця кількість рядків. Як хтось правильно зазначив, це не дасть вам ТОЧНОГО розміру вибірки, але при випадкових вибірках, ТОЧНО, найчастіше не є суворою вимогою.
невіглас

Чи є спосіб вказати мінімальну кількість рядків?
CMCDragonkai

5

Очевидно, в деяких версіях SQL є TABLESAMPLEкоманда, але вона є не у всіх реалізаціях SQL (зокрема, Redshift).

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


Дуже круто! Схоже, це не реалізовано PostgreSQL або MySQL / MariaDB, але це чудова відповідь, якщо ви перебуваєте на реалізації SQL, яка його підтримує.
ojrac

Я розумію, що TABLESAMPLEце не випадково у статистичному сенсі.
Шон,

4

Просто використовуйте

WHERE RAND() < 0.1 

отримати 10% записів або

WHERE RAND() < 0.01 

отримати 1% записів тощо.


1
Це буде викликати RAND для кожного рядка, роблячи його O (n). Плакат шукав щось краще, ніж це.
user12861

1
Мало того, але RAND()повертає одне і те ж значення для наступних викликів (принаймні на MSSQL), тобто ви отримаєте або цілу таблицю, або жодну з них із такою ймовірністю.
Ендрю Мао,

4

Швидше, ніж ЗАМОВИТИ НА РЕНД ()

Я протестував цей метод набагато швидше 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 хв за допомогою ЗАМОВЛЕННЯ ЗА РАНДОМ () до 0,0 секунд !


1

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

Якщо ви хочете, щоб ваш зразок був незалежним, вам доведеться зробити зразок із заміною. Див. Запитання 25451034 для одного прикладу того, як це зробити за допомогою JOIN способом, подібним до рішення user12861. Рішення написано для T-SQL, але концепція працює в будь-якій базі даних SQL.


0

Починаючи з спостереження, що ми можемо отримати ідентифікатори таблиці (наприклад, граф 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(']', ')');

Якщо ідентифікатори мають прогалини, то початковий список списку indicesє результатом запиту sql на ідентифікатори.


0

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

Ось рішення O(max(n, m lg n))-time, O(n)-space, яке передбачає використання простих ключів BTREE:

  1. Отримати всі значення ключового стовпця таблиці даних у будь-якому порядку в масив улюбленою мовою сценаріїв у O(n)
  2. Виконайте перетасувати Fisher-Yates , зупиняючись після mсвопів, і витягти подмассів [0:m-1]вϴ(m)
  3. "Приєднати" підмасив до вихідного набору даних (наприклад SELECT ... WHERE id IN (<subarray>)) у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])

0

Виберіть 3000 випадкових записів у Netezza:

WITH IDS AS (
     SELECT ID
     FROM MYTABLE;
)

SELECT ID FROM IDS ORDER BY mt_random() LIMIT 3000

Окрім додавання деяких нотаток, специфічних для діалекту SQL, я не думаю, що це відповідає на питання про те, як запитувати випадкову вибірку рядків без "ORDER BY rand () LIMIT $ 1".
ojrac

0

Спробуйте

SELECT TOP 10000 * FROM table ORDER BY NEWID()

Чи дасть це бажані результати, не будучи надто складним?


Зверніть увагу, що NEWID()це специфічно для T-SQL.
Пітер О.

Мої вибачення. Це є. Дякую. Однак корисно знати, якщо хтось приходить сюди, дивлячись на мене кращим чином, і використовує T-SQL
Northernlad

ORDER BY NEWID()функціонально те саме, що ORDER BY RAND()- він викликає RAND()кожен рядок у наборі - O (n) - і потім сортує всю річ - O (n lg n). Іншими словами, це найгірше рішення, яке це питання хоче покращити.
ojrac

0

У певних діалектах, таких як Microsoft SQL Server, PostgreSQL та Oracle (але не MySQL чи SQLite), ви можете робити щось на зразок

select distinct top 10000 customer_id from nielsen.dbo.customer TABLESAMPLE (20000 rows) REPEATABLE (123);

Причиною того, що не просто (10000 rows)обійтися без, topє те, що TABLESAMPLEлогіка дає надзвичайно неточну кількість рядків (наприклад, іноді 75%, іноді 1,25% разів), тому ви хочете зробити вибірку та вибрати точну кількість, яку хочете. Це REPEATABLE (123)для надання випадкового насіння.


-4

Можливо, ти міг би це зробити

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

1
Схоже, це вибрало б випадковий фрагмент моїх даних; Я шукаю щось дещо складніше - 10000 випадково розподілених рядків.
ojrac

Тоді вашим єдиним варіантом, якщо ви хочете це зробити в базі даних, є ORDER BY rand ().
staticsan
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.