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.