Falamos sobre a criação de chaves artificiais, como usar UUID ou sequências, inclusive falamos sobre a nova sintaxe adotada no PostgreSQL 10 utilizando as chamadas IDENTITY COLUMNs.
Uma reação comum que vimos por aí é achar que é mais simples sempre usar UUID, uma vez que não há risco de colisão e pode-se utilizá-lo 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 armazená-las no banco de dados.
Vamos explorar aqui algumas possibilidades entre utilizar uma SEQUENCE armazenada num campo INTEGER e índices do tipo BTREE e HASH (que melhoraram muito no PostgreSQL 10). Depois, vamos utilizar campos dos tipos 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);
Note que não existem índices UNIQUE do tipo HASH, logo, ele não pode ser utilizado em uma PRIMARY KEY. Nesse 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 sequências 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);
Tomamos alguns cuidados antes de fazer a carga:
- Aumentamos o valor do WORK_MEM;
- Rodamos um CHECKPOINT antes de cada teste;
- Realizamos todos os testes 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 sequência 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
Veja que utilizar UUID é mais eficiente do que armazenar esse 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, 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 do 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 sequências, particularmente se 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 apenas um registro 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 com base na sua chave artificial, as sequências têm uma vantagem enorme, e os índices HASH, uma boa desvantagem.
Conclusões
Esperamos 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 fizemos testes com UPDATE e DELETE, pois consideramos que não são relevantes 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 é cerca de 5x mais lento em operações de INSERT;
- UUID e sequências têm desempenho semelhante em consultas simples do tipo “id = xxxx’, mas, em consultas complexas, as sequences são mais eficientes devido à 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 a 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 dado 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 o VARCHAR utilizam pelo menos um byte por caractere. A diferença está na validação e formatação de entrada e saída, que ficam mais simples no UUID.

Deixe um comentário