IDs x Chaves Primárias Compostas

Objetivo

O objetivo deste artigo é discutir um pouco sobre técnicas de modelagem de bancos de dados relacionais. Mesmo com o uso constante de frameworks de mapeamento objeto/relacional (NHibernate, Entity Framework, iBatis e outros), uma boa modelagem de banco de dados continua sendo muito necessária.

Mesmo com o crescimento de abordagens de modelagem de aplicações totalmente orientadas a objetos, o uso de BD’s relacionais parece não cair em desuso. Por isso acabei investindo nesta idéia.

Existem muitas armadilhas quando definindo um modelo entidade/relacionamento e impactos relativamente fáceis de serem previstos e contornados, caso conhecidos em tempo de modelagem (depois que a aplicação está em produção, qualquer mudança é bem complexa).

Por isso essa discussão de chaves compostas x ID’s

Definindo a chave primária (Primary Key)

O conceito de chave primária é uma forma de identificar unicamente um registro numa relação (os acadêmicos gostam de chamar tabelas de relações. Daqui pra frente vou usar tabela, porque é como é comumente conhecido). Chamamos de chave primária justamente pq é a “principal”. Ela será usada em todo e qualquer relacionamento com outras entidades (chaves estrangeiras, ou foreign keys).

Existem algumas discussões sobre a melhor forma de definí-las, que basicamente encontram-se nos dois pontos:

Usar ou não usar campos da própria entidade?

Chave primária com campos da própria entidade

Geralmente tem-se a idéia de utilizar um dado do próprio modelo de negócio para identificar unicamente o registro. Um exemplo disso, seria definir uma entidade “Cliente”, como a seguir:

Nesse caso estamos definindo um campo CPFCNPJ, do tipo varchar (string) como chave primária. Ou seja, o próprio CPF ou CNPJ do cliente será definido como chave.

Geralmente essa idéia vem da pergunta. Se eu já tenho um campo que identifica unicamente o registro, pra que criar outro?

Chave primária com identificador automático (popularmente conhecido como Id)

Invés de usarmos o próprio CPF/CNPJ como chave, vamos criar um campo “burro” (Id) para servir como chave primária. Nesse caso, nada impede que definimos uma chave alternativa (alternate key ou unique index) para impedir que sejam cadastrados dois clientes com o mesmo CPF/CNPJ. No modelo, ficaríamos com a seguinte idéia:

Usar chaves compostas?

A idéia de chave composta é ter mais de um campo na chave primária. Em meios acadêmicos essa é a forma comumente apresentada nos livros “clássicos”. Eu vou expor as duas formas de resolver esse problema, e posteriormente discutiremos os benefícios e problemas gerados por cada uma delas.

O exemplo usado será um modelo simples, com as entidades cliente, pedido, item de pedido e produto, com as características:

  • Cada pedido possui um cliente. Cada cliente pode estar em um ou mais pedidos.
  • Cada pedido possui “n” itens. Aqui ocorre um relacionamento master/detail ou pai/filho entre essas tabelas.
  • Cada item de pedido possui um produto. Cada produto pode estar em um ou mais itens de pedido.

Modelo 1: Exemplo de relacionamento master/detail com chave composta

No exemplo acima, a tabela Cliente segue o conceito de usar um campo da própria entidade para identificá-la unicamente. No caso o CPF/CNPJ é ideal.

O relacionamento usado entre Cliente e Pedido aparece em muitas ferramentas como relacionamento não-identificado. É quando ele não é exportado para a chave primária da tabela destino. Nesse caso, apenas é criada uma foreign key na tabela destino para garantir que não pode entrar registro na tabela Pedido, caso o CPFCNPJ informado não exista na tabela pedido. Não faz sentido colocar o campo CPFCNPJ na tabela de pedido já que por regra de negócio eu não posso ter pedidos com código duplicado e clientes diferentes.

Já a chave primária da tabela Pedido é CodPedido. Vamos pensar no código de pedido como um número sequencial conhecido pelo usuário já que não existe outro atributo melhor na entidade pedido para fazer essa função.

Já o relacionamento entre Pedido e ItemPedido é conhecido como relacionamento identificado. É quando ele exporta para a tabela destino sua chave primária. A tabela item de pedido “ganha” o código de pedido em sua chave primária. Nesse caso faz sentido, pq eu posso ter código do item de pedido duplicado entre pedidos diferentes.

O relacionamento entre produto e item de pedido também é não-dentificado (não exporta para a chave primária). Nesse caso, propositalmente coloquei o código do produto como varchar, para identificar que é um código que o usuário conhece.

Modelo 2: Exemplo de relacionamento master/detail com Id no lugar da chave composta

O modelo acima é a mesma idéia do anterior, utilizando a idéia de Id’s e sem chaves compostas. Vamos explicar cada uma das entidades.

Na tabela cliente, usamos o campo ClienteID (identity) como um identificador único na chave primária. A idéia aqui é criar um Id interno, invisível para o usuário final, somente para administrar os relacionamentos entre as entidades. Para o usuário final, criamos uma chave alternativa (alternate key ou unique index) no campo CPFCNPJ, para impedir que sejam inseridos 2 registros com mesmo CPF ou CNPJ.

Na tabela Pedido, usamos o campo PedidoID (identity) como chave primária. É a mesma idéia da tabela cliente. O código visível para o usuário final, CodPedido fica em outro atributo, fora da chave. Deve ser criada uma chave alternativa para evitar dois pedidos com o mesmo código de usuário.

O relacionamento entre pedido e cliente é não-identificado, ou seja, não exporta para a chave primária.

A tabela ItemPedido continua sendo filha da tabela Pedido, com a diferença que ela não recebe mais o código do pedido na chave primária. Este passa a ser um relacionamento não identificado, mas não impede que se localize para qual pedido este item pertence.

A tabela de Produto segue a mesma idéia da de Cliente e Pedido. Ganha o Id e o código visível para o usuário.

Análise das duas abordagens

Para conseguirmos ter uma idéia de qual das duas abordagens gera mais benefícios ou problemas, vamos analisar alguns critérios isoladamente.

Manutenção de Códigos

Quando descrevendo o campo CPFCNPJ no modelo 1, usei a pergunta: Se eu já tenho um campo que identifica unicamente o registro, pra que criar outro? A resposta é: Para simplificar a manutenção da aplicação em produção.

Sempre que trabalhamos com códigos da própria entidade na chave primária caímos num problema de manutenção. Olhando para o modelo 1, percebemos que a tabela de Pedido ganha o CPFCNPJ do cliente. A tabela ItemPedido o código do produto e assim sucessivamente.

As vezes acontecem em 3, 4, 5 e níveis jamais imaginados quando a aplicação vai crescendo. Exemplo: se surgir uma tabela “ProgramacaoEntrega” por item de pedido, ele vai ganhar o código do pedido, do item e da programação.

Como esses códigos são “vistos” pelo usuário, eventualmente precisamos de alguma manutenção. Quando precisamos fazer esse tipo de update, vamos cair em violações de foreign key. Aí temos que dar um insert num novo registro e ir dando update nas filhas, que podem gerar impactos parecidos. Em bases com grandes volumes de dados, essas operações são bastante desgastantes.

Acredito que esse seja um ponto importante, já que hoje em dia as equipes são muito reduzidas e não existem DBA’s para ficar fazendo esse tipo de manutenção.

Existe a opção de cascade constraints (cascade updates, cascade deletes), que também eventualmente geram outros problemas como transações muito grandes (Sql Server) gerando locks enormes na base de dados ou mesmo estouro de rollback segment no Oracle.

Tipos de Dados

As vezes caímos em algumas armadilhas por causa do tipo de dados que jogamos na chave primária. No modelo 1, vamos pensar na modelagem da tabela de ProgramacaoEntrega citada acima.

Pensemos numa entidade que para cada item de pedido, permita que sejam informadas “n” datas de entrega com a respectiva quantidade naquela data.

Nesse caso, a solução “ideal” seria jogar a própria informação da entidade como chave. No caso a data da programação de entrega.

Data é um tipo complicado. Se existe um milisegundo de diferença entre duas datas, as datas são diferentes, consequentemente o modelo aceitaria duas datas iguais com hora diferente (o que não atende a regra de negócio).

No Modelo 1 proposto a solução nesse caso seria colocar um código para a programação de entrega, somente para evitar o problema com a data na chave primária.

O mesmo problema ocorre para números float na chave primária e outros.

Aumento do número de campos nas tabelas

Nesse caso, as duas abordagens tem seus problemas. É muito esquisito ver uma tabela com PedidoID e CodPedido, parece uma redundância, mas se pensarmos num como identificador de relacionamento e outro como código visto pelo usuário, a idéia de redundância desaparece.

Olhando o exemplo acima, parece que o modelo 2 aumenta o número de campos, mas em outras situações como na tabela ItemPedido (geralmente tabelas filhas, netas, bisnetas), seguindo o modelo 1 aumenta-se muito o número de campos da entidade e na chave primária.

Performance em Joins

Em alguns casos, no exemplo da tabela fictícia ProgramacaoEntrega, ter o código do pedido na chave economizaria um join em ItemPedido para pegar informações do Pedido, se pensarmos no Modelo 1.

No modelo 2, teríamos que obrigatóriamente fazer join para ItemPedido e posteriormente para Pedido.

Particularmente nunca observei problemas de performance graves usando o modelo 2. Visto que é um join direto, por chave primária o custo dessa operação não é muito grande, principalmente para as máquinas com os recursos de hoje. Geralmente as chaves primárias são clusterizadas e tem um custo baixo para esse busca.

São raros outros casos em que o modelo 1 é mais econômico que o 2 em “economia de joins”.

Desenvolvimento das aplicações – Passagem de valores

Por último, vamos sair um pouco do banco de dados e pensar nas aplicações.

A maioria dos componentes visuais em qualquer linguagem e ambiente são orientados à seleção de um único valor. Exemplo: ListBox tem um único Selected Value, um combobox tem um único selected value.

Por causa das chaves compostas (modelo 1) as vezes para conseguir usar um combo box para selecionar determinado valor, precisa-se fazer operações de “concatenar” valores e depois fazer um parse deles novamente, devido a essa “restrição”. Esse problema é inexistente no modelo 2. Para ilustrar melhor esse ponto, é só pensar num list box que seleciona um item de pedido baseado numa lista de valores que possuem pedido + item (nesse exemplo não faz sentido, mas em muitos outros faz).

Um outro ponto relacionado ao desenvolvimento das aplicações está na passagem de valores. Quando se tem chaves com valores “string”, “data” e precisa-se passar esses valores em aplicações Web, utiliza-se de QueryString ou passagem de parâmetros via Post. Nesse caso, envolve um esforço de parse para enviar e recuperar a informação, que podem gerar outros problemas como encoding (ANSI/UTF8) para strings, ou mesmo UrlEncode/UrlDecode de caracteres especiais.

Conclusão

A pergunta que não quer calar… qual é a melhor abordagem?

Como quase tudo em TI, depende. Depende da aplicação, dos padrões impostos por clientes, da equipe e de uma série de fatores.

Eu particularmente prefiro o modelo 2. Trabalho a muitos anos já modelando dessa forma e por várias vezes já fui salvo por ela, principalmente no critério “manutenção”.

Não existe certo e errado para essas questões de modelagem. Ambas abordagens permitem que as regras de normalização, eliminação de redundância e outras sejam aplicadas.

Espero que com esse artigo, mais informações possam ser colocadas nas apimentadas discussões que esses assuntos acabam gerando e os critérios expostos possam ser melhor analisados.

8 thoughts on “IDs x Chaves Primárias Compostas

  1. Eric, gostei muito do tema pois, costumo também, usar o “modelo 2”.Devido a forte influência/raiz acadêmica me deparei modelando pelo “modelo 1”. Como eu também desenvolvo, e costumo fazer via IDs, comecei a me indagar se deveria ou não tentar usar o “modelo 1” ( que não gosto muito) por ser “academicamente correto”…. No impasse, resolvi pesquisar algo e me deparei com este Blog. Parabéns por sua análise que foi muito bem transmitida. Pode parecer um assunto simples mas, nós que buscamos a razão dos fatos, sabemos que gera dúvidas e, no nosso mundo virtual, precisamos ser precisos em nossas análises e não apenas fazer sem medir conseqüências…

    1. Antonio,

      Que bom que você gostou. Eu particularmente gosto de abordagens mais práticas. Eu preciso fazer uma revisão mais formal e acadêmica, em cima das formas normais, porém, não acredito que o uso de chaves substitutas (surrogate keys) seja contrário a qualquer um dos princípios acadêmicos.

      Na verdade, acredito que a maioria segue o modelo com chave composta porque a maioria das pessoas sempre fez assim e ninguém parou pra questionar os prós e contras das duas abordagens. Era mais comum nos bancos de dados antigos, em faculdades procurava-se essa abordagem e acabou virando uma verdade absoluta o uso de chaves compostas. Acho que esse tema merece mais estudo.

      Abraço,

      Eric

  2. Nossa, que tema legal, pouco discutido, mas muito importante, a muito tempo para cá tenho usado o os IDs (modelo 2) e é muito satisfatório, as queries tendem a aumentar sim, mas não é tão custoso, parece que as coisas ficam mais claras.

    Começei a usar o modelo 2 por dedução própria de que seria mais vantajoso, mas confesso que nunca consegui explicar para quem usa o modelo 1 o porquê de mudar, seu artigo ficou muito bem elaborado, parabéns!

  3. Imagino que o custo de execução é maior, no 1o modo, em vários casos. No modo 1, tende-se a utilizar muitos VARCHAR como chave, o que, teoricamente, aumentaria muito o trabalho interno do banco de dados. Tanto que, após trocar as chaves por um simples ID numérico, os joins são feitos mais rapidamente (se tratando de um banco de dados com MUITA informação e também já considerando os índices).
    Outra coisa, escrever inner joins com chaves compostas é um pé no saco!! Fala sério.

  4. Eric, boa tarde… muito interessante seu artigo. Concordo com você que cada caso é um caso. O que sempre questiono no modelo de surrogate keys é que muitos confundem com integridade referencial do banco de dados. As chaves compostas podem ser geradas a partir de chaves substitutas sem maiores problemas. Abordemos o cado de uma estrutura geográfica onde eu tenho pais, estado, município e bairro. para todas eu posso ter um chave substituta tipo identity, autonumerada, e mesmo assim nas tabelas filhas utilizar a PK da tabela pai para garantir a integridade em todos os níveis. Nesse caso a PK de bairro seria idpais, idestado, idmunicipio e idbairro. A questão é: Tendo eu vinculação entre bairro e município, a vinculação deste com o estado não é direta? e minha resposta é: Talvez. caso ocorra alteração do código do estado na tabela de municipio, todos os bairros daquele municipio migrariam automaticamente para o novo estado, o que seria impedido no caso de integridade completa. Sei que o exemplo pode ser um tanto esdruxulo, mas em estruturas em que a integridade deve ser garantida a qualquer custo, a chave composta deve ser utilizada, em casos em que o impacto no sistema seria menos relevante a utilização das chaves únicas com FKs não obrigatórias pode ser mais “barata”. Ou seja, é quase como discutir sexo dos anjos…

    Grande abraço… Parabéns pela iniciativa.

    PS. A título de curiosidade, creio que os bancos de dados relacionais terão ainda longa vida a frente. Mesmo com as tecnologias OO em adiantado estado e com desenvolvimento ainda a todo vapor, o volume de dados legados em bancos de dados relacionais espalhados pelo mundo é tamanho que a migração destas estruturas ainda é utópica. Em 2009 já tínhamos aproximadamente 1,8 Zettabytes armazenados.

    1. Hemerson,

      Obrigado pelo comentário. Não sei se eu gosto da abordagem de usar chaves substitutas e compostas, mas precisaria raciocinar melhor os prós e contras dessa abordagem.

      O exemplo que você citou como integridade completa, eu vejo como sendo atingida da mesma maneira. Na minha cabeça, se um estado muda de país, automaticamente tudo que está debaixo daquele estado deveria mudar também justamente para garantir a integridade. Se o estado pode mudar de país ou não, entendo que é uma integridade da aplicação e não dos dados.

      Vejo que nas chaves compostas tradicionais dificultam significativamente esse tipo de manutenção de dados, exigindo a inserção de um novo registro, atualização dos dados e inclusão de um novo registro, por isso sou bastante contrário a essa abordagem.

      Abraço,

      Eric

  5. Ola Eric.
    Vamos supor que eu usasse o campo ClienteID nas telas da aplicação para o usuário informar o codigo. Seria tambem surrogate key?

    1. Felipe,

      No meu entendimento, não. Se o cliente conhece o código, vc não pode mudar esse código no caso de manutenção do banco de dados, o que inviabiliza todo o processo.

      Abraço,

      Eric

Leave a reply to Antonio Marcos Souza Cancel reply