PostgreSQL
 sql >> Baza danych >  >> RDS >> PostgreSQL

Najlepszy sposób na losowe wybieranie wierszy PostgreSQL

Biorąc pod uwagę twoje specyfikacje (plus dodatkowe informacje w komentarzach),

  • Masz kolumnę numerycznego identyfikatora (liczby całkowite) z tylko kilkoma (lub umiarkowanie kilkoma) przerwami.
  • Oczywiście brak lub kilka operacji zapisu.
  • Twoja kolumna ID musi być zindeksowana! Klucz podstawowy 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;

Jedyną potencjalnie kosztowną częścią jest count(*) (dla ogromnych stołów). Biorąc pod uwagę powyższe specyfikacje, nie potrzebujesz tego. Szacunek będzie w porządku, dostępny prawie za darmo (szczegółowe wyjaśnienie tutaj):

SELECT reltuples AS ct FROM pg_class
WHERE oid = 'schema_name.big'::regclass;

Tak długo, jak ct to nie dużo mniejszy niż id_span , zapytanie będzie lepsze niż 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
  • Generuj losowe liczby w id przestrzeń. Masz „kilka luk”, więc dodaj 10% (wystarczająco, aby łatwo zakryć puste miejsca) do liczby rzędów do pobrania.

  • Każdy id mogą być wybierane wielokrotnie przez przypadek (choć bardzo mało prawdopodobne przy dużej przestrzeni identyfikatora), więc zgrupuj wygenerowane liczby (lub użyj DISTINCT ).

  • Dołącz do id s do dużego stołu. Powinno to nastąpić bardzo szybko z włożonym indeksem.

  • Na koniec przytnij nadmiar id s, które nie zostały zjedzone przez naśladowców i luki. Każdy rząd ma całkowicie równe szanse do wyboru.

Krótka wersja

Możesz uprościć to zapytanie. CTE w powyższym zapytaniu służy 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;

Doprecyzuj za pomocą rCTE

Zwłaszcza jeśli nie masz pewności co do 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
   )
TABLE  random_pick
LIMIT  1000;  -- actual limit

Możemy pracować z mniejszą nadwyżką 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 niewielu luki w przestrzeni ID lub rekurencja mogą wyschnąć przed osiągnięciem limitu - lub musimy zacząć od wystarczająco dużego bufora, który jest sprzeczny z celem optymalizacji wydajności.

Duplikaty są eliminowane przez UNION w rCTE.

Zewnętrzny LIMIT powoduje zatrzymanie CTE, gdy tylko mamy wystarczającą liczbę wierszy.

To zapytanie jest starannie opracowane, aby korzystać z dostępnego indeksu, generować faktycznie losowe wiersze i nie zatrzymywać się, dopóki nie osiągniemy limitu (chyba że rekurencja wyschnie). Istnieje wiele pułapek, jeśli zamierzasz to przepisać.

Zawijaj w funkcję

Do wielokrotnego użytku z różnymi parametrami:

CREATE OR REPLACE FUNCTION f_random_sample(_limit int = 1000, _gaps real = 1.03)
  RETURNS SETOF big
  LANGUAGE plpgsql VOLATILE ROWS 1000 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
   )
   TABLE  random_pick
   LIMIT  _limit;
END
$func$;

Zadzwoń:

SELECT * FROM f_random_sample();
SELECT * FROM f_random_sample(500, 1.05);

Możesz nawet sprawić, by ta ogólna działała dla dowolnej tabeli:weź nazwę kolumny PK i tabeli jako typ polimorficzny i użyj EXECUTE ... Ale to wykracza poza zakres tego pytania. Zobacz:

  • Refaktoryzuj funkcję PL/pgSQL, aby zwrócić dane wyjściowe różnych zapytań SELECT

Możliwa alternatywa

JEŚLI Twoje wymagania pozwalają identyczne zestawy dla powtarzających się połączeń (i mówimy o powtarzających się połączeniach) Rozważałbym widok zmaterializowany . Wykonaj powyższe zapytanie raz i zapisz wynik do tabeli. Użytkownicy otrzymują quasi losowy wybór w błyskawicznym tempie. Odśwież swój losowy wybór w określonych odstępach czasu lub wydarzeniach.

Postgres 9.5 wprowadza TABLESAMPLE SYSTEM (n)

Gdzie n to procent. Instrukcja:

BERNOULLI i SYSTEM każda z metod próbkowania akceptuje pojedynczy argument, który jest ułamkiem tabeli do próbkowania, wyrażony jakoprocent od 0 do 100 . Ten argument może być dowolnym real wyrażenie wartościowe.

Moje odważne podkreślenie. Jest bardzo szybki , ale wynik jest niezupełnie przypadkowy . Instrukcja ponownie:

SYSTEM metoda jest znacznie szybsza niż BERNOULLI metodagdy określone są małe wartości procentowe próbkowania, ale może zwrócić mniej losową próbkę tabeli w wyniku efektów grupowania.

Liczba zwracanych wierszy może się bardzo różnić. Na przykład, aby uzyskać z grubsza 1000 wierszy:

SELECT * FROM big TABLESAMPLE SYSTEM ((1000 * 100) / 5100000.0);

Powiązane:

  • Szybki sposób na sprawdzenie liczby wierszy tabeli w PostgreSQL

Lub zainstaluj dodatkowy moduł tsm_system_rows aby uzyskać dokładną liczbę żądanych wierszy (jeśli jest ich wystarczająca) i zezwolić na wygodniejszą składnię:

SELECT * FROM big TABLESAMPLE SYSTEM_ROWS(1000);

Zobacz odpowiedź Evana, aby poznać szczegóły.

Ale to wciąż nie jest dokładnie losowe.



  1. Database
  2.   
  3. Mysql
  4.   
  5. Oracle
  6.   
  7. Sqlserver
  8.   
  9. PostgreSQL
  10.   
  11. Access
  12.   
  13. SQLite
  14.   
  15. MariaDB
  1. Postępy w aktualizacji online

  2. Opcjonalny argument w funkcji PL/pgSQL

  3. Rails:Instalacja PG gem na OS X - nie udało się zbudować natywnego rozszerzenia

  4. Samouczek PostgreSQL dla początkujących – wszystko, co musisz wiedzieć o PostgreSQL

  5. jak wykonać skrypt .sql na heroku?