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?

Author: Eric Leschinski, 2011-12-30

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żyj DISTINCT).

  • 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ż metoda BERNOULLI 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:

Metody pobierania próbek BERNOULLI i SYSTEM 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ści real.

/ Align = "left" /

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.
 166
Author: Erwin Brandstetter,
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ń.

I linkowany artykuł depeza opisujący kilka innych podejść:


1 "duże" jak w "cała tabela nie zmieści się w pamięci".

 76
Author: A.H.,
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
 59
Author: Eric Leschinski,
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

 28
Author: Mickaël Le Baillif,
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.

 23
Author: Donald Miner,
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;
 9
Author: Bogdan Surai,
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);
 5
Author: Evan Carroll,
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));
 4
Author: Nelo Mitranim,
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.

 2
Author: Raman,
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.

 0
Author: MK Yung,
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
 0
Author: Daniel Gerber,
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