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

Amostra de tabela e outros métodos para obter tuplas aleatórias


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 SYSTEMBERNOULLI 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ção
  • random() <= 0.1 é semelhante a BERNOULLI 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 linhas
  • tsm_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.