sqlpostgresqlrandomsampling

RANDOM() no SQL: amostragem aleatoria, inteiros e seed

Como o RANDOM() funciona no PostgreSQL, por que ORDER BY RANDOM() e lento em escala e o que usar no lugar.

2 min de leituraReferencesql · postgresql · random · sampling · mysql

RANDOM() retorna um valor pseudoaleatorio de ponto flutuante em [0, 1) e esta por tras de quase toda consulta "aleatoria" que voce escreve: escolher um usuario de amostra, distribuir grupos A/B ou embaralhar linhas. Vamos ver como usa-lo bem e onde ele vira um gargalo.

O valor base e linhas aleatorias

No PostgreSQL, RANDOM() gera a cada chamada um numero novo de 0 (incluso) ate, mas sem incluir, 1:

SELECT random();              -- e.g. 0.8473920192
SELECT random() AS r1, random() AS r2;  -- two different values

A forma classica de pegar linhas aleatorias e ordenar por RANDOM() e tomar o topo:

SELECT id, email, country
FROM users
ORDER BY random()
LIMIT 5;

Cada linha recebe uma chave de ordenacao aleatoria, entao o resultado muda a cada execucao. Em tabelas pequenas isso fica legivel e responde na hora.

Por que ORDER BY RANDOM() e custoso

O pulo do gato e o custo. Para ordenar por RANDOM(), o motor precisa atribuir um numero aleatorio a cada linha e ordenar a tabela inteira antes de aplicar o LIMIT. Em milhoes de linhas isso e um scan completo mais uma ordenacao em disco.

  • Um indice sobre a expressao nao ajuda: a chave muda a cada chamada.
  • O custo cresce com o tamanho da tabela, nao com o LIMIT.

Para tabelas grandes, use TABLESAMPLE, que le paginas aleatorias em vez de ordenar tudo:

-- roughly 5% of physical pages, very cheap
SELECT id, amount, status
FROM orders TABLESAMPLE BERNOULLI (5);

-- block sampling: even faster, coarser
SELECT * FROM orders TABLESAMPLE SYSTEM (1);

Atencao: TABLESAMPLE devolve uma porcentagem aproximada, nao um numero exato de linhas, e amostra no nivel de pagina, entao a distribuicao nao e perfeitamente uniforme. Quando voce precisa de exatamente N linhas, combine os dois: amostre de forma grosseira e depois refine.

SELECT id, email
FROM users TABLESAMPLE SYSTEM (10)
ORDER BY random()
LIMIT 5;

Inteiros aleatorios

RANDOM() da uma fracao, mas muitas vezes voce precisa de um int num intervalo. A formula e floor(random() * n) para [0, n):

-- integer in [0, 100)
SELECT floor(random() * 100)::int AS n;

-- integer in [1, 6] like a dice roll
SELECT floor(random() * 6)::int + 1 AS dice;

Um uso pratico e espalhar funcionarios em baldes aleatorios ou atribuir um desconto aleatorio:

SELECT id, name, dept,
       floor(random() * 3)::int AS shard
FROM employees;

Atencao: nao escreva round(random() * n) para um inteiro uniforme. round puxa para as pontas a metade das vezes, entao as bordas (0 e n) recebem metade da probabilidade. Use sempre floor.

Reprodutibilidade com setseed

A aleatoriedade atrapalha nos testes: o resultado muda a cada execucao. setseed() fixa o gerador para a sessao atual, recebendo um valor em [-1, 1]:

SELECT setseed(0.42);
SELECT id FROM users ORDER BY random() LIMIT 3;  -- same order on replay

Depois de setseed, a sequencia de RANDOM() e deterministica ate o proximo setseed ou o fim da sessao, o que e util para testes e demos repetiveis.

MySQL e ClickHouse

A sintaxe varia entre os motores:

  • MySQL usa RAND(), tambem [0, 1). A amostragem e ORDER BY RAND() LIMIT 5; nao existe TABLESAMPLE. A seed vai como argumento: RAND(42).
  • ClickHouse tem rand(), que e um UInt32 em [0, 2^32), nao uma fracao. Para [0,1) divida por 4294967295.0; para amostrar ha um SELECT ... SAMPLE 0.1 nativo.
-- MySQL: random rows + reproducible seed
SELECT id, email FROM users ORDER BY RAND(42) LIMIT 5;

-- ClickHouse: native sampling, no full sort
SELECT id, amount FROM orders SAMPLE 0.1;

A licao: ORDER BY RANDOM() e conveniente e correto, mas nao escala. Com dados grandes, migre para amostragem por paginas e reserve setseed/RAND(seed) para uma saida reproduzivel.

Pratique com exercícios reais

Resolva exercícios no treinador de SQL com correção instantânea e dicas.

Abrir o treinador