Mês: novembro 2009

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.

Anúncios