Chaves Artificiais no PostgreSQL: desempenho

Falamos sobre a criação de chaves artificiais, sobre como usar UUID ou sequencias, inclusive sobre a nova sintaxe adotada no PostgreSQL 10 utilizando as chamadas IDENTITY COLUMNs. Uma reação comum que eu vi por aí é achar que é mais simples usar UUID sempre, uma vez que você não tem risco de colisão e pode usar em sistemas distribuídos. Bom, a história não é bem assim. Existe um custo adicional ao se escolher UUIDs ou mesmo gerar hashs enormes e armazenar no banco de dados. Vou aqui explorar algumas possibilidades, entre utilizar uma SEQUENCE armazenada num campo INTEGER e índices do tipo BTREE e HASH (que melhorou muito no PostgreSQL 10). Depois vamos utilizar campos do tipo UUID, VARCHAR e CHAR, também com índices BTREE e HASH.

Primeiro vamos criar nossas tabelas:

CREATE TABLE seq_btree (id integer PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY);
CREATE TABLE seq_hash (id integer  GENERATED BY DEFAULT AS IDENTITY);
CREATE INDEX ON seq_hash USING hash (id);
CREATE TABLE uuid_btree (id uuid PRIMARY KEY);
CREATE TABLE uuid_hash (id uuid );
CREATE INDEX ON uuid_hash USING hash (id);
CREATE TABLE uuid_char_btree (id char(36) PRIMARY KEY);
CREATE TABLE uuid_char_hash (id varchar);
CREATE INDEX ON uuid_char_hash USING hash (id);
CREATE TABLE uuid_varchar_btree (id varchar PRIMARY KEY);
CREATE TABLE uuid_varchar_hash (id varchar);
CREATE INDEX ON uuid_varchar_hash USING hash (id);

Notem que não existem índices UNIQUE do tipo HASH, logo ele não pode ser utilizado em uma PRIMARY KEY. Neste caso adicionamos o índice após a criação da tabela.

INSERT

Agora vamos inserir um milhão de registros em cada uma das tabelas utilizando a função nextval() nas sequencias e a função uuid_generate_v4() para gerar o UUID:

INSERT INTO seq_btree SELECT nextval('seq_btree_id_seq') FROM generate_series(1::integer,1000000::integer);

INSERT INTO  uuid_btree SELECT uuid_generate_v4() FROM generate_series(1,1000000);

Tomei alguns cuidados antes de fazer a carga:

  • Aumentei o valor do WORK_MEM;
  • Rodei um CHECKPOINT antes de cada teste;
  • Realizei todos os teste num mesmo ambiente controlado, sem nada rodando em paralelo.

Vejamos o tempo levado para a carga de cada tabela:

seq_btree           Time: 11152.751 ms (00:11.153)
seq_hash            Time: 11692.158 ms (00:11.692)
uuid_btree          Time: 58822.429 ms (00:58.822)
uuid_hash           Time: 55405.673 ms (00:55.406)
uuid_char_btree     Time: 77064.257 ms (01:17.064)
uuid_char_hash      Time: 57885.347 ms (00:57.885)
uuid_varchar_btree  Time: 75555.282 ms (01:15.555)
uuid_varchar_hash   Time: 57805.156 ms (00:57.805)

Aqui vemos uma clara vantagem em se utilizar sequências! Elas são muito mais rápidas durante o INSERT. Depois vemos que os dados armazenados em UUID são mais rápidos que os armazenados em CHAR ou VARCHAR. Note também que nas tabelas que utilizam UUID, o índice HASH foi mais veloz que o índice BTREE. Para números sequenciais isso se inverte.

Tamanho

Além disso, vemos que o tamanho das tabelas muda, uma vez que uma sequencia ocupa menos espaço:

seq_btree 35 MB
seq_hash 35 MB
uuid_btree 42 MB
uuid_hash 42 MB
uuid_char_btree 65 MB
uuid_char_hash 65 MB
uuid_varchar_btree 65 MB
uuid_varchar_hash 65 MB

Vejam que utilizar UUID é mais eficiente do que armazenar este tipo de dado em CHAR ou VARCHAR, daí o melhor desempenho na carga.

Agora vejamos como fica o tamanho dos índices:

seq_btree_pkey           seq_btree          21 MB
seq_hash_id_idx          seq_hash           37 MB
uuid_btree_pkey          uuid_btree         39 MB
uuid_hash_id_idx         uuid_hash          36 MB
uuid_char_btree_pkey     uuid_char_btree    73 MB
uuid_char_hash_id_idx    uuid_char_hash     37 MB
uuid_varchar_btree_pkey  uuid_varchar_btree 74 MB
uuid_varchar_hash_id_idx uuid_varchar_hash  37 MB

Aqui nós vemos como o índice do tipo HASH tem vantagens em dados aleatórios sobre o BTREE que é melhor em dados sequenciais.

SELECT

Outro ponto de vista importante é referente às estatísticas coletadas pelo ANALYZE. Olhando a tabela pg_stats, vemos algumas coisas interessantes:

     tablename      | avg_width | correlation
--------------------+-----------+--------------
 seq_btree          |         4 |            1
 seq_hash           |         4 |            1
 uuid_btree         |        16 |   0.00269653
 uuid_hash          |        16 |   0.00022602
 uuid_char_btree    |        37 |   0.00133542
 uuid_char_hash     |        37 |   0.00420481
 uuid_varchar_btree |        37 |   0.0120523
 uuid_varchar_hash  |        37 |   0.0016304

A primeira coisa que vemos é que o tamanho (avg_width) do campo é muito menor no caso de um INTEGER e muito pior no caso do VARCHAR e CHAR. depois notamos que a correlação (relação entre a distribuição dos dados e a ordem em que eles estão armazenados no disco) é favorável ao uso de sequencias, particularmente quando você quiser trazer vários registros baseados na ordenação.

Vejamos como fica o desempenho das nossas chaves artificiais em operações de leitura. Primeiro vamos pegar um registro apenas de cada tabela e medir o tempo:

SELECT * FROM seq_btree WHERE id = 100001;

seq_btree           Time: 0.390 ms
seq_hash            Time: 0.366 ms
uuid_btree          Time: 0.407 ms
uuid_hash           Time: 0.396 ms
uuid_char_btree     Time: 0.494 ms
uuid_char_hash      Time: 0.391 ms
uuid_varchar_btree  Time: 0.440 ms
uuid_varchar_hash   Time: 0.414 ms

Vemos que a diferença aqui é pequena, com leve vantagem para índices do tipo HASH e para o uso de sequências. Agora vamos utilizar uma ordenação para trazer os dados:

SELECT * FROM seq_btree ORDER BY id LIMIT 1 OFFSET 100000;

seq_btree           Time: 26.792 ms
seq_hash            Time: 217.644 ms
uuid_btree          Time: 100.570 ms
uuid_hash           Time: 507.426 ms
uuid_char_btree     Time: 114.143 ms
uuid_char_hash      Time: 3753.417 ms
uuid_varchar_btree  Time: 109.001 ms
uuid_varchar_hash   Time: 3815.933 ms

Aqui a correlação faz muita diferença e os índices do tipo HASH são uma péssima escolha. Então se você tem o hábito de ordenar os dados baseado na sua chave artificial, as sequências tem uma vantagem enorme, e os índices HASH uma boa desvantagem.

Conclusões

Espero que tenha ficado claro que o uso de UUID tem custo sim e deve ser avaliado com cuidado. Então vejamos algumas considerações finais:

  • Não fiz testes com UPDATE e DELETE aqui, pois considerei que não relevantes para no uso de chaves artificiais.
  • No geral ele ocupa mais espaço (ainda mais se você fizer a besteira de utilizar CHAR ou VARCHAR)
  • UUID e é cerca de 5x mais lento em operações de INSERT
  • UUID e sequências tem desempenho semelhante em consultas simples do tipo “id = xxxx’, mas em consultas complexas as sequences são mais eficientes devido a correlação entre a sequência e o armazenamento físico;
  • Índices do tipo HASH são ligeiramente mais eficientes com UUID, mas não valem à pena quando se utiliza sequences. No entanto, vale lembrar que índices do tipo hash não podem ser utilizados como PK ou UNIQUE.

Observação

Você também pode utilizar o tipo de dados binário bytea para armazenar números hexadecimais com a mesma eficiência que o campo do tipo UUID, gravando dois números hexadecimais por byte, enquanto o CHAR e VARCHAR utilizam pelo menos um byte por caractere. A diferença está na validação e formatação de entrada e saída que fica mais simples no UUID.

Compartilhe

Você pode gostar

Sobre minha saída da Timbira

Há 14 anos, durante o PGConf.Brasil 2009, lá na UNICAMP em Campinas/SP, 4 pessoas se reuniram e idealizaram a criação da primeira empresa dedicada exclusivamente

Split brain

Já tem algum tempo que eu pensava em fazer isso e chegou a hora. Este blog vai se dividir em 2 partes a partir de

plugins premium WordPress