O TABLESAMPLE do PostgreSQL traz algumas vantagens a mais em comparação com outras formas tradicionais de obter tuplas aleatórias.
TABLESAMPLE
é uma cláusula SQL SELECT e fornece dois métodos de amostragem que são SYSTEM
e BERNOULLI
. Com a ajuda de TABLESAMPLE
podemos recuperar facilmente linhas aleatórias de uma tabela. Para ler mais sobre TABLESAMPLE, você pode verificar a postagem anterior do blog
. Nesta postagem do blog, falaremos sobre formas alternativas de obter linhas aleatórias. Como as pessoas selecionavam linhas aleatórias antes de
TABLESAMPLE
, quais são os prós e contras dos outros métodos e o que ganhamos com TABLESAMPLE
? Existem postagens incríveis no blog sobre como selecionar linhas aleatórias, você pode começar a ler as seguintes postagens no blog para obter uma compreensão profunda desse tópico.
Meus pensamentos de obter uma linha aleatória
Obtendo linhas aleatórias de uma tabela de banco de dados
random_agg()
Vamos comparar as formas tradicionais de obter linhas aleatórias de uma tabela com as novas formas fornecidas por TABLESAMPLE.
Antes do TABLESAMPLE
cláusula, havia 3 métodos comumente usados para selecionar aleatoriamente linhas de uma tabela.
1- Ordenar aleatoriamente()
Para fins de teste, precisamos criar uma tabela e colocar alguns dados dentro dela.
Vamos criar a tabela ts_test e inserir 1 milhão de linhas nela:
CREATE TABLE ts_test (
id SERIAL PRIMARY KEY,
title TEXT
);
INSERT INTO ts_test (title)
SELECT
'Record #' || i
FROM
generate_series(1, 1000000) i;
Considerando a seguinte instrução SQL para selecionar 10 linhas aleatórias:
SELECT * FROM ts_test ORDER BY random() LIMIT 10;
Faz com que o PostgreSQL execute uma varredura completa da tabela e também a ordenação. Portanto, esse método não é preferido para tabelas com grande número de linhas por motivos de desempenho.
Vamos analisar
EXPLAIN ANALYZE
saída desta consulta acima:random=# EXPLAIN ANALYZE SELECT * FROM ts_test ORDER BY random() LIMIT 10;
QUERY PLAN
--------------------------------------------------------------------------------------------------------------------------------
Limit (cost=33959.03..33959.05 rows=10 width=36) (actual time=1956.786..1956.807 rows=10 loops=1)
-> Sort (cost=33959.03..35981.18 rows=808863 width=36) (actual time=1956.780..1956.789 rows=10 loops=1)
Sort Key: (random())
Sort Method: top-N heapsort Memory: 25kB
-> Seq Scan on ts_test (cost=0.00..16479.79 rows=808863 width=36) (actual time=0.276..1001.109 rows=1000000 loops=1)
Planning time: 1.434 ms
Execution time: 1956.900 ms
(7 rows)
Como
EXPLAIN ANALYZE
aponta, selecionar 10 de 1 milhão de linhas levou quase 2 segundos. A consulta também usou a saída de random()
como a chave de classificação para ordenar os resultados. A classificação parece ser a tarefa mais demorada aqui. Vamos executar isso com o cenário usando TABLESAMPLE
. No PostgreSQL 9.5, para obter o número exato de linhas aleatoriamente, podemos usar o método de amostragem SYSTEM_ROWS. Fornecido pelo
tsm_system_rows
contrib, ele nos permite especificar quantas linhas devem ser retornadas por amostragem. Normalmente, apenas a porcentagem solicitada pode ser especificada com TABLESAMPLE SYSTEM
e BERNOULLI
métodos. Primeiro, devemos criar
tsm_system_rows
extensão para usar este método, pois tanto TABLESAMPLE SYSTEM
e TABLESAMPLE BERNOULLI
métodos não garantem que a porcentagem fornecida resultará no número esperado de linhas. Verifique o TABLESAMPLE anterior p
ost para lembrar por que eles funcionam assim. Vamos começar criando
tsm_system_rows
extensão:random=# CREATE EXTENSION tsm_system_rows;
CREATE EXTENSION
Agora vamos comparar “
ORDER BY random()
” EXPLAIN ANALYZE
saída com o EXPLAIN ANALYZE
saída de tsm_system_rows
consulta que retorna 10 linhas aleatórias de uma tabela de 1 milhão de linhas. random=# EXPLAIN ANALYZE SELECT * FROM ts_test TABLESAMPLE SYSTEM_ROWS(10);
QUERY PLAN
-------------------------------------------------------------------------------------------------------
Sample Scan on ts_test (cost=0.00..4.10 rows=10 width=18) (actual time=0.069..0.083 rows=10 loops=1)
Sampling: system_rows ('10'::bigint)
Planning time: 0.646 ms
Execution time: 0.159 ms
(4 rows)
A consulta inteira levou 0,159 ms. Este método de amostragem é extremamente rápido comparando com o método “
ORDER BY random()
” que levou 1956,9 ms. 2- Comparar com random()
O seguinte SQL nos permite recuperar linhas aleatórias com 10% de probabilidade
SELECT * FROM ts_test WHERE random() <= 0.1;
Esse método é mais rápido do que ordenar aleatoriamente porque não classifica as linhas selecionadas. Ele retornará a porcentagem aproximada de linhas da tabela, assim como
BERNOULLI
ou SYSTEM
TABLESAMPLE
métodos. Vamos verificar o
EXPLAIN ANALYZE
saída de random()
consulta acima:random=# EXPLAIN ANALYZE SELECT * FROM ts_test WHERE random() <= 0.1;
QUERY PLAN
------------------------------------------------------------------------------------------------------------------
Seq Scan on ts_test (cost=0.00..21369.00 rows=333333 width=18) (actual time=0.089..280.483 rows=100014 loops=1)
Filter: (random() <= '0.1'::double precision)
Rows Removed by Filter: 899986
Planning time: 0.704 ms
Execution time: 367.527 ms
(5 rows)
A consulta levou 367,5 ms. Muito melhor que
ORDER BY random()
. Mas é mais difícil controlar a contagem exata de linhas. Vamos comparar “random()
” EXPLAIN ANALYZE
saída com o EXPLAIN ANALYZE
saída de TABLESAMPLE BERNOULLI
consulta que retorna aproximadamente 10% de linhas aleatórias da tabela de 1 milhão de linhas. random=# EXPLAIN ANALYZE SELECT * FROM ts_test TABLESAMPLE BERNOULLI (10);
QUERY PLAN
--------------------------------------------------------------------------------------------------------------------
Sample Scan on ts_test (cost=0.00..7369.00 rows=100000 width=18) (actual time=0.015..147.002 rows=100155 loops=1)
Sampling: bernoulli ('10'::real)
Planning time: 0.076 ms
Execution time: 239.289 ms
(4 rows)
Demos 10 como parâmetro para
BERNOULLI
porque precisamos de 10% de todos os registros. Aqui podemos ver que o
BERNOULLI
O método levou 239,289 ms para ser executado. Esses dois métodos são bastante semelhantes em como funcionam, BERNOULLI
é um pouco mais rápido, pois a seleção aleatória é feita no nível inferior. Uma vantagem de usar BERNOULLI
comparado com WHERE random() <= 0.1
é o REPEATABLE
cláusula que descrevemos em TABLESAMPLE
anterior publicar. 3- Coluna extra com um valor aleatório
Esse método sugere adicionar uma nova coluna com valores atribuídos aleatoriamente, adicionar um índice a ela, realizar a classificação por essa coluna e, opcionalmente, atualizar seus valores periodicamente para randomizar a distribuição.
Esta estratégia permite uma amostragem aleatória repetível principalmente. Ele funciona muito mais rápido do que o primeiro método, mas exige um esforço para configurar pela primeira vez e resulta em um custo de desempenho nas operações de inserção, atualização e exclusão.
Vamos aplicar este método em nosso
ts_test
tabela. Primeiro, adicionaremos uma nova coluna chamada
randomcolumn
com o tipo de precisão dupla porque o random()
do PostgreSQL A função retorna um número com precisão dupla. random=# ALTER TABLE ts_test ADD COLUMN randomcolumn DOUBLE PRECISION;
ALTER TABLE
Em seguida, atualizaremos a nova coluna usando
random()
função. random=# \timing
Timing is on.
random=# BEGIN;
BEGIN
Time: 2.071 ms
random=# UPDATE ts_test SET randomcolumn = RANDOM();
UPDATE 1000000
Time: 8483.741 ms
random=# COMMIT;
COMMIT
Time: 2.615 ms
Esse método tem um custo inicial para criar uma nova coluna e preencher essa nova coluna com valores aleatórios (0,0-1,0), mas é um custo único. Neste exemplo, para 1 milhão de linhas, levou quase 8,5 segundos.
Vamos tentar observar se nossos resultados são reproduzíveis consultando 100 linhas com nosso novo método:
random=# SELECT * FROM ts_test ORDER BY randomcolumn LIMIT 100;
-------+---------------+----------------------
13522 | Record #13522 | 6.4261257648468e-08
671584 | Record #671584 | 6.4261257648468e-07
714012 | Record #714012 | 1.95764005184174e-06
162016 | Record #162016 | 3.44449654221535e-06
106867 | Record #106867 | 3.66196036338806e-06
865669 | Record #865669 | 3.96883115172386e-06
927 | Record #927 | 4.65428456664085e-06
526017 | Record #526017 | 4.65987250208855e-06
98338 | Record #98338 | 4.91179525852203e-06
769625 | Record #769625 | 4.91319224238396e-06
...
462484 | Record #462484 | 9.83504578471184e-05
(100 rows)
Quando executamos a consulta acima, geralmente obtemos o mesmo conjunto de resultados, mas isso não é garantido porque usamos
random()
função para preencher randomcolumn
valores e, neste caso, mais de uma coluna pode ter o mesmo valor. Para ter certeza de que obteremos os mesmos resultados para cada execução, devemos melhorar nossa consulta adicionando a coluna ID a ORDER BY
cláusula, desta forma garantimos que ORDER BY
A cláusula especifica um conjunto exclusivo de linhas, porque a coluna id possui um índice de chave primária. Agora vamos executar a consulta aprimorada abaixo:
random=# SELECT * FROM ts_test ORDER BY randomcolumn, id LIMIT 100;
id | title | randomcolumn
--------+----------------+----------------------
13522 | Record #13522 | 6.4261257648468e-08
671584 | Record #671584 | 6.4261257648468e-07
714012 | Record #714012 | 1.95764005184174e-06
162016 | Record #162016 | 3.44449654221535e-06
106867 | Record #106867 | 3.66196036338806e-06
865669 | Record #865669 | 3.96883115172386e-06
927 | Record #927 | 4.65428456664085e-06
526017 | Record #526017 | 4.65987250208855e-06
98338 | Record #98338 | 4.91179525852203e-06
769625 | Record #769625 | 4.91319224238396e-06
...
462484 | Record #462484 | 9.83504578471184e-05
(100 rows)
Agora temos certeza de que podemos obter uma amostra aleatória reproduzível usando este método.
É hora de ver
EXPLAIN ANALYZE
resultados da nossa consulta final:random=# EXPLAIN ANALYZE SELECT * FROM ts_test ORDER BY randomcolumn, id LIMIT 100;
QUERY PLAN
--------------------------------------------------------------------------------------------------------------------------------
Limit (cost=55571.28..55571.53 rows=100 width=26) (actual time=1951.811..1952.039 rows=100 loops=1)
-> Sort (cost=55571.28..58071.28 rows=1000000 width=26) (actual time=1951.804..1951.891 rows=100 loops=1)
Sort Key: randomcolumn, id
Sort Method: top-N heapsort Memory: 32kB
-> Seq Scan on ts_test (cost=0.00..17352.00 rows=1000000 width=26) (actual time=0.058..995.011 rows=1000000 loops=1)
Planning time: 0.481 ms
Execution time: 1952.215 ms
(7 rows)
Para comparar este método com
TABLESAMPLE
métodos, vamos escolher SYSTEM
método com REPEATABLE
opção, uma vez que este método nos dá resultados reprodutíveis. TABLESAMPLE
SYSTEM
basicamente faz amostragem no nível do bloco/página; ele lê e retorna páginas aleatórias do disco. Assim, fornece aleatoriedade no nível da página em vez do nível da tupla. É por isso que é difícil controlar exatamente a contagem de linhas recuperadas. Se não houver páginas selecionadas, podemos não obter nenhum resultado. A amostragem no nível da página também é propensa ao efeito de agrupamento. TABLESAMPLE
A SINTAXE é a mesma para BERNOULLI
e SYSTEM
métodos, especificaremos a porcentagem como fizemos em BERNOULLI
método. Lembrete rápido: SINTAXE
SELECT select_expression
FROM table_name
TABLESAMPLE sampling_method ( argument [, ...] ) [ REPEATABLE ( seed ) ]
...
Para recuperar aproximadamente 100 linhas, precisamos solicitar 100 * 100 / 1.000.000 =0,01% das linhas da tabela. Então nosso percentual será 0,01.
Antes de começar a consultar, vamos lembrar como
REPEATABLE
funciona. Basicamente, escolheremos um parâmetro de semente e obteremos os mesmos resultados para cada vez que usarmos a mesma semente com a consulta anterior. Se fornecermos uma semente diferente, a consulta produzirá um conjunto de resultados bem diferente. Vamos tentar ver os resultados com consultas.
random=# Select * from ts_test tablesample system (0.01) repeatable (60);
id | title | randomcolumn
--------+----------------+---------------------
659598 | Record #659598 | 0.964113113470376
659599 | Record #659599 | 0.531714483164251
659600 | Record #659600 | 0.477636905387044
659601 | Record #659601 | 0.861940925940871
659602 | Record #659602 | 0.545977566856891
659603 | Record #659603 | 0.326795583125204
659604 | Record #659604 | 0.469275736715645
659605 | Record #659605 | 0.734953186474741
659606 | Record #659606 | 0.41613544523716
...
659732 | Record #659732 | 0.893704727292061
659733 | Record #659733 | 0.847225237637758
(136 rows)
Obtemos 136 linhas, pois você pode considerar que esse número depende de quantas linhas são armazenadas em uma única página.
Agora vamos tentar executar a mesma consulta com o mesmo número de semente:
random=# Select * from ts_test tablesample system (0.01) repeatable (60);
id | title | randomcolumn
--------+----------------+---------------------
659598 | Record #659598 | 0.964113113470376
659599 | Record #659599 | 0.531714483164251
659600 | Record #659600 | 0.477636905387044
659601 | Record #659601 | 0.861940925940871
659602 | Record #659602 | 0.545977566856891
659603 | Record #659603 | 0.326795583125204
659604 | Record #659604 | 0.469275736715645
659605 | Record #659605 | 0.734953186474741
659606 | Record #659606 | 0.41613544523716
...
659732 | Record #659732 | 0.893704727292061
659733 | Record #659733 | 0.847225237637758
(136 rows)
Obtemos o mesmo conjunto de resultados graças a
REPEATABLE
opção. Agora podemos comparar TABLESAMPLE SYSTEM
método com método de coluna aleatória olhando o EXPLAIN ANALYZE
saída. random=# EXPLAIN ANALYZE SELECT * FROM ts_test TABLESAMPLE SYSTEM (0.01) REPEATABLE (60);
QUERY PLAN
---------------------------------------------------------------------------------------------------------
Sample Scan on ts_test (cost=0.00..5.00 rows=100 width=26) (actual time=0.091..0.230 rows=136 loops=1)
Sampling: system ('0.01'::real) REPEATABLE ('60'::double precision)
Planning time: 0.323 ms
Execution time: 0.398 ms
(4 rows)
SYSTEM
método usa varredura de amostra e é extremamente rápido. O único retrocesso desse método é que não podemos garantir que a porcentagem fornecida resultará no número esperado de linhas. Conclusão
Nesta postagem do blog, comparamos o padrão
TABLESAMPLE
(SYSTEM
, BERNOULLI
) e o tsm_system_rows
módulo com os métodos aleatórios mais antigos. Aqui você pode revisar as descobertas rapidamente:
ORDER BY random()
é lento devido à classificaçãorandom() <= 0.1
é semelhante aBERNOULLI
método- Adicionar coluna com valor aleatório requer trabalho inicial e pode causar problemas de desempenho
SYSTEM
é rápido, mas é difícil controlar o número de linhastsm_system_rows
é rápido e pode controlar o número de linhas
Como resultado
tsm_system_rows
supera qualquer outro método para selecionar apenas algumas linhas aleatórias. Mas o verdadeiro vencedor é definitivamente
TABLESAMPLE
em si. Graças à sua extensibilidade, extensões personalizadas (ou seja, tsm_system_rows
, tsm_system_time
) pode ser desenvolvido usando TABLESAMPLE
funções do método. Observação do desenvolvedor: Mais informações sobre como escrever métodos de amostragem personalizados podem ser encontradas no capítulo Escrevendo um método de amostragem de tabela da documentação do PostgreSQL.
Observação para o futuro: Discutiremos o Projeto AXLE e a extensão tsm_system_time em nosso próximo
TABLESAMPLE
postagem no blog.