Alguém poderia explicar o comportamento bizarro executando milhões de atualizações?


8

Alguém poderia me explicar esse comportamento? Eu executei a seguinte consulta no Postgres 9.3 executando nativamente no OS X. Eu estava tentando simular algum comportamento em que o tamanho do índice pudesse crescer muito maior que o tamanho da tabela e, em vez disso, achei algo ainda mais bizarro.

CREATE TABLE test(id int);
CREATE INDEX test_idx ON test(id);

CREATE FUNCTION test_index(batch_size integer, total_batches integer) RETURNS void AS $$
DECLARE
  current_id integer := 1;
BEGIN
FOR i IN 1..total_batches LOOP
  INSERT INTO test VALUES (current_id);
  FOR j IN 1..batch_size LOOP
    UPDATE test SET id = current_id + 1 WHERE id = current_id;
    current_id := current_id + 1;
  END LOOP;
END LOOP;
END;
$$ LANGUAGE plpgsql;

SELECT test_index(500, 10000);

Eu deixei isso funcionar por cerca de uma hora na minha máquina local, antes de começar a receber avisos de problemas de disco do OS X. Notei que o Postgres estava sugando cerca de 10 MB / s do meu disco local e que o banco de dados do Postgres estava consumindo um total geral. de 30 GB da minha máquina. Acabei cancelando a consulta. Independentemente disso, o Postgres não retornou o espaço em disco para mim e consultei o banco de dados para obter estatísticas de uso com o seguinte resultado:

test=# SELECT nspname || '.' || relname AS "relation",
    pg_size_pretty(pg_relation_size(C.oid)) AS "size"
  FROM pg_class C
  LEFT JOIN pg_namespace N ON (N.oid = C.relnamespace)
  WHERE nspname NOT IN ('pg_catalog', 'information_schema')
  ORDER BY pg_relation_size(C.oid) DESC
  LIMIT 20;

           relation            |    size
-------------------------------+------------
 public.test                   | 17 GB
 public.test_idx               | 14 GB

No entanto, a seleção na tabela não produziu resultados.

test=# select * from test limit 1;
 id
----
(0 rows)

A execução de 10000 lotes de 500 é de 5.000.000 de linhas, o que deve gerar um tamanho muito pequeno de tabela / índice (na escala de MB). Eu suspeito que o Postgres esteja criando uma nova versão da tabela / índice para cada INSERT / UPDATE que está acontecendo com a função, mas isso parece estranho. Toda a função é executada transacionalmente e a tabela estava vazia para iniciar.

Alguma idéia de por que estou vendo esse comportamento?

Especificamente, as duas perguntas que tenho são: por que esse espaço ainda não foi recuperado pelo banco de dados e a segunda é por que o banco de dados exigiu tanto espaço em primeiro lugar? 30 GB parece muito, mesmo quando se considera o MVCC

Respostas:


7

Versão curta

Seu algoritmo parece O (n * m) à primeira vista, mas cresce efetivamente O (n * m ^ 2), porque todas as linhas têm o mesmo ID. Em vez de 5 milhões de linhas, você recebe mais de 1,25 G

Versão longa

Sua função está dentro de uma transação implícita. É por isso que você não vê dados depois de cancelar sua consulta e também por que ele precisa manter versões distintas das tuplas atualizadas / inseridas nos dois loops.

Além disso, suspeito que você tenha um erro em sua lógica ou subestime o número de atualizações feitas.

Primeira iteração do loop externo - current_id começa em 1, insere 1 linha e, em seguida, o loop interno executa uma atualização 10000 vezes para a mesma linha, finalizando com a única linha mostrando um ID de 10001 e current_id com um valor de 10001. 10001 as versões da linha ainda são mantidas, pois a transação não está concluída.

Segunda iteração do loop externo - como current_id é 10001, uma nova linha é inserida com o ID 10001. Agora você tem 2 linhas com o mesmo "ID" e 10003 versões no total de ambas as linhas (10002 do primeiro, 1 de o segundo). Em seguida, o loop interno atualiza 10000 vezes as duas linhas, criando 20000 novas versões e chegando a 3.0003 tuplas até agora ...

Terceira iteração do loop externo: o ID atual é 20001, uma nova linha é inserida com o ID 20001. Você tem 3 linhas, todas com as mesmas versões de "ID" 20001, 30006 linhas / tuplas até o momento. Em seguida, você executa 10000 atualizações de 3 linhas, criando 30000 novas versões, agora 60006 ...

...

(Se o seu espaço permitir) - 500ª iteração do loop externo, cria atualizações de 5M de 500 linhas, apenas nesta iteração

Como você vê, em vez das atualizações esperadas de 5 milhões, você tem 1000 + 2000 + 3000 + ... + 4990000 + 5000000 atualizações (mais alterações), que seriam 10000 * (1 + 2 + 3 + ... + 499+ 500), mais de 1,25G de atualizações. E, é claro, uma linha não é apenas do tamanho do seu int, ela precisa de alguma estrutura adicional; portanto, sua tabela e índice têm mais de dez gigabytes.

Perguntas e Respostas relacionadas:


5

O PostgreSQL somente retorna espaço em disco depois VACUUM FULL, não após um DELETEou ROLLBACK(como resultado do cancelamento)

O formulário padrão do VACUUM remove as versões de linhas mortas em tabelas e índices e marca o espaço disponível para reutilização futura. No entanto, ele não retornará o espaço para o sistema operacional, exceto no caso especial em que uma ou mais páginas no final de uma tabela ficam totalmente livres e um bloqueio exclusivo da tabela pode ser obtido facilmente. Por outro lado, o VACUUM FULL compacta ativamente as tabelas escrevendo uma nova versão completa do arquivo da tabela sem espaço morto. Isso minimiza o tamanho da tabela, mas pode levar muito tempo. Também requer espaço em disco extra para a nova cópia da tabela, até que a operação seja concluída.

Como observação, toda a sua função parece questionável. Não tenho certeza do que você está tentando testar, mas se você deseja criar dados, pode usargenerate_series

INSERT INTO test
SELECT x FROM generate_series(1, batch_size*total_batches) AS t(x);

Legal, isso explica por que a tabela ainda estava marcada como consumindo tantos dados, mas por que ela precisava de todo esse espaço em primeiro lugar? Pelo meu entendimento do MVCC, ele precisa manter versões distintas das tuplas atualizadas / inseridas para a transação, mas não deve precisar manter versões separadas para cada iteração do loop.
Nikhil N

11
Cada iteração do loop está gerando novas tuplas.
Evan Carroll

2
Certo, mas minha impressão é que o MVCC não deve criar tuplas para todas as tuplas que foram modificadas ao longo da transação. Ou seja, quando o primeiro INSERT executa o Postgres, cria uma única tupla e adiciona uma nova única para cada UPDATE. Como as UPDATES são executadas para cada linha 500 vezes e existem 10000 INSERTs, isso equivale a 500 * 10000 linhas = 5M de tuplas no momento em que a transação é confirmada. Agora, isso é apenas uma estimativa, mas, independentemente de 5M *, diga 50 bytes para rastrear cada tupla ~ = 250 MB, que é MUITO menor que 30 GB. De onde tudo isso está vindo?
precisa

Também re: função questionável, estou tentando testar o comportamento de um índice quando os campos indexados estão sendo atualizados muitas vezes, mas de uma maneira monoticamente crescente, produzindo assim um índice muito esparso, mas sempre anexado ao disco.
precisa

Estou confuso quanto ao que você pensa. Você acha que uma linha atualizada 18e vezes em um loop é uma tupla ou 1e8?
Evan Carroll

3

Os números reais após a análise da função são muito maiores porque todas as linhas da tabela obtêm o mesmo valor que é atualizado várias vezes em cada iteração.

Quando o executamos com parâmetros ne m:

SELECT test_index(n, m);

existem minserções e n * (m^2 + m) / 2atualizações de linha . Portanto, para n = 500e m = 10000, o Postgres precisará inserir apenas 10 mil linhas, mas executar ~ 25G (25 bilhões) de atualizações de tupla.

Considerando que uma linha no Postgres possui uma sobrecarga de 24 bytes, uma tabela com apenas uma intcoluna precisará de 28 bytes por linha mais a sobrecarga da página. Portanto, para a operação terminar, precisaríamos de cerca de 700 GB mais o espaço para o índice (que também seria algumas centenas de GB).


Teste

Para testar a teoria, criamos outra tabela test_testcom uma única linha.

CREATE TABLE test_test (i int not null) ;
INSERT INTO test_test (i) VALUES (0);

Em seguida, adicionamos um gatilho testpara que cada atualização aumente o contador em 1. (Código omitido). Em seguida, executamos a função, com valores menores, n = 50e m = 100.

Nossa teoria prevê :

  • 100 inserções de linha,
  • Atualizações de tupla de 250 K (252500 = 50 * 100 * 101/2)
  • pelo menos 7 MB para a tabela em disco
  • (+ espaço para o índice)

Teste 1 ( testtabela original , com índice)

    SELECT test_index(50, 100) ;

Após a conclusão, verificamos o conteúdo da tabela:

x=# SELECT COUNT(*) FROM test ;
 count 
-------
   100
(1 row)

x=# SELECT i FROM test_test ;
   i    
--------
 252500
(1 row)

E uso do disco (consulta em Estatísticas do tamanho / uso do Índice em Manutenção de Índice ):

tablename | indexname | num_rows | table_size | index_size | unique | number_of_scans | tuples_read 
----------+-----------+----------+------------+------------+--------+-----------------+-------------
test      | test_idx  |      100 | 8944 kB    | 5440 kB    | N      |           10001 |      505003
test_test |           |        1 | 8944 kB    |            | N      |                 |            

A testtabela usou quase 9 MB para a tabela e 5 MB para o índice. Observe que a test_testtabela usou outros 9 MB! Isso é esperado, pois também passou por 250 mil atualizações (nosso segundo gatilho atualizou a única linha de test_testcada atualização de uma linha test).

Observe também o número de varreduras na tabela test(10K) e as tuplas lêem (500K).

Teste 2 ( testtabela sem índice)

Exatamente o mesmo que acima, exceto que a tabela não possui índice.

tablename | indexname | num_rows | table_size | index_size | unique | number_of_scans | tuples_read 
----------+-----------+----------+------------+------------+--------+-----------------+-------------
 test        |        |      100 | 8944 kB    |            | N      |                 |            
 test_test   |        |        1 | 8944 kB    |            | N      |                 |            

Temos o mesmo tamanho para uso de disco da tabela e, é claro, nenhum uso de disco para índices. O número de varreduras na tabela testé zero e as tuplas também são lidas.

Teste 3 (com fator de preenchimento inferior)

Tentei com o fator de preenchimento 50 e o mais baixo possível, 10. Nenhuma melhoria. O uso do disco era quase idêntico aos testes anteriores (que usavam o fator de preenchimento padrão, 100%)

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.