sql >> Databasteknik >  >> RDS >> PostgreSQL

Bästa sättet att välja slumpmässiga rader PostgreSQL

Med tanke på dina specifikationer (plus ytterligare information i kommentarerna),

  • Du har en numerisk ID-kolumn (heltal) med endast få (eller måttligt få) luckor.
  • Självklart inga eller få skrivoperationer.
  • Din ID-kolumn måste indexeras! En primärnyckel fungerar bra.

Frågan nedan behöver inte en sekventiell genomsökning av den stora tabellen, bara en indexskanning.

Få först uppskattningar för huvudfrågan:

SELECT count(*) AS ct              -- optional
     , min(id)  AS min_id
     , max(id)  AS max_id
     , max(id) - min(id) AS id_span
FROM   big;

Den enda möjligen dyra delen är count(*) (för stora bord). Med tanke på ovanstående specifikationer behöver du det inte. En uppskattning kommer att fungera bra, tillgänglig nästan utan kostnad (detaljerad förklaring här):

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

Så länge som ct är inte mycket mindre än id_span , kommer frågan att överträffa andra tillvägagångssätt.

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
  • Generera slumpmässiga tal i id Plats. Du har "få luckor", så lägg till 10 % (tillräckligt för att enkelt täcka de tomma områdena) till antalet rader som ska hämtas.

  • Varje id kan väljas flera gånger av en slump (dock mycket osannolikt med ett stort id-utrymme), så gruppera de genererade talen (eller använd DISTINCT ).

  • Gå med i id s till det stora bordet. Detta bör vara mycket snabbt med indexet på plats.

  • Slutligen trimma överskott id s som inte har blivit uppätna av duper och luckor. Varje rad har en helt lika chans ska plockas.

Kort version

Du kan förenkla denna fråga. CTE i frågan ovan är bara för utbildningsändamål:

SELECT *
FROM  (
   SELECT DISTINCT 1 + trunc(random() * 5100000)::integer AS id
   FROM   generate_series(1, 1100) g
   ) r
JOIN   big USING (id)
LIMIT  1000;

Förfina med rCTE

Speciellt om du inte är så säker på luckor och uppskattningar.

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

Vi kan arbeta med ett mindre överskott i basfrågan. Om det finns för många luckor så att vi inte hittar tillräckligt många rader i den första iterationen, fortsätter rCTE att iterera med den rekursiva termen. Vi behöver fortfarande relativt luckor i ID-utrymmet eller rekursionen kan torka innan gränsen nås - eller så måste vi börja med en tillräckligt stor buffert som trotsar syftet med att optimera prestandan.

Dubbletter elimineras av UNION i rCTE.

Den yttre LIMIT gör att CTE slutar så fort vi har tillräckligt med rader.

Den här frågan är noggrant utformad för att använda det tillgängliga indexet, generera faktiskt slumpmässiga rader och inte sluta förrän vi uppfyller gränsen (såvida inte rekursionen är torr). Det finns ett antal fallgropar här om du ska skriva om det.

Lägg in i funktion

För upprepad användning med varierande parametrar:

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$;

Ring:

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

Du kan till och med få detta generiskt att fungera för vilken tabell som helst:Ta namnet på PK-kolumnen och tabellen som polymorf typ och använd EXECUTE ... Men det ligger utanför ramen för denna fråga. Se:

  • Refaktorera en PL/pgSQL-funktion för att returnera utdata från olika SELECT-frågor

Möjligt alternativ

OM dina krav tillåter identiska uppsättningar för upprepade samtal (och vi pratar om upprepade samtal) Jag skulle överväga en materialiserad syn . Kör ovanstående fråga en gång och skriv resultatet till en tabell. Användare får ett nästan slumpmässigt urval med blixthastighet. Uppdatera ditt slumpmässiga val med intervaller eller evenemang som du väljer.

Postgres 9.5 introducerar TABLESAMPLE SYSTEM (n)

Där n är en procentsats. Manualen:

BERNOULLI och SYSTEM Samplingsmetoderna accepterar vart och ett av ett enda argument som är den del av tabellen som ska provas, uttryckt som enprocentandel mellan 0 och 100 . Detta argument kan vara vilken real som helst -värderade uttryck.

Djärv betoning min. Det är mycket snabbt , men resultatet är inte exakt slumpmässigt . Manualen igen:

SYSTEM metoden är betydligt snabbare än BERNOULLI metod när små samplingsprocenter anges, men den kan returnera ett mindre slumpmässigt urval av tabellen som ett resultat av klustringseffekter.

Antalet rader som returneras kan variera kraftigt. För vårt exempel, för att få ungefär 1000 rader:

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

Relaterat:

  • Snabbt sätt att upptäcka radantalet i en tabell i PostgreSQL

Eller installera tilläggsmodulen tsm_system_rows för att få exakt antalet begärda rader (om det finns tillräckligt många) och tillåta den mer bekväma syntaxen:

SELECT * FROM big TABLESAMPLE SYSTEM_ROWS(1000);

Se Evans svar för detaljer.

Men det är fortfarande inte helt slumpmässigt.



  1. Hur avrundar man ett genomsnitt till 2 decimaler i PostgreSQL?

  2. MySQL UNION-klausul

  3. Konvertera 'smalldatetime' till 'datetime' i SQL Server (T-SQL-exempel)

  4. Hur REGEXP_LIKE()-funktionen fungerar i MySQL