Criar chaves artificiais virou um vício com o advento das ferramentas de ORM. Mesmo antes delas, as chaves artificiais já existiam e temos bons motivos (e maus motivos também) para adota-las. Existem várias formas de criar uma chave artificial. No PostgreSQL temos duas bastante utilizadas. Uma utiliza as sequências, para gerar números sequenciais. Note que não há garantia de que a sequência não ganhe buracos no caminho. Outra utiliza números aleatórios que nunca se repetem, o famoso UUID. O uso de sequências é bastante conhecido e difundido, mas o UUID é útil em sistemas distribuídos onde você pode gerar um número para sua chave artificial em diversos nós distintos. Se você utilizar uma sequencia comum, você não poderá garantir que dois nós não utilizem o mesmo número, perdendo assim a unicidade da sua chave. Existem sequências distribuídas também, até a versão 10 do PostgreSQL ela não foi implementada ainda. O Oracle utiliza sequencias distribuías no Oracle RAC onde cada nó guarda no cache números diferentes para os próximos valores da sequência. No MySQL, é comum criar sequências com um salto entre cada número, onde cada nó possui um valor inicial diferente, assim com 3 nós por exemplo, o nó um teria números sequenciais como 1, 4, 7, 10… o nó 2 teria números 2, 5, 8, 11… e o nó 3 teria números 3, 6, 9, 12…
UUID
UUID é um acrônimo de “Universally unique identifier” ou identificador único universal. O PostgreSQL tem suporte nativo para este tipo de dado e funções para gerar localmente estes números, sem precisar da aplicação para isso. Mas estas funções não são nativas no core. Por sorte estão num módulo do contrib (sempre recomendo instalar todos os módulos do contrib e depois criar as extensões conforme a necessidade). Vejamos um exemplo simples de sua utilização:
postgres=# CREATE TABLE uuid_teste (id uuid, nome varchar); CREATE TABLE postgres=# \d uuid_teste Tabela "public.uuid_teste" Coluna | Tipo | Modificadores -------+-------------------+--------------- id | uuid | nome | character varying | postgres=# CREATE EXTENSION "uuid-ossp"; CREATE EXTENSION postgres=# INSERT INTO uuid_teste VALUES (uuid_generate_v4(), 'Fábio'); INSERT 0 1 postgres=# INSERT INTO uuid_teste VALUES (uuid_generate_v4(), 'Paulo'); INSERT 0 1 postgres=# INSERT INTO uuid_teste VALUES (uuid_generate_v4(), 'Maria'); INSERT 0 1 postgres=# SELECT * FROM uuid_teste ; id | nome --------------------------------------+------- 8feca1d8-8461-4df1-8951-7a3b0abfcd56 | Fábio 68bd6900-8c5f-4852-a124-b881dcf2edf5 | Paulo 179fc357-17c4-449f-8d7c-03a1e922d6f1 | Maria (3 registros)
Note que não existe um objeto como uma SEQUENCE aqui, apenas o tipo de dados uuid e a função uuid_generate_v4() para gerar os números aleatórios (que nunca se repetem). Você também pode automatizar um pouco as coisas e usar a função uuid_generate_v4() como valor padrão para nossa coluna id:
postgres=# ALTER TABLE uuid_teste ALTER COLUMN id SET DEFAULT uuid_generate_v4(); ALTER TABLE postgres=# \d uuid_teste Tabela "public.uuid_teste" Coluna | Tipo | Modificadores --------+-------------------+------------------------------------ id | uuid | valor padrão de uuid_generate_v4() nome | character varying | postgres=# INSERT INTO uuid_teste (id, nome) VALUES (DEFAULT, 'Pedro'); INSERT 0 1 postgres=# INSERT INTO uuid_teste (id, nome) VALUES (DEFAULT, 'Ana'); INSERT 0 1 postgres=# INSERT INTO uuid_teste (nome) VALUES ('José'); INSERT 0 1 postgres=# INSERT INTO uuid_teste (nome) VALUES ('Joana'); INSERT 0 1 postgres=# SELECT * FROM uuid_teste ; id | nome --------------------------------------+------- 8feca1d8-8461-4df1-8951-7a3b0abfcd56 | Fábio 68bd6900-8c5f-4852-a124-b881dcf2edf5 | Paulo 179fc357-17c4-449f-8d7c-03a1e922d6f1 | Maria 51f88f73-9cc1-4fdb-be4a-bd001d7bc934 | Pedro 05e96034-369f-4f42-b458-3ba872d55fc6 | Ana f9807337-fab0-4913-990b-8cc2b168bb04 | José d6ac1694-91de-44d1-8087-6bed2207b228 | Joana (7 registros)
Note que utilizei duas sintaxes ligeiramente diferentes para obter o mesmo resultado no INSERT.
Por fim, existem diversos algorítimos diferentes para gerar o número aleatório do UUID, o pacote uuid-ossp tem 5 diferentes funções para gera-lo. Leia a documentação deste módulo e escolha a mais apropriada antes de começar a utilizar o UUID.
SEQUENCE
As sequencias existem como um objeto independente no banco de dados. Ao cria-las você pode definir algumas propriedades particulares, como número inicial e final, saltos, etc:
postgres=# \h CREATE SEQUENCE Comando: CREATE SEQUENCE Descrição: define um novo gerador de sequência Sintaxe: CREATE [ TEMPORARY | TEMP ] SEQUENCE [ IF NOT EXISTS ] nome [ INCREMENT [ BY ] incremento ] [ MINVALUE valor_mínimo | NO MINVALUE ] [ MAXVALUE valor_máximo | NO MAXVALUE ] [ START [ WITH ] início ] [ CACHE cache ] [ [ NO ] CYCLE ] [ OWNED BY { nome_tabela.nome_coluna | NONE } ]
A partir da versão 10, ficou mais fácil acompanhar a vida da
test=# CREATE SEQUENCE s; CREATE SEQUENCE test=# SELECT nextval('s'); nextval --------- 2 (1 row) test=# SELECT nextval('s'); nextval --------- 3 (1 row) test=# SELECT * FROM pg_sequences; schemaname | sequencename | sequenceowner | data_type | start_value | min_value | max_value | increment_by | cycle | cache_size | last_value -----------+--------------+---------------+-----------+-------------+-----------+---------------------+--------------+-------+------------+------------ public | s | postgres | bigint | 1 | 1 | 9223372036854775807 | 1 | f | 1 | 3 (1 row)
A view pg_sequences é nova no PostgreSQL 10 e ajuda bastante a verificar o status da sua sequencia. Note que o valor da coluna last_value, reflete aqui o último valor gerado.
Algumas aplicações utilizam a função nextval exatamente da forma como mostrada e só depois realizam um INSERT em uma tabela. Mas assim como no caso do UUID, você pode fazer isso de forma mais direta:
test=# CREATE table seq_teste(id bigint, nome varchar); CREATE TABLE test=# \d seq_teste Table "public.seq_teste" Column | Type | Collation | Nullable | Default -------+--------------------+-----------+----------+--------- id | bigint | | | nome | character varying | | | test=# INSERT INTO seq_teste (id, nome) VALUES (nextval('s'), 'Fábio'); INSERT 0 1 test=# INSERT INTO seq_teste (id, nome) VALUES (nextval('s'), 'Ana'); INSERT 0 1 test=# SELECT * FROM seq_teste ; id | nome ----+------- 4 | Fábio 5 | Ana (2 rows)
Você pode também ter uma segunda tabela utilizando a mesma sequência:
test=# CREATE TABLE seq_teste2 (id bigint, idade smallint); LOG: statement: CREATE TABLE seq_teste2 (id bigint, idade smallint); CREATE TABLE test=# INSERT INTO seq_teste2 (id, idade) VALUES (nextval('s'), 18); INSERT 0 1 test=# INSERT INTO seq_teste2 (id, idade) VALUES (nextval('s'), 36); INSERT 0 1 test=# SELECT * FROM seq_teste2; id | idade ----+------- 6 | 18 7 | 36 (2 rows)
Muitas pessoas preferem pegar o próximo número da sequência antes do INSERT por um simples motivo: elas vão utilizar esta numeração em outros lugares depois. Da forma que fizemos, para saber qual foi o número gerado pela nossa sequência ‘s’, temos que fazer um SELECT depois do INSERT. é claro que existe um jeito mais prático de fazer isso, utilizando a cláusula RETURNING existente nos comandos de DML INSERT, UPDATE e DELETE:
test=# INSERT INTO seq_teste (id, nome) VALUES (nextval('s'), 'Hugo') RETURNING id; id ---- 8 (1 row) INSERT 0 1 test=# INSERT INTO seq_teste (id, nome) VALUES (nextval('s'), 'Priscila') RETURNING id; id ---- 9 (1 row) INSERT 0 1
Vejam que o seu INSERT assume um comportamento parecido com um SELECT, uma vez que ele retorna um valor para você.
Você também pode colocar o valor da sua sequência como valor padrão para a sua coluna na tabela, assim como fizemos com o UUID:
test=# ALTER TABLE seq_teste ALTER COLUMN id SET DEFAULT nextval('s'); LOG: statement: ALTER TABLE seq_teste ALTER COLUMN id SET DEFAULT nextval('s'); ALTER TABLE test=# \d seq_teste Table "public.seq_teste" Column | Type | Collation | Nullable | Default -------+-------------------+-----------+----------+------------------------ id | bigint | | | nextval('s'::regclass) nome | character varying | | | test=# INSERT INTO seq_teste (nome) VALUES ('Patrícia') RETURNING id; id ---- 10 (1 row) INSERT 0 1 test=# INSERT INTO seq_teste (nome) VALUES ('Denis') RETURNING id; id ---- 11 (1 row) INSERT 0 1
Um detalhe extra sobre a sequência é que ela pode ter um “dono”. Você pode definir que um determinado campo de uma tabela seja o dono desta sequência. A única vantagem de fazer isso é que se em algum momento a sua coluna (ou a tabela inteira, claro) for excluída, a sequência será excluída junto:
test=# ALTER SEQUENCE s OWNED BY seq_teste.id; LOG: statement: ALTER SEQUENCE s OWNED BY seq_teste.id; ALTER SEQUENCE test=# \d s Sequence "public.s" Type | Start | Minimum | Maximum | Increment | Cycles? | Cache --------+-------+---------+---------------------+-----------+---------+------- bigint | 1 | 1 | 9223372036854775807 | 1 | no | 1 Owned by: public.seq_teste.id test=# DROP TABLE seq_teste; LOG: statement: DROP TABLE seq_teste; DROP TABLE test=# \d s Did not find any relation named "s".
De fato, estas 3 operações são muito comuns:
- Criar uma sequência
- Definir a sequência como valor padrão de uma coluna de uma tabela
- Definir a sequência como pertencente à coluna desta tabela
Para facilitar a vida, o Postgres possui os tipos de dados serial, bigserial e smallserial que criam respectivamente campos do tipo integer, bigint e smallint com uma sequence associada utilizando estes 3 passos implicitamente:
test=# CREATE TABLE seq_teste (id bigserial, nome varchar); LOG: statement: CREATE TABLE seq_teste (id bigserial, nome varchar); CREATE TABLE test=# \d seq_teste Table "public.seq_teste" Column | Type | Collation | Nullable | Default --------+-------------------+-----------+----------+--------------------------------------- id | bigint | | not null | nextval('seq_teste_id_seq'::regclass) nome | character varying | | |
Note que estes tipos de dados são apenas atalhos, e não tipos de dados reais. Os tipos de dados na tabela continuam sendo do tipo integer, bigint e smallint. Muitas pessoas que utilizam o serial não percebem as 3 operações criadas implicitamente e sequer imaginam que existe uma sequência sendo utilizada nos bastidores.
Colisão
O grande problema com as sequências é que embora você possa utilizar uma única sequência para várias tabelas, uma chave artificial só pode utilizar valores gerados por uma única sequência. Se você resolver inserir dados literais na sua tabela, o valor da sequência não vai ser incrementado e você terá uma colisão:
test=# ALTER TABLE seq_teste ADD PRIMARY KEY (id); LOG: statement: ALTER TABLE seq_teste ADD PRIMARY KEY (id); ALTER TABLE test=# INSERT INTO seq_teste (nome) VALUES ('Fábio'); INSERT 0 1 test=# INSERT INTO seq_teste (nome) VALUES ('Paulo'); INSERT 0 1 test=# INSERT INTO seq_teste (nome) VALUES ('Cícero'); INSERT 0 1 test=# SELECT * FROM seq_teste; id | nome ----+-------- 1 | Fábio 2 | Paulo 3 | Cícero (3 rows) test=# INSERT INTO seq_teste (id, nome) VALUES (4, 'Paula'); INSERT 0 1 test=# INSERT INTO seq_teste (id, nome) VALUES (5, 'Márcia'); INSERT 0 1 test=# INSERT INTO seq_teste (id, nome) VALUES (6, 'Joana'); INSERT 0 1 test=# SELECT * FROM seq_teste; id | nome ----+-------- 1 | Fábio 2 | Paulo 3 | Cícero 4 | Paula 5 | Márcia 6 | Joana (6 rows) test=# INSERT INTO seq_teste (nome) VALUES ('Joaquim'); ERROR: duplicate key value violates unique constraint "seq_teste_pkey" DETAIL: Key (id)=(4) already exists.
Ou seja, depois de inserir os IDs 4, 5 e 6 manualmente, a sequência continuou no valor 4. Ao tentar inserir utilizando a sequencia novamente ela duplicou a chave primária e deu erro. Para concertar isso, vamos ter que alterar o valor do próximo número da sequência, utilizando a função setval:
test=# SELECT setval('seq_teste_id_seq',6); setval -------- 6 (1 row) test=# INSERT INTO seq_teste (nome) VALUES ('Joaquim') RETURNING id; id ---- 7 (1 row)
Um detalhe aqui: o nome da sequência agora é seq_teste_id_seq, que é o nome gerado internamente pelo Postgres quando você utilizou o serial na criação da tabela.
Além do problema da possível colisão, existem alguns detalhes adicionais que podem complicar a vida de quem usa sequências para alimentar chaves artificiais:
- Se você criar uma tabela com o CREATE TABLE … LIKE, a nova tabela vai apontar para a mesma sequência da tabela original e não criar uma nova;
- Quando você remove o valor padrão que referencia uma sequência, a sequência em si não é removida da base;
- Você tem permissões (GRANT, REVOKE) adicionais para a sequência, além das permissões na tabela;
- Se você quiser apagar uma sequência utilizada como valor padrão de uma tabela, você precisa utilizar o DROP SEQUENCE … CASCADE;
Para mais detalhes sobre estes problemas e a comparação com o uso das novas IDENTITY COLUMNS veja o artigo do Sr. Peter Eisentraut sobre o assunto.
IDENTITY COLUMNS
O padrão SQL:2003 criou um padrão novo para lidar com sequências em chaves artificiais. O postgreSQL passou a implementar este padrão no PostgreSQL 10, embora continue suportando o uso de campos do tipo serial, mantendo a compatibilidade com o legado. No lugar da cláusula DEFAULT de uma coluna, você pode utilizar a seguinte sintaxe:
GENERATED { ALWAYS | BY DEFAULT } AS IDENTITY [ ( sequence_options ) ]
Desta forma você tem que escolher entre as opções ALWAYS ou BY DEFAULT e pode adicionar opcionalmente algumas opções para a sequência que será criada implicitamente. A diferença entre o ALWAYS e o BY DEFAULT é justamente a possibilidade de colisão durante o INSERT. Se você utilizar o BY DEFAULT, terá um comportamento mais parecido com o tipo de dados serial do Postgres, ou seja, qualquer INSERT pode escolher valores fora da sequência:
test=# CREATE TABLE teste_by_default (id integer PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY, nome varchar); LOG: statement: CREATE TABLE teste_by_default (id integer PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY, nome varchar); CREATE TABLE test=# \d teste_by_default Table "public.teste_by_default" Column | Type | Collation | Nullable | Default --------+-------------------+-----------+----------+---------------------------------- id | integer | | not null | generated by default as identity nome | character varying | | | Indexes: "teste_by_default_pkey" PRIMARY KEY, btree (id) test=# \d teste_by_default_id_seq Sequence "public.teste_by_default_id_seq" Type | Start | Minimum | Maximum | Increment | Cycles? | Cache ---------+-------+---------+------------+-----------+---------+------- integer | 1 | 1 | 2147483647 | 1 | no | 1 Sequence for identity column: public.teste_by_default.id test=# INSERT INTO teste_by_default (nome) VALUES ('Fábio'); INSERT 0 1 test=# INSERT INTO teste_by_default (nome) VALUES ('Paulo'); INSERT 0 1 test=# INSERT INTO teste_by_default (nome) VALUES ('João'); INSERT 0 1 test=# INSERT INTO teste_by_default (id, nome) VALUES (4, 'Pedro'); INSERT 0 1 test=# INSERT INTO teste_by_default (id, nome) VALUES (5, 'José'); INSERT 0 1 test=# INSERT INTO teste_by_default (nome) VALUES ( 'Rita'); ERROR: duplicate key value violates unique constraint "teste_by_default_pkey" DETAIL: Key (id)=(4) already exists.
Aqui vemos que o valor padrão da coluna id da tabela é referenciado como “generated by default as identity” e não aparece o nome da sequência. Já na sequência aparece a referência “Sequence for identity column: public.teste_by_default.id”
Agora veremos que utilizando a opção ALWAYS, não é possível fazer a mesma coisa:
test=# CREATE TABLE teste_always (id integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY, nome varchar); LOG: statement: CREATE TABLE teste_always (id integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY, nome varchar); CREATE TABLE test=# \d teste_always Table "public.teste_always" Column | Type | Collation | Nullable | Default -------+-------------------+-----------+----------+------------------------------ id | integer | | not null | generated always as identity nome | character varying | | | Indexes: "teste_always_pkey" PRIMARY KEY, btree (id) test=# \d teste_always_id_seq Sequence "public.teste_always_id_seq" Type | Start | Minimum | Maximum | Increment | Cycles? | Cache ---------+-------+---------+------------+-----------+---------+------- integer | 1 | 1 | 2147483647 | 1 | no | 1 Sequence for identity column: public.teste_always.id test=# INSERT INTO teste_always (nome) VALUES ('Fábio'); INSERT 0 1 test=# INSERT INTO teste_always (nome) VALUES ('Paulo'); INSERT 0 1 test=# INSERT INTO teste_always (nome) VALUES ('João'); INSERT 0 1 test=# INSERT INTO teste_always (id, nome) VALUES (4,'Pedro'); ERROR: cannot insert into column "id" DETAIL: Column "id" is an identity column defined as GENERATED ALWAYS. HINT: Use OVERRIDING SYSTEM VALUE to override.
Agora o valor padrão da coluna aparece como “generated always as identity” e quando tentamos atribuir um valor arbitrário para a coluna id recebemos um erro. Mas logo abaixo vem uma dica de como podemos burlar isso, usando o OVERRIDING SYSTEM VALUE.
OVERRIDING VALUE
Existem duas opções para esta cláusula OVERRIDING SYSTEM VALUE e OVERRRIDING USER VALUE:
test=# INSERT INTO teste_always (id, nome) OVERRIDING SYSTEM VALUE VALUES (4, 'Pedro'); INSERT 0 1 test=# INSERT INTO teste_always (id, nome) OVERRIDING SYSTEM VALUE VALUES (5, 'José'); INSERT 0 1 test=# INSERT INTO teste_always (nome) VALUES ('Rita'); ERROR: duplicate key value violates unique constraint "teste_always_pkey" DETAIL: Key (id)=(4) already exists.
Ou seja, você pode sim utilizar um valor arbitrário seu, mas se o fizer, tem que tomar cuidado com o que está fazendo! O padrão SQL exige que você coloque a cláusula OVERRIDING SYSTEM VALUE, como um alerta para que você tenha certeza do que está fazendo.
Existe também a cláusula OVERRIDING USER VALUE, que pode ser utilizada numa situação onde você quer ignorar os valores arbitrários e utilizar apenas os valores da sequencia:
test=# INSERT INTO teste_by_default (id, nome) VALUES (42, 'Ana'); INSERT 0 1 test=# INSERT INTO teste_by_default (id, nome) VALUES (36, 'Paula'); INSERT 0 1 test=# INSERT INTO teste_by_default (id, nome) VALUES (111, 'Priscila'); INSERT 0 1 test=# SELECT * FROM teste_by_default; id | nome ----+---------- 1 | Fábio 2 | Paulo 3 | João 4 | Pedro 5 | José 42 | Ana 36 | Paula 111 | Priscila (8 rows) test=# CREATE TABLE teste_by_default2 (id integer PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY, nome varchar); LOG: statement: CREATE TABLE teste_by_default2 (id integer PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY, nome varchar); CREATE TABLE test=# INSERT INTO teste_by_default2 (id, nome) OVERRIDING USER VALUE SELECT * FROM teste_by_default; INSERT 0 8 test=# INSERT INTO teste_by_default2 (id, nome) OVERRIDING USER VALUE VALUES (4, 'Maria'); INSERT 0 1 test=# INSERT INTO teste_by_default2 (id, nome) OVERRIDING USER VALUE VALUES (6, 'Luiza'); INSERT 0 1 test=# SELECT * FROM teste_by_default2; id | nome ---+---------- 1 | Fábio 2 | Paulo 3 | João 4 | Pedro 5 | José 6 | Ana 7 | Paula 8 | Priscila 9 | Maria 10 | Luiza (10 rows)
Veja que toda a sequência da tabela teste_by_default2 ignora os valores que vieram da tabela teste_by_default ou mesmo os inseridos com valores arbitrários.
Um detalhe: você só pode utilizar esta opção em colunas definidas com o GENERATED BY DEFAULT, em colunas GENERATED ALWAYS você obtém o seguinte erro:
test=# INSERT INTO teste_always (id, nome) OVERRIDING USER VALUE VALUES (6, 'Luiza'); ERROR: cannot insert into column "id" DETAIL: Column "id" is an identity column defined as GENERATED ALWAYS. HINT: Use OVERRIDING SYSTEM VALUE to override.
Por último, você tem a opção de passar algumas opções para a sequência que será gerada implicitamente:
test=# CREATE TABLE teste_seq_opts (id integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY (INCREMENT BY 2 START WITH 100), nome varchar); LOG: statement: CREATE TABLE teste_seq_opts (id integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY (INCREMENT BY 2 START WITH 100), nome varchar); CREATE TABLE test=# \d teste_seq_opts; Table "public.teste_seq_opts" Column | Type | Collation | Nullable | Default -------+-------------------+-----------+----------+------------------------------ id | integer | | not null | generated always as identity nome | character varying | | | Indexes: "teste_seq_opts_pkey" PRIMARY KEY, btree (id) test=# \d teste_seq_opts_id_seq Sequence "public.teste_seq_opts_id_seq" Type | Start | Minimum | Maximum | Increment | Cycles? | Cache --------+-------+---------+------------+-----------+---------+------- integer | 100 | 1 | 2147483647 | 2 | no | 1 Sequence for identity column: public.teste_seq_opts.id test=# INSERT INTO teste_seq_opts (nome) VALUES ('Fábio'); INSERT 0 1 test=# INSERT INTO teste_seq_opts (nome) VALUES ('Telles'); INSERT 0 1 test=# INSERT INTO teste_seq_opts (nome) VALUES ('Rodriguez'); INSERT 0 1 test=# SELECT * FROM teste_seq_opts; id | nome ----+----------- 100 | Fábio 102 | Telles 104 | Rodriguez (3 rows)
Você não pode especificar o nome da sequencia que será criada. Seja utilizando o serial ou o IDENTITY, o nome da sequência sempre terá o formato <nome_da_tabela>_<nome da coluna>_seq.
Bom, recomendo que você faça alguns testes com chaves artificiais e se já estiver utilizando o PostgreSQL 10, passe a adotar o IDENTITY COLUMN ao invés do serial. Além deste formato ser mais robusto e apresentar menos problemas, ele também é padrão SQL, o que ajuda na portabilidade do seu código. Qualquer dúvida, deixe um comentário aqui.
Continuação…
Publiquei outro artigo com ênfase no desempenho de chaves artificiais utilizando sequencias e UUID, confira!