PostgreSQL
 sql >> Base de Dados >  >> RDS >> PostgreSQL

Melhor maneira de selecionar linhas aleatórias PostgreSQL


Dadas as suas especificações (mais informações adicionais nos comentários),
  • Você tem uma coluna de código numérico (números inteiros) com apenas poucas (ou moderadamente poucas) lacunas.
  • Obviamente nenhuma ou poucas operações de gravação.
  • Sua coluna de ID deve ser indexada! Uma chave primária funciona bem.

A consulta abaixo não precisa de uma varredura sequencial da tabela grande, apenas uma varredura de índice.

Primeiro, obtenha estimativas para a consulta principal:
SELECT count(*) AS ct              -- optional
     , min(id)  AS min_id
     , max(id)  AS max_id
     , max(id) - min(id) AS id_span
FROM   big;

A única parte possivelmente cara é o count(*) (para mesas enormes). Dadas as especificações acima, você não precisa disso. Uma estimativa funcionará muito bem, disponível quase sem custo (explicação detalhada aqui):
SELECT reltuples AS ct FROM pg_class
WHERE oid = 'schema_name.big'::regclass;

Desde que ct não é muito menor que id_span , a consulta superará outras abordagens.
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

  • Gere números aleatórios no id espaço. Você tem "poucas lacunas", então adicione 10% (o suficiente para cobrir facilmente os espaços em branco) ao número de linhas a serem recuperadas.

  • Cada id pode ser escolhido várias vezes por acaso (embora muito improvável com um grande espaço de id), então agrupe os números gerados (ou use DISTINCT ).

  • Junte-se ao id s para a mesa grande. Isso deve ser muito rápido com o índice no lugar.

  • Por fim, corte o id excedente s que não foram comidos por tolos e lacunas. Cada linha tem uma chance completamente igual ser escolhido.

Versão curta


Você pode simplificar esta consulta. O CTE na consulta acima é apenas para fins educacionais:
SELECT *
FROM  (
   SELECT DISTINCT 1 + trunc(random() * 5100000)::integer AS id
   FROM   generate_series(1, 1100) g
   ) r
JOIN   big USING (id)
LIMIT  1000;

Refinar com rCTE


Especialmente se você não tiver tanta certeza sobre lacunas e estimativas.
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

Podemos trabalhar com um excedente menor na consulta básica. Se houver muitas lacunas e não encontrarmos linhas suficientes na primeira iteração, o rCTE continuará a iterar com o termo recursivo. Ainda precisamos de relativamente poucos lacunas no espaço de ID ou a recursão pode secar antes que o limite seja atingido - ou temos que começar com um buffer grande o suficiente que desafia o propósito de otimizar o desempenho.

As duplicatas são eliminadas pelo UNION no rCTE.

O LIMIT externo faz com que o CTE pare assim que tivermos linhas suficientes.

Esta consulta é cuidadosamente elaborada para usar o índice disponível, gerar linhas realmente aleatórias e não parar até atingirmos o limite (a menos que a recursão se esgote). Há uma série de armadilhas aqui se você for reescrevê-lo.

Agrupar na função


Para uso repetido com parâmetros variados:
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$;

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

Você pode até tornar isso genérico para funcionar para qualquer tabela:Pegue o nome da coluna PK e a tabela como tipo polimórfico e use EXECUTE ... Mas isso está além do escopo desta pergunta. Ver:
  • Refatorar uma função PL/pgSQL para retornar a saída de várias consultas SELECT

Possível alternativa


SE seus requisitos permitirem conjuntos idênticos para repetidos chamadas (e estamos falando de chamadas repetidas) eu consideraria uma visão materializada . Execute a consulta acima uma vez e escreva o resultado em uma tabela. Os usuários obtêm uma seleção quase aleatória na velocidade da luz. Atualize sua escolha aleatória em intervalos ou eventos de sua escolha.

O Postgres 9.5 introduz o TABLESAMPLE SYSTEM (n)


Onde n é uma porcentagem. O manual:

O BERNOULLI e SYSTEM cada um dos métodos de amostragem aceita um único argumento que é a fração da tabela a ser amostrada, expressa como umaporcentagem entre 0 e 100 . Este argumento pode ser qualquer real expressão valorizada.

Minha ênfase em negrito. É muito rápido , mas o resultado não é exatamente aleatório . O manual novamente:

O SYSTEM é significativamente mais rápido que o BERNOULLI methodquando pequenas porcentagens de amostragem são especificadas, mas pode retornar uma amostra menos aleatória da tabela como resultado de efeitos de agrupamento.

O número de linhas retornadas pode variar muito. Para o nosso exemplo, para obter aproximadamente 1000 linhas:
SELECT * FROM big TABLESAMPLE SYSTEM ((1000 * 100) / 5100000.0);

Relacionado:
  • Maneira rápida de descobrir a contagem de linhas de uma tabela no PostgreSQL

Ou instale o módulo adicional tsm_system_rows para obter exatamente o número de linhas solicitadas (se houver o suficiente) e permitir a sintaxe mais conveniente:
SELECT * FROM big TABLESAMPLE SYSTEM_ROWS(1000);

Veja a resposta de Evan para detalhes.

Mas isso ainda não é exatamente aleatório.