Proste próbki losowe z bazy danych SQL


93

Jak pobrać wydajną, prostą próbkę losową w języku SQL? Wspomniana baza danych korzysta z MySQL; moja tabela ma co najmniej 200 000 wierszy, a potrzebuję prostej losowej próbki około 10 000.

„Oczywista” odpowiedź brzmi:

SELECT * FROM table ORDER BY RAND() LIMIT 10000

W przypadku dużych tabel jest to zbyt wolne: wywołuje RAND()każdy wiersz (który już umieszcza go w pozycji O (n)) i sortuje je, tworząc w najlepszym przypadku O (n lg n). Czy istnieje sposób, aby to zrobić szybciej niż O (n)?

Uwaga : Jak wskazuje Andrew Mao w komentarzach, jeśli używasz tego podejścia na serwerze SQL, powinieneś użyć funkcji T-SQL NEWID(), ponieważ RAND () może zwrócić tę samą wartość dla wszystkich wierszy .

EDYCJA: 5 LAT PÓŹNIEJ

Ponownie natknąłem się na ten problem z większą tabelą i ostatecznie użyłem wersji rozwiązania @ ignorant, z dwoma poprawkami:

  • Wypróbuj wiersze, aby uzyskać 2-5x żądany rozmiar próbki, aby tanio ORDER BY RAND()
  • Zapisz wynik w RAND()indeksowanej kolumnie przy każdym wstawieniu / aktualizacji. (Jeśli zestaw danych nie wymaga dużej ilości aktualizacji, może być konieczne znalezienie innego sposobu, aby zachować aktualność tej kolumny).

Aby pobrać próbkę tabeli zawierającą 1000 pozycji, liczę wiersze i próbuję wynik do średnio 10000 wierszy z kolumną 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

(Moja rzeczywista implementacja wymaga więcej pracy, aby upewnić się, że nie zaniżam próbki, i aby ręcznie zawijać rand_high, ale podstawową ideą jest „losowe zmniejszenie liczby N do kilku tysięcy”).

Chociaż wymaga to pewnych poświęceń, pozwala mi na próbkowanie bazy danych za pomocą skanowania indeksu, dopóki nie będzie wystarczająco mała, aby ORDER BY RAND()ponownie.


3
To nawet nie działa na serwerze SQL, ponieważ RAND()zwraca tę samą wartość przy każdym kolejnym wywołaniu.
Andrew Mao,

1
Słuszna uwaga - dodam uwagę, że użytkownicy SQL Server powinni zamiast tego używać ORDER BY NEWID ().
ojrac,

Wciąż jest strasznie nieefektywny, ponieważ musi sortować wszystkie dane. Technika losowego próbkowania dla niektórych procent jest lepsza, ale nawet po przeczytaniu kilku postów tutaj nie znalazłem akceptowalnego rozwiązania, które jest wystarczająco losowe.
Andrew Mao,

Jeśli czytasz pytanie, pytam konkretnie, ponieważ ORDER BY RAND () to O (n lg n).
ojrac

Odpowiedź muposat poniżej jest świetna, jeśli nie masz obsesji na punkcie statystycznej losowości RAND ().
Josh Greifer

Odpowiedzi:


25

Jest tutaj bardzo interesująca dyskusja na ten temat: http://www.titov.net/2005/09/21/do-not-use-order-by-rand-or-how-to-get-random-rows-from-table/

Myślę, że bez żadnych założeń dotyczących tabeli, twoje rozwiązanie O (n lg n) jest najlepsze. Chociaż w rzeczywistości z dobrym optymalizatorem lub nieco inną techniką zapytanie, które podajesz, może być nieco lepsze, O (m * n) gdzie m to liczba żądanych losowych wierszy, ponieważ nie musiałoby to koniecznie sortować całej dużej tablicy , może po prostu wyszukać najmniejsze m razy. Ale dla rodzaju liczb, które opublikowałeś, i tak m jest większe niż lg n.

Trzy założenia, które możemy wypróbować:

  1. w tabeli znajduje się unikalny, indeksowany klucz podstawowy

  2. liczba losowych wierszy, które chcesz zaznaczyć (m) jest znacznie mniejsza niż liczba wierszy w tabeli (n)

  3. unikalny klucz podstawowy to liczba całkowita w zakresie od 1 do n bez przerw

Przy tylko założeniach 1 i 2 myślę, że można to zrobić w O (n), chociaż będziesz musiał zapisać cały indeks do tabeli, aby pasował do założenia 3, więc niekoniecznie jest to szybkie O (n). Jeśli możemy DODATKOWO założyć coś fajnego w tabeli, możemy wykonać zadanie w O (m log m). Założenie 3 byłoby łatwą, przyjemną dodatkową właściwością do pracy. Z ładnym generatorem liczb losowych, który gwarantowałby brak duplikatów podczas generowania m liczb w rzędzie, możliwe byłoby rozwiązanie O (m).

Biorąc pod uwagę te trzy założenia, podstawową ideą jest wygenerowanie m unikalnych liczb losowych od 1 do n, a następnie wybranie wierszy z tymi kluczami z tabeli. Nie mam teraz mysql ani nic przed sobą, więc w nieco pseudokodzie wyglądałoby to mniej więcej tak:


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

Jeśli naprawdę martwisz się o wydajność, możesz rozważyć wykonanie losowego generowania kluczy w jakimś języku proceduralnym i wstawienie wyników do bazy danych, ponieważ prawie wszystko inne niż SQL prawdopodobnie byłoby lepsze w rodzaju pętli i generowaniu liczb losowych .


Zalecałbym dodanie unikalnego indeksu do losowego wyboru klucza i być może zignorowanie duplikatów na wkładce, wtedy możesz pozbyć się różnych rzeczy, a połączenie będzie szybsze.
Sam Saffron

Myślę, że algorytm liczb losowych mógłby użyć pewnych poprawek - albo unikalnego ograniczenia, jak wspomniano, lub po prostu wygenerować 2 * m liczb i SELECT DISTINCT, ORDER BY id (kto pierwszy, ten lepszy, więc ogranicza się to do ograniczenia UNIQUE ) LIMIT m. Lubię to.
ojrac

Jeśli chodzi o dodanie unikalnego indeksu do losowego wyboru klucza, a następnie ignorowanie duplikatów przy wstawianiu, pomyślałem, że może to spowodować powrót do zachowania O (m ^ 2) zamiast O (m lg m) w celu sortowania. Nie jestem pewien, jak wydajnie serwer utrzymuje indeks podczas wstawiania losowych wierszy pojedynczo.
user12861

Jeśli chodzi o sugestie dotyczące generowania liczb 2 * m lub czegoś podobnego, chciałem, aby algorytm działał bez względu na wszystko. Zawsze istnieje (niewielka) szansa, że ​​twoje 2 * m liczb losowych będą miały więcej niż m duplikatów, więc nie będziesz mieć ich wystarczająco dużo na zapytanie.
user12861

1
Jak obliczasz liczbę wierszy w tabeli?
Awesome-o

54

Myślę, że najszybszym rozwiązaniem jest

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

Oto dlaczego uważam, że to powinno wystarczyć.

  • Stworzy losową liczbę dla każdego wiersza. Liczba zawiera się w przedziale od 0 do 1
  • Ocenia, czy wyświetlić ten wiersz, jeśli wygenerowana liczba zawiera się w przedziale od 0 do 0,3 (30%).

Zakłada się, że rand () generuje liczby w równomiernym rozkładzie. To najszybszy sposób, aby to zrobić.

Widziałem, że ktoś polecił to rozwiązanie i został zestrzelony bez dowodu ... oto, co bym na to powiedział -

  • To jest O (n), ale sortowanie nie jest wymagane, więc jest szybsze niż O (n lg n)
  • mysql jest bardzo zdolny do generowania liczb losowych dla każdego wiersza. Spróbuj tego -

    wybierz rand () z INFORMATION_SCHEMA.TABLES limit 10;

Ponieważ ta baza danych to mySQL, jest to właściwe rozwiązanie.


1
Po pierwsze, masz problem z tym, że to tak naprawdę nie odpowiada na pytanie, ponieważ otrzymuje półlosową liczbę zwróconych wyników, zbliżoną do żądanej, ale niekoniecznie dokładnie tej liczby, zamiast dokładnej pożądanej liczby wyników.
user12861

1
Następnie, jeśli chodzi o wydajność, twoja to O (n), gdzie n to liczba wierszy w tabeli. To nie jest tak dobre, jak O (m log m), gdzie m to liczba żądanych wyników, a m << n. Wciąż możesz mieć rację, że w praktyce byłoby to szybsze, ponieważ, jak mówisz, generowanie rand () i porównywanie ich ze stałą MOGŁO być bardzo szybkie. Musiałbyś to przetestować, żeby się dowiedzieć. Przy mniejszych stołach możesz wygrać. Przy ogromnych tabelach i znacznie mniejszej liczbie pożądanych wyników wątpię w to.
user12861,

1
Chociaż @ user12861 ma rację co do tego, że nie otrzymuje dokładnej właściwej liczby, jest to dobry sposób na zmniejszenie zestawu danych do odpowiedniego, przybliżonego rozmiaru.
ojrac

1
W jaki sposób baza danych obsługuje następujące zapytanie - SELECT * FROM table ORDER BY RAND() LIMIT 10000 ? Najpierw musi utworzyć losową liczbę dla każdego wiersza (tak samo jak rozwiązanie, które opisałem), a następnie zamówić… sortowania są drogie! Dlatego to rozwiązanie BĘDZIE wolniejsze niż to, które opisałem, ponieważ żadne rodzaje nie są wymagane. Możesz dodać ograniczenie do opisanego przeze mnie rozwiązania i nie da ci to więcej niż ta liczba wierszy. Jak ktoś słusznie zauważył, nie da ci to DOKŁADNEJ wielkości próby, ale w przypadku próbek losowych, EXACT najczęściej nie jest wymaganiem ścisłym.
ignorant

Czy istnieje sposób określenia minimalnej liczby wierszy?
CMCDragonkai

5

Najwyraźniej w niektórych wersjach SQL jest TABLESAMPLEpolecenie, ale nie we wszystkich implementacjach SQL (zwłaszcza w Redshift).

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


Bardzo fajny! Wygląda na to, że nie jest również zaimplementowany przez PostgreSQL lub MySQL / MariaDB, ale jest to świetna odpowiedź, jeśli korzystasz z implementacji SQL, która ją obsługuje.
ojrac

Rozumiem, że TABLESAMPLEnie jest to przypadek w sensie statystycznym.
Sean

4

Po prostu użyj

WHERE RAND() < 0.1 

aby uzyskać 10% rekordów lub

WHERE RAND() < 0.01 

uzyskać 1% rekordów itp.


1
Spowoduje to wywołanie RAND dla każdego wiersza, dzięki czemu będzie O (n). Plakat szukał czegoś lepszego.
user12861

1
Nie tylko to, ale RAND()zwraca tę samą wartość dla kolejnych wywołań (przynajmniej na MSSQL), co oznacza, że ​​otrzymasz całą tabelę lub żadną z niej z takim prawdopodobieństwem.
Andrew Mao,

4

Szybciej niż ZAMÓWIENIE LASEM ()

Przetestowałem tę metodę jako znacznie szybszą niż ORDER BY RAND(), dlatego działa w czasie O (n) i robi to imponująco szybko.

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

Wersja inna niż MSSQL - nie testowałem tego

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

Wersja MSSQL:

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

Spowoduje to wybranie ~ 1% rekordów. Więc jeśli potrzebujesz dokładnej liczby procent lub rekordów do wybrania, oszacuj swój procent z pewnym marginesem bezpieczeństwa, a następnie losowo wyciągnij nadmiar rekordów z wynikowego zestawu, używając droższej ORDER BY RAND()metody.

Nawet szybciej

Udało mi się ulepszyć tę metodę jeszcze bardziej, ponieważ miałem dobrze znany indeksowany zakres wartości kolumn.

Na przykład, jeśli masz indeksowaną kolumnę z równomiernie rozłożonymi liczbami całkowitymi [0..max], możesz użyć jej do losowego wybrania N małych przedziałów. Zrób to dynamicznie w swoim programie, aby uzyskać inny zestaw dla każdego uruchomienia zapytania. Ten podzbiór będzie O (N) , który może być o wiele rzędów wielkości mniejszy niż pełny zestaw danych.

W moim teście zredukowałem czas potrzebny do uzyskania 20 (z 20 milionów) przykładowych rekordów z 3 minut przy użyciu funkcji ORDER BY RAND () do 0,0 sekundy !


1

Chcę zaznaczyć, że wszystkie te rozwiązania wydają się próbkować bez wymiany. Wybranie górnych K wierszy z losowego sortowania lub dołączenie do tabeli zawierającej unikalne klucze w losowej kolejności spowoduje wygenerowanie losowej próbki bez zastępowania.

Jeśli chcesz, aby Twoja próbka była niezależna, musisz pobrać próbkę z wymianą. Zobacz pytanie 25451034, aby zapoznać się z jednym przykładem, jak to zrobić za pomocą JOIN w sposób podobny do rozwiązania user12861. Rozwiązanie jest napisane dla T-SQL, ale koncepcja działa w każdej bazie danych SQL.


0

Zaczynając od obserwacji, że możemy pobrać identyfikatory tabeli (np. Count 5) na podstawie zbioru:

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

możemy dojść do wyniku, że gdybyśmy mogli wygenerować ciąg "(4, 1, 2, 5, 3)", mielibyśmy bardziej wydajny sposób niż RAND().

Na przykład w Javie:

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(']', ')');

Jeśli identyfikatory mają luki, początkowa lista arraylista indicesjest wynikiem zapytania sql dotyczącego identyfikatorów.


0

Jeśli potrzebujesz dokładnie mwierszy, realistycznie wygenerujesz podzbiór identyfikatorów poza SQL. Większość metod wymaga w pewnym momencie wybrania pozycji „nth”, a tabele SQL w rzeczywistości nie są tablicami. Założenie, że klucze są kolejne, aby po prostu łączyć losowe liczby między 1 a liczbą, również jest trudne do spełnienia - na przykład MySQL nie obsługuje go natywnie, a warunki blokady są ... trudne .

Oto rozwiązanie czasowe O(max(n, m lg n))i O(n)przestrzenne, zakładające zwykłe klucze BTREE:

  1. Pobierz wszystkie wartości kolumny klucza tabeli danych w dowolnej kolejności do tablicy w swoim ulubionym języku skryptowym w O(n)
  2. Przeprowadzić Fisher-Yates shuffle, zatrzymując się po mswapach, i wyodrębnić subarray [0:m-1]wϴ(m)
  3. „Połącz” podtablicę z oryginalnym zbiorem danych (np. SELECT ... WHERE id IN (<subarray>)) W formacieO(m lg n)

Każda metoda, która generuje losowy podzbiór poza SQL, musi mieć co najmniej taką złożoność. Łączenie nie może być szybsze niż w O(m lg n)przypadku BTREE (więc O(m)twierdzenia są fantastyczne w przypadku większości silników), a tasowanie jest ograniczone poniżej ni m lg nnie wpływa na asymptotyczne zachowanie.

W pseudokodzie Pythonic:

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

Wybierz 3000 losowych rekordów w Netezza:

WITH IDS AS (
     SELECT ID
     FROM MYTABLE;
)

SELECT ID FROM IDS ORDER BY mt_random() LIMIT 3000

Poza dodaniem kilku uwag specyficznych dla dialektu SQL, nie sądzę, aby to odpowiadało na pytanie, jak zapytać o losową próbkę wierszy bez 'ORDER BY rand () LIMIT $ 1'.
ojrac

0

Próbować

SELECT TOP 10000 * FROM table ORDER BY NEWID()

Czy przyniosłoby to pożądane rezultaty, nie będąc zbyt skomplikowanym?


Zauważ, że NEWID()jest to specyficzne dla T-SQL.
Peter O.

Przepraszam. To jest. Dzięki Dobrze jest jednak wiedzieć, czy ktoś przychodzi tutaj patrząc tak jak ja w lepszy sposób i używa T-SQL
Northernlad

ORDER BY NEWID()jest funkcjonalnie taki sam jak ORDER BY RAND()- wywołuje RAND()każdy wiersz w zbiorze - O (n) - a następnie sortuje całość - O (n lg n). Innymi słowy, jest to najgorsze rozwiązanie, które ma poprawić to pytanie.
ojrac

0

W niektórych dialektach, takich jak Microsoft SQL Server, PostgreSQL i Oracle (ale nie MySQL lub SQLite), możesz zrobić coś takiego

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

Powodem, dla którego nie wystarczy (10000 rows)obejść się bez tego, topjest to, że TABLESAMPLElogika daje bardzo niedokładną liczbę wierszy (np. 75% tej, czasami 1,25% razy więcej), więc chcesz przesadzić i wybrać dokładną liczbę, którą chcesz. Służy REPEATABLE (123)do dostarczania losowego ziarna.


-4

Może mógłbyś to zrobić

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

1
Wygląda na to, że wybrano losowy wycinek moich danych; Szukam czegoś bardziej skomplikowanego - 10000 losowo rozmieszczonych wierszy.
ojrac

Wtedy jedyną opcją, jeśli chcesz to zrobić w bazie danych, jest ORDER BY rand ().
staticsan
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.