Najlepszy sposób na wybranie losowych wierszy PostgreSQL
Chcę losowego wyboru wierszy w PostgreSQL, próbowałem tego:
select * from table where random() < 0.01;
Ale inne polecają to:
select * from table order by random() limit 1000;
Mam bardzo dużą tabelę z 500 milionami wierszy, chcę, aby była szybka.
Które podejście jest lepsze? Jakie są różnice? Jaki jest najlepszy sposób na wybranie losowych wierszy?
11 answers
Biorąc pod uwagę twoje specyfikacje (plus dodatkowe informacje w komentarzach),
- masz kolumnę numeryczną (liczby całkowite) z tylko kilkoma (lub umiarkowanie niewielkimi) przerwami.
- oczywiście brak lub kilka operacji zapisu.
- twoja kolumna ID musi być indeksowana! Klucz główny dobrze służy.
Poniższe zapytanie nie wymaga sekwencyjnego skanowania dużej tabeli, tylko skanowania indeksu.
Najpierw uzyskaj szacunki dla głównego zapytania:
SELECT count(*) AS ct -- optional
, min(id) AS min_id
, max(id) AS max_id
, max(id) - min(id) AS id_span
FROM big;
Jedyny prawdopodobnie kosztowną częścią jest count(*)
(dla ogromnych stołów). Biorąc pod uwagę powyższe specyfikacje, nie potrzebujesz go. Kosztorys będzie w porządku, dostępny prawie bez kosztów (szczegółowe wyjaśnienie tutaj):
SELECT reltuples AS ct FROM pg_class WHERE oid = 'schema_name.big'::regclass;
Tak długo, jak ct
nie jest dużo mniejszy niż id_span
, zapytanie będzie przewyższać inne podejścia.
WITH params AS (
SELECT 1 AS min_id -- minimum id <= current min id
, 5100000 AS id_span -- rounded up. (max_id - min_id + buffer)
)
SELECT *
FROM (
SELECT p.min_id + trunc(random() * p.id_span)::integer AS id
FROM params p
,generate_series(1, 1100) g -- 1000 + buffer
GROUP BY 1 -- trim duplicates
) r
JOIN big USING (id)
LIMIT 1000; -- trim surplus
Generowanie liczb losowych w przestrzeni
id
. Masz "kilka luk", więc dodaj 10 % (wystarczająco, aby łatwo pokryć puste miejsca) do liczby wiersze do pobrania.Każda
id
może być wybrana kilka razy przez przypadek( choć bardzo mało prawdopodobne z dużą spacją id), więc Grupuj wygenerowane liczby (lub użyjDISTINCT
).Dołącz do
id
s do dużego stołu. Powinno to być bardzo szybkie z indeksem na miejscu.Wreszcie wykończyć nadwyżki
id
S, które nie zostały zjedzone przez dupki i luki. Każdy rząd ma całkowicie równe szanse do wyboru.
Krótki wersja
Możesz uprościć to zapytanie. CTE w powyższym zapytaniu jest tylko do celów edukacyjnych:
SELECT *
FROM (
SELECT DISTINCT 1 + trunc(random() * 5100000)::integer AS id
FROM generate_series(1, 1100) g
) r
JOIN big USING (id)
LIMIT 1000;
Udoskonalaj za pomocą rCTE
Szczególnie, jeśli nie jesteś taki pewien luk i szacunków.
WITH RECURSIVE random_pick AS (
SELECT *
FROM (
SELECT 1 + trunc(random() * 5100000)::int AS id
FROM generate_series(1, 1030) -- 1000 + few percent - adapt to your needs
LIMIT 1030 -- hint for query planner
) r
JOIN big b USING (id) -- eliminate miss
UNION -- eliminate dupe
SELECT b.*
FROM (
SELECT 1 + trunc(random() * 5100000)::int AS id
FROM random_pick r -- plus 3 percent - adapt to your needs
LIMIT 999 -- less than 1000, hint for query planner
) r
JOIN big b USING (id) -- eliminate miss
)
SELECT *
FROM random_pick
LIMIT 1000; -- actual limit
Możemy pracować z mniejsza nadwyżka w zapytaniu podstawowym. Jeśli jest zbyt wiele luk, więc nie znajdziemy wystarczającej liczby wierszy w pierwszej iteracji, rCTE kontynuuje iterację z terminem rekurencyjnym. Nadal potrzebujemy stosunkowo kilka luk w przestrzeni ID lub rekurencja może wyschnąć przed osiągnięciem limitu - lub musimy zacząć od wystarczająco dużego bufora, który nie służy optymalizacji wydajności.
Duplikaty są eliminowane przez UNION
w rCTE.
Zewnętrzny LIMIT
sprawia, że CTE zatrzymuje się, gdy tylko mamy wystarczającą ilość wierszy.
To zapytanie jest starannie przygotowane, aby korzystać z dostępnego indeksu, generować faktycznie losowe wiersze i nie zatrzymywać się, dopóki nie wypełnimy limitu (chyba że recursion runs dry). Istnieje wiele pułapek tutaj, jeśli masz zamiar go przepisać.
Zawiń w funkcję
Do wielokrotnego użytku o różnych parametrach:
CREATE OR REPLACE FUNCTION f_random_sample(_limit int = 1000, _gaps real = 1.03)
RETURNS SETOF big AS
$func$
DECLARE
_surplus int := _limit * _gaps;
_estimate int := ( -- get current estimate from system
SELECT c.reltuples * _gaps
FROM pg_class c
WHERE c.oid = 'big'::regclass);
BEGIN
RETURN QUERY
WITH RECURSIVE random_pick AS (
SELECT *
FROM (
SELECT 1 + trunc(random() * _estimate)::int
FROM generate_series(1, _surplus) g
LIMIT _surplus -- hint for query planner
) r (id)
JOIN big USING (id) -- eliminate misses
UNION -- eliminate dupes
SELECT *
FROM (
SELECT 1 + trunc(random() * _estimate)::int
FROM random_pick -- just to make it recursive
LIMIT _limit -- hint for query planner
) r (id)
JOIN big USING (id) -- eliminate misses
)
SELECT *
FROM random_pick
LIMIT _limit;
END
$func$ LANGUAGE plpgsql VOLATILE ROWS 1000;
Wywołanie:
SELECT * FROM f_random_sample();
SELECT * FROM f_random_sample(500, 1.05);
Możesz nawet sprawić, by ten rodzajnik działał dla dowolnej tabeli: weź nazwę kolumny PK i tabelę jako typ polimorficzny i użyj EXECUTE
... Ale to wykracza poza zakres tego pytania. Zobacz:
Możliwa alternatywa
Jeśli twoje wymagania dopuszczają identyczne zestawy dla powtarzających się wywołań (a mówimy o powtarzających się wywołaniach), rozważyłbym zmaterializowany widok. Wykonaj powyższe zapytanie raz i zapisz wynik do tabeli. Użytkownicy otrzymują quasi losowy wybór przy prędkości rozjaśniania. Odśwież swój losowy wybór w odstępach czasu lub w wybranych przez Ciebie wydarzeniach.
Postgres 9.5 wprowadza TABLESAMPLE SYSTEM (n)
To jest bardzo szybkie , ale wynik jest nie do końca przypadkowy . Instrukcja:
Metoda
SYSTEM
jest znacznie szybsza niż metodaBERNOULLI
gdy podano małe procenty pobierania próbek, ale może to zwrócić less-losowa próbka tabeli wynikająca z efektów klastrowania.
I liczba zwracanych wierszy może się bardzo różnić. Dla naszego przykładu, aby uzyskać mniej więcej 1000 wierszy, spróbuj:
SELECT * FROM big TABLESAMPLE SYSTEM ((1000 * 100) / 5100000.0);
Gdzie n jest procentem. Instrukcja:
/ Align = "left" /Metody pobierania próbek
BERNOULLI
iSYSTEM
akceptują pojedynczą argument będący ułamkiem tabeli do próby, wyrażony jako procent od 0 do 100 . Argument ten może być dowolnym wyrażeniem o wartościreal
.
Powiązane:
Lub zainstalować dodatkowy moduł tsm_system_rows aby uzyskać dokładną liczbę żądanych wierszy (jeśli jest ich wystarczająco dużo) i pozwolić na wygodniejszą składnię:
SELECT * FROM big TABLESAMPLE SYSTEM_ROWS(1000);
Zobacz odpowiedź Evana Po szczegóły.
Ale to wciąż nie jest przypadkowe.Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-05-23 12:26:34
Możesz sprawdzić i porównać plan wykonania obu za pomocą
EXPLAIN select * from table where random() < 0.01;
EXPLAIN select * from table order by random() limit 1000;
Szybki test na dużym stole1 pokazuje, że ORDER BY
najpierw sortuje kompletną tabelę, a następnie wybiera pierwsze 1000 pozycji. Sortowanie dużej tabeli nie tylko odczytuje tę tabelę, ale także wymaga odczytu i zapisu plików tymczasowych. where random() < 0.1
skanuje całą tabelę tylko raz.
W przypadku dużych tabel może to nie być to, czego chcesz, ponieważ nawet jedno pełne skanowanie tabeli może trwać długo.
Trzecia propozycja to
select * from table where random() < 0.01 limit 1000;
Ten zatrzymuje skanowanie tabeli, gdy tylko znaleziono 1000 wierszy i dlatego zwraca je wcześniej. Oczywiście to trochę psuje przypadkowość, ale być może jest to wystarczająco dobre w Twoim przypadku.
Edit: oprócz tych rozważań, możesz sprawdzić już zadane pytania. Użycie zapytania [postgresql] random
zwraca sporo trafień.
- szybki losowy wybór wierszy w Postgres
- Jak odzyskać losowe wiersze danych z tabeli postgreSQL?
- postgres: pobieraj losowe wpisy z tabeli-zbyt wolno
I linkowany artykuł depeza opisujący kilka innych podejść:
1 "duże" jak w "cała tabela nie zmieści się w pamięci".
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-05-23 12:26:34
Kolejność Postgresql według random (), wybierz wiersze w losowej kolejności:
select your_columns from your_table ORDER BY random()
Kolejność Postgresql przez random() z odrębnym:
select * from
(select distinct your_columns from your_table) table_alias
ORDER BY random()
Kolejność Postgresql według losowego limitu jednego wiersza:
select your_columns from your_table ORDER BY random() limit 1
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2015-05-05 15:15:10
Począwszy od PostgreSQL 9.5, pojawiła się nowa składnia poświęcona pobieraniu losowych elementów z tabeli:
SELECT * FROM mytable TABLESAMPLE SYSTEM (5);
Ten przykład da ci 5% elementów z mytable
.
Zobacz więcej wyjaśnień w tym wpisie na blogu: http://www.postgresql.org/docs/current/static/sql-select.html
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-01-25 10:24:28
Ten z kolejnością będzie wolniejszy.
select * from table where random() < 0.01;
idzie rekord po rekordzie i decyduje się losowo filtrować, czy nie. To będzie O(N)
, ponieważ musi sprawdzić każdy rekord tylko raz.
select * from table order by random() limit 1000;
będzie posortować cały stół, a następnie wybrać pierwsze 1000. Pomijając magię voodoo za kulisami, kolejność według jest O(N * log N)
.
Minusem random() < 0.01
jest to, że otrzymasz zmienną liczbę rekordów wyjściowych.
Uwaga, Istnieje lepszy sposób na tasowanie zbioru danych niż sortowanie losowe: The Fisher-Yates Shuffle , który działa w O(N)
. Implementacja shuffle w SQL brzmi jak nie lada wyzwanie.
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2011-12-29 23:46:46
Oto decyzja, która mi odpowiada. Myślę, że jest to bardzo proste do zrozumienia i wykonania.
SELECT
field_1,
field_2,
field_2,
random() as ordering
FROM
big_table
WHERE
some_conditions
ORDER BY
ordering
LIMIT 1000;
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-06-01 11:34:40
select * from table order by random() limit 1000;
Jeśli wiesz, ile wierszy chcesz, sprawdź tsm_system_rows
.
Tsm_system_rows
Moduł dostarcza metodę table sampling system_rows, która może być użyta w klauzuli TABLESAMPLE polecenia SELECT.
Ta metoda próbkowania tabeli akceptuje pojedynczy argument całkowity, który jest maksymalną liczbą wierszy do odczytania. Wynikowa próbka zawsze będzie zawierać dokładnie tyle wierszy, chyba że tabela nie zawiera wystarczającej liczby wierszy, w których wielkość liter zaznaczona jest cała tabela. podobnie jak wbudowana metoda próbkowania systemu, SYSTEM_ROWS wykonuje próbkowanie na poziomie bloków, tak że próbka nie jest całkowicie losowa, ale może podlegać efektom klastrowania, zwłaszcza jeśli wymagana jest tylko niewielka liczba wierszy.
Najpierw zainstaluj rozszerzenie
CREATE EXTENSION tsm_system_rows;
Then your query,
SELECT *
FROM table
TABLESAMPLE SYSTEM_ROWS(1000);
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-12-27 01:03:24
Jeśli chcesz tylko jeden wiersz, możesz użyć obliczonej offset
pochodzącej z count
.
select * from table_name limit 1
offset floor(random() * (select count(*) from table_name));
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2015-09-12 09:16:56
Istnieje możliwość zmiany zmaterializowanego widoku "Possible alternative" przedstawionego przez Erwina Brandstettera.
Powiedzmy na przykład, że nie chcesz duplikatów w losowych wartościach, które są zwracane. Musisz więc ustawić wartość logiczną w tabeli podstawowej zawierającą Twój (niezwiązany z randomizacją) zestaw wartości.
Zakładając, że jest to tabela wejściowa:
id_values id | used
----+--------
1 | FALSE
2 | FALSE
3 | FALSE
4 | FALSE
5 | FALSE
...
Uzupełnij tabelę ID_VALUES
w razie potrzeby. Następnie, zgodnie z opisem Erwina, Utwórz zmaterializowany widok który losuje ID_VALUES
tabelę raz:
CREATE MATERIALIZED VIEW id_values_randomized AS
SELECT id
FROM id_values
ORDER BY random();
Zauważ, że zmaterializowany widok nie zawiera używanej kolumny, ponieważ szybko stanie się Nieaktualny. Widok nie musi też zawierać innych kolumn, które mogą znajdować się w tabeli id_values
.
Aby uzyskać (i" zużywać") losowe wartości, użyj UPDATE-return on id_values
, wybierając id_values
z id_values_randomized
z łącznikiem i stosując pożądane kryteria, aby uzyskać tylko odpowiednie możliwości. Na przykład:
UPDATE id_values
SET used = TRUE
WHERE id_values.id IN
(SELECT i.id
FROM id_values_randomized r INNER JOIN id_values i ON i.id = r.id
WHERE (NOT i.used)
LIMIT 5)
RETURNING id;
Zmień LIMIT
w razie potrzeby -- jeśli potrzebujesz tylko jednej wartości losowej na raz, Zmień LIMIT
na 1
.
Z odpowiednimi indeksami na id_values
, uważam, że UPDATE-Return powinien działać bardzo szybko przy niewielkim obciążeniu. Zwraca randomizowane wartości z jedną bazą danych w obie strony. Kryteria dla" kwalifikujących się " wierszy mogą być tak złożone, jak jest to wymagane. Nowe wiersze mogą być dodawane do tabeli id_values
w dowolnym momencie, a staną się one dostępne dla aplikacji, gdy tylko zmaterializowany widok jest odświeżany(co może być uruchamiane w czasie poza szczytem). Tworzenie i odświeżanie zmaterializowanego widoku będzie powolne, ale musi być wykonywane tylko wtedy, gdy nowe identyfikatory zostaną dodane do Tabeli id_values
.
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-05-23 12:10:47
Dodaj kolumnę o nazwie r
z typem serial
. Indeks r
.
Załóżmy, że mamy 200 000 wierszy, będziemy generować liczbę losową n
, gdzie 0 n
Wybierz wiersze za pomocą r > n
, posortuj je ASC
i wybierz najmniejszy.
Kod:
select * from YOUR_TABLE
where r > (
select (
select reltuples::bigint AS estimate
from pg_class
where oid = 'public.YOUR_TABLE'::regclass) * random()
)
order by r asc limit(1);
Kod jest oczywisty. Subquery w środku służy do szybkiego oszacowania liczby wierszy tabeli z https://stackoverflow.com/a/7945274/1271094 .
W aplikacji poziom polecenie należy wykonać ponownie, jeśli n
> ilość wierszy lub trzeba wybrać wiele wierszy.
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-05-23 12:10:47
Wiem, że jestem trochę spóźniony na imprezę, ale właśnie znalazłem to niesamowite narzędzie o nazwie pg_sample :
pg_sample
- wyodrębnij mały, przykładowy zestaw danych z większej bazy danych PostgreSQL, zachowując integralność odniesienia.
Próbowałem tego z bazą 350m wierszy i było naprawdę szybko, Nie wiem o randomness.
./pg_sample --limit="small_table = *" --limit="large_table = 100000" -U postgres source_db | psql -U postgres target_db
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-05-30 08:25:05