Dadas as suas especificações (além de informações adicionais nos comentários),
- Você tem uma coluna de ID numérica (números inteiros) com apenas algumas (ou moderadamente poucas) lacunas.
- Obviamente, nenhuma ou poucas operações de gravação.
- Sua coluna de ID deve ser indexada! Uma chave primária serve muito bem.
A consulta abaixo não precisa de uma varredura seqüencial 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 é a count(*)
(para mesas enormes). Dadas as especificações acima, você não precisa disso. Uma estimativa funcionará perfeitamente, 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 seja muito menor 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 um id
pode ser escolhido várias vezes por acaso (embora seja muito improvável com um grande espaço de identificação), portanto, agrupe os números gerados (ou use DISTINCT
).
Junte os id
s à mesa grande. Isso deve ser muito rápido com o índice no lugar.
Finalmente, apare os excedentes id
que não foram consumidos por burros e lacunas. Cada linha tem uma chance completamente igual de ser escolhida.
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
)
SELECT *
FROM random_pick
LIMIT 1000; -- actual limit
Podemos trabalhar com um excedente menor na consulta base. Se houver muitas lacunas para não encontrar linhas suficientes na primeira iteração, o rCTE continuará a iterar com o termo recursivo. Ainda precisamos de relativamente poucas lacunas no espaço de identificação ou a recursão pode ficar seca antes que o limite seja atingido - ou precisamos começar com um buffer grande o suficiente que desafie o objetivo de otimizar o desempenho.
As duplicatas são eliminadas pelo UNION
no rCTE.
O externo LIMIT
faz o CTE parar assim que tivermos linhas suficientes.
Essa consulta é cuidadosamente elaborada para usar o índice disponível, gerar linhas realmente aleatórias e não parar até cumprirmos o limite (a menos que a recursão se esgote). Existem várias armadilhas aqui, se você deseja reescrevê-lo.
Enrole em função
Para uso repetido com vários parâmetros:
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;
Ligar:
SELECT * FROM f_random_sample();
SELECT * FROM f_random_sample(500, 1.05);
Você pode até fazer esse genérico funcionar em 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. Vejo:
Alternativa possível
Se seus requisitos permitirem conjuntos idênticos para chamadas repetidas (e estamos falando de chamadas repetidas), 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.
Onde n
está uma porcentagem. O manual:
O BERNOULLI
eSYSTEM
métodos amostragem aceitam um único argumento, que é a fração da tabela a ser amostrada, expressa como uma
porcentagem entre 0 e 100 . Este argumento pode ser qualquer real
expressão com valor.
Negrito ênfase minha. É muito rápido , mas o resultado não é exatamente aleatório . O manual novamente:
o SYSTEM
método é significativamente mais rápido que o BERNOULLI
método quando pequenas porcentagens de amostragem são especificadas, mas pode retornar uma amostra menos aleatória da tabela como resultado de efeitos de cluster.
O número de linhas retornadas pode variar bastante. Para o nosso exemplo, para obter aproximadamente 1000 linhas:
SELECT * FROM big TABLESAMPLE SYSTEM ((1000 * 100) / 5100000.0);
Relacionado:
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);
Vejo a resposta de Evan para detalhes.
Mas isso ainda não é exatamente aleatório.