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 noid
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.
-
Cadaid
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 useDISTINCT
).
-
Junte-se aoid
s para a mesa grande. Isso deve ser muito rápido com o índice no lugar.
-
Por fim, corte oid
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:
OBERNOULLI
eSYSTEM
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 qualquerreal
expressão valorizada.
Minha ênfase em negrito. É muito rápido , mas o resultado não é exatamente aleatório . O manual novamente:
OSYSTEM
é significativamente mais rápido que oBERNOULLI
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.