Práticas de design de banco de dados, parte 1

Quais são as melhores práticas quando está desenvolvendo o esquema de uma banco de dados relacional? Qual é a decisão mais racional na hora de decidir em favor de uma no lugar de outra alternativa? Dado a quantidade de recomendações de cada tipo de banco, é muito fácil negligenciar os fundamentos básicos dos bancos de dados relacionais.

Neste artigo de duas parte, traduzido do autor Philipp K. Janert, será apresentada uma introdução a normatização de banco de dados e as cinco Formas Normais. Ele também discorre outros possíveis usos para um banco de dados em um projeto, por exemplo como um repositório para uma configuração ou dados de log.

Na primeira parte do artigo, iremos discutir algumas práticas boas e gerais que podem ser úteis. Nenhuma delas é específica a algum produto específico e todas, devem, portante, ser aplicáveis não importa qual implementação de banco de dados é usada. Na conclusão desse artigo, tentará ser fornecida uma introdução adequada ao tópico de normatização de banco de dados e as cinco Formas Normais. Será discutido também possíveis usos para um banco de dados.

Chaves primárias e questões relacionadas

Um banco de dados relacional armazena dois tipos de informação – dados e canais. Os dados compreendem nomes de clientes, números de inventário, descrições de itens, e assim em diante, que a aplicação usará. Canais referem-se as chaves primárias e estrangeiras que o banco necessita para achar os registros da base de dados e relaciona-los entre si.

Canalização básica

Para o propósito da modelagem de dados, a canalização deve ser largamente transparente. De fato, puristas de banco de dados não sabem fazer distinção entre dados e canais. Todavia, você verá que é mais eficiente para administração e manutenção, assim como em se tratando de performance de execução, ter alguns campos adicionais que servirão como chaves do banco de dados.

Cada tabela precisa ter uma chave primária: um atributo ou combinação de atributos que são garantidamente únicos e não nulos. Geralmente é útil introduzir uma surrogate key – um atributo da tabela que não possui significado para o negócio, mas simplesmente serve como identificados único para cada registro da tabela. Isso é o canal que temos nos referido.

Os requisitos para uma chave primária são bastantes estritos. Ela deve:

  • Existir
  • Ser única
  • Não mudar com o passar do tempo

As surrogates keys ajudam a atenuar o fato que os dados de negócios reais nunca preenche com segurança esses requisitos. Nem todas as pessoas possuem um número de RG ou CPF, pessoas mudam seus nomes, e outras informações importantes.

Dados de negócio podem também ser simplesmente ruins – falhas do sistema de registro geral ou do cadastro da Receita podem levar diferentes pessoas a ter o mesmo número. Uma surrogate key ajuda a isolar o sistema de problemas como esses.

A segunda razão que essas chaves são favoráveis tem a ver como eficiência e fácil manutenção, já que você pode escolher o tipo mais eficiente para ela. Além do mais, esse tipo de chave tipicamente é um campo único (não uma chave composta), o que simplifica o esquema (particularmente quando a chave é usada em outras tabelas como chave estrangeira).

Cada tabela deve ter uma coluna dedicada a função de chave primária da tabela. Essa coluna pode ser chamada id ou pk (ou possivelmente <table_name>_id ou <table_name>_pk). Muitos banco de dados são refinados para fazer consultas a inteiros, por isso faz sentido usar esse tipo para a chave primária. Muitos banco de dados, incluindo Postgres e Oracle, também fornecem um tipo especial chamado serial ou sequence, que gera uma sequência de inteiros únicos. Declarando uma coluna desse tipo garante que uma chave única é gerada para cada linha inserida.

Chaves estrangeiras são atributos da tabela, cujos valores que são chaves primárias em outra tabela. Frequentemente faz sentido nomear colunas para chaves estrangeiras explicitamente, por exemplo, adotando uma convenção de nomenclatura como <other_table_name>_fk. Uma restrição de integridade referencial (references) deve ser declarado como parte da sentença CREATE quando criar a tabela.

Vale a pena repetir que surrogate keys discutidas anteriormente são parte da canalização apenas – sua existência não evita que os requisitos de modelagem sejam capazes de formar uma chave primária a partir dos dados de negócio. Como o candidato a chave de um dado do negócio é um sub-conjunto de todos os atributos, os valores desses nunca devem ser nulos, e cada combinação de valores é único. Como verificação para uma modelagem correta, cada candidato deve existir e deve ser documentado para cada tabela.

Estritamente falando, você pode nem sempre encontrar um candidato a chave entre os dados do negócio. Imagine uma tabela que registre os primeiro e último nome de cada usuário, mas não tenha nenhum outro atributo. Agora assume que existam duas pessoas diferentes, ambas tendo o primeiro nome “Joe” ‘ o segundo nome “Blow”. Nesse caso, não haverá nenhuma combinação de atributos da tabela que possam formar uma chave aceitável.

O problema básico aqui é que ou você estaria falando sobre a unicidade dos conjuntos de dados ou sobre a unicidade das entidades básicas – usuários, nesse exemplo. Geralmente é mais intuitivo, em particular para desenvolvedores acostumados a analise orientada a objetos, modelar a unicidade de entidades básicas. As surrogate keys discutidas anteriormente podem ajudar a resolver isso

Chaves alternativas e identificadores visíveis

Como parte da canalização, as surrogate keys não precisam ser sempre visíveis fora do banco de dados. Em particular, nunca deve deve ser revelada ao usuário. Isso permite que o administrador do banco de dado altere a representação da chave quando necessário. Se um negócio precisa fornecer ao usuário um identificador único para um conjunto de dados em particular, esse identificador deve ser considerado um dado do negócio e manter-se separado da canalização. Por exemplo, uma coluna adicional chamada VisibleAccountNumber ou algo do tipo pode ser introduzida na tabela. Naturalmente, esse atributo deve ser não nulo e único e por isso ser uma candidata a chave (uma chave alternativa). Ter uma coluna separada para identificadores visíveis também torna possível gerar e formatar valores para esse atributo de uma forma amigável ao usuário.

Um caso incerto é quando o identificador não é diretamente visível, mas ainda pode ser acessível pelo usuário. Exemplos incluem campos ocultos em páginas Web onde um identificador é lançado pelo cliente para ser usado como parâmetro em uma requisição. Embora não haja necessidade do usuário manipular o identificador, um usuário malicioso pode ler e tentar burla-lo. Usando um valor numérico de uma chave primária diretamente, em principio, permite que um invasor acesse toda a tabela!

Defesas contra esse problema incluem desde encriptação e decriptação do valor da chave primária, ou proteger a chave pela anexação de um MAC (Message Authentication Code). Uma alternativa é usar um identificador visível difícil de burlar para a tabela, como o hash das chaves primárias dos registros ou uma timestamp (naturalmente, a unicidade desse atributo precisa ser assegurado).

Se a chave será visível para a aplicação (e oculta para o usuário) depende da especificação do projeto. Usando um tipo numérico diretamente carrega a representação do base dados da  chave direto ao código da aplicação e deve ser evitado para prevenir acoplamento. Em desenvolvimento de pequeno porte, uma representação String do valor da chave pode ser aceitável (todos os tipos de dados que podem ser armazenados em uma banco de dados precisam ser capazes de ser serializados).

Mas um solução melhor é um simples objeto empacotador que adiciona pouca complexidade, mas fornece um forte desacoplamento da representação da chave de sua interface. Um perigo existe em fazer com que o objeto empacotador muito esperto. A intenção em usar surrogate keys é fazê-las simples e eficientes para manipulação pelo banco de dados. Configuradas de um valor do banco de dados e possivelmente de uma String, comparando com outro objeto chave, e possivelmente serialização são todos os métodos necessários. Espertos, como a habilidade de verificar o conteúdo baseado no cálculo de um checksum, sugeri que esse objeto provavelmente pertence ao domínio dos dados do negócio (como um identificador de registro visível, mencionado anteriormente).

O problema do Identificador Único Universal – UUID

Uma consideração final è sobre a possível necessidade de um UUID (Universally Unique Identifier – Identificador Único Universal). A resposta curta é que banco de dados relacionais não precisam de UUIDs de forma nenhuma. De fato, todo o conceito de UUID de forma alguma é relacionado ao gerenciamento de banco de dados relacionais. Chaves de banco de dados relacionais – os canais – precisam apenas ser únicos por tabela, o que pode ser alcançado usando um tipo de dados com auto-incremento como o tipo serial mencionado antes.

Os UUISs podem ter algumas dificuldades técnicas. Para garantir a unicidade, todos os UUIDs precisam ser gerados por um serviço centralizado – o que acarreta problemas de escalabilidade e pode se tornar um ponto único de falha (O problema de escalabilidade pode ser mitigado por uma abordagem estratificada na qual um mestre central é usado para distribuir sementes para diversos clientes, que por sua vez gera os identificadores finais em lotes, e assim em diante). Para representar um UUID em um banco de dados, use ou uma string ou uma chave composta que envolva várias colunas de atributos inteiros. Ambas abordagens são significativamente mais lentas que operações baseadas em chaves compostas por inteiros longos; Chaves compostas também aumentam a complexidade do esquema do banco de dados quando usadas como chave estrangeira.

No final, se um registro precisa ter o não um ID verdadeiramente único e global, isso é ditado pelas  regras do negócio, não pela arquitetura do banco de dados. Alguns registros podem já conter alguma forma de UUID (itens de merchandise, por exemplo, tipicamente possuem um Universal Product Code como código de barras). Alguns outros registros, possivelmente correspondentes as entidades do negócio principal, podem ao contrário já conter um identificador único como parte de seus dados de negócio (como uma combinação da hora e nome da conta para uma entrada de um livro-razão). Se seu caso não se encaixa em nenhum desses, um UUID pode ser gerado e armazenado ao lado dos dados de negócio para esses registros que precisam de uma. Em qualquer caso, as UUIDs devem ser consideradas parte dos dados de negócio – não dos canais.

Mesmo se (e nesse caso um grande se) a abordagem para o mapeamento objeto-relacional precise que cada objeto do negócio tenha um ID persistente e único, não existe necessidade de basear o trabalho interno no mecanismo básico do banco de dados relacional.

Em resumo, é possível argumentar que deve-se manter os dados do negócio separados da canalização interna do banco de dados. Construir um banco de dados em volta de um UUID quebra esse principio pelo uso de atributos, que, no fim, são parte dos dados de negócio, como infraestrutura interna. (Para um ponto de vista completamente diferente desse problema e uma discussão cuidadosa do problema envolvendo a geração de UUIDs de uma maneira escalável, leia o artigo de Scott ambler, “Mapping objects to relational databases”, na seção de Recursos).

Tipos de dados

O padrão SQL define um número de padrões para tipos de dados e muitos produtos suportam alguns tipos adicionais que são específicos de cada plataforma. Na ausência de razões totalmente convincentes para o contrário, evite essas extensões para conseguir portabilidade.

Strings e números

Em geral, tipos de dados numéricos tem poucos problemas – apenas selecione um que seja grande o suficiente para suporta a faixa de valores necessária.
A tentativa de encontrar a largura ótima de uma coluna string normalmente não vale o esforço. Você pode evitar muita confusão mais tarde fazendo todas as mensagens de texto do tipo varcha(n) e limitando você mesmo a uns poucos padrões de tamanho de string e introduzindo apelidos para eles, como: 32 bytes (Label), 256 bytes (Nota) e 4k (Text).

Mesmo se os requisitos do negócio restringirem o tamanho máximo de certos campos a valores específicos, o esquema do banco de dados não é, indiscutivelmente, o lugar para forçar essas regras. Quando os dados alcancem a base de dados, é muito tarde para fazer alguma coisa sobre isso (exceto rejeita-las). Restrições individuais, decorrentes das regras e requisitos do negócio, devem ser executadas pela camada da lógica de negócio, que manipula a interação com o usuário e a validação dos dados. De outra forma, a manutenção do esquema do banco de dados é consideravelmente simplificada se for restrita a um punhado de atributos de strings diferentes.

Limite o uso de strings de tamanho fixo para códigos de todos os tipos (em oposição a strings de comprimento variável para texto). Mantenha em mente, porém, que muitos códigos que aparentam ser de comprimento fixo realmente tornam-se mais amplo ao longo do tempo. O arquiteto de banco de dados prudente tenta evitar qualquer coisa parecida com o problema do ano 2000 para novos trabalhos de desenvolvimento.

Tempo e moeda

Um tipo para registrar timestamps (combinações de data e hora) é sempre necessário e, felizmente, coberto pelo padrão SQL. Não existe uma maneira completamente satisfatória de registrar um valor monetário, todavia.
Salvar valores monetários e trata-los no código do programa como valores de ponto flutuante sempre ocasiona erros de arredondamento. Registrar os valores como inteiros exatos da menor sub-divisão monetária (como o “cent” para dolares americanos, assim como Euros e outras moedas) pode não ser suficiente também. Muitos valores carregam mais dígitos após o ponto decimal que os dois que normalmente existem (só visite um posto de gasolina próximo a você). Uma escolha de decimal com 5 a 9 dígitos deve funcionar, porém.

Desnecessário dizer que nenhum valor monetário deve ser gravado sem o registro da moeda – mesmo se você achar que sua aplicação irá manipular uma outra moeda além do Real (ou Dolar americano, ou Euro, etc). Considere Considere criar uma tabela de moedas e relaciona-la com o valores monetários usando chaves estrangeiras ao invés de embutir a informação da moeda diretamente. Isso ajuda com a internacionalização (diferentes nomes de moedas  e símbolos), assim como com problemas de formatação.

Booleanos e a Tabela de Tipos dos Sistema

O uso do tipo bool em qualquer lugar do esquema é uma dica para repensar esse módulo em particular. Muitos poucos atributos são verdadeiramente limitados a apenas dois valores – mesmo uma coluna “gênero” tem uma tendência maliciosa de transformar-se em pelo menos estados – macho, fêmea e desconhecido. Permitir valor nulo apenas mascara o problema real. uma abordagem mais flexível para códigos de tipos é necessária.

Em muitos lugares do banco de dados, os atributos determinam um tipo de registro de alguma maneira. O Tipo de Gênero mencionado acuna é um exemplo. Outros exemplos incluem ItemType (como Merchandise, Freight, Packagin, Insurance), PaymentType (Cash,Check,MoneyOrder, CreditCard, Barter) e coisas como StoreType, MembershipType, DeviceType e muitos mais. Isso também inclui exemplos em que você quer armazenar o tipo de um objeto em um modelo objetos apropriado.

Parta cada tipo, você precisa de alguma forma de documentação que lhe diga não apenas o nome do tipo, mas também as características associadas a ele; por exemplo, você pode queres saber quais permissões cada UserType tem. Que lugar melhor para manter essa informação do que a própria base de dados?
Qualquer registro que tenha a forma de informação de tipo associada a ele deve conter uma coluna de código do tipo, que será uma chave estrangeira que referencia uma tabela de códigos. A chave estrangeira garante que nenhuma registro com tipo inválido exista. A tabela de códigos de tipo deve ter os seguintes atributos:

  • typeCode_pk
  • label (nome único, como um varchar(32))
  • description (varchar(256) deve ser suficiente)
  • uri (apontando para recursos adicionais, onde for necessário)
  • codeGroup_fk

Atributos adicionais são, naturalmente, concebíveis – como um código de três letras ou um código numérico visível.

O atributo codeGroup_fk serve para organizar tipos relacionados. Esse atributo é uma chave estrangeira para uma tabela a parte de códigos de grupo. Porém, ao perceber que um código de grupo é nada mais que um código de tipo, esse relacionamento pode se tornar recursivo, assim codeGroup_fk referencia typeCode_pk. Não apenas isso  torna a tabela de códigos de tipo desnecessária, mas também torna possível ordenar os grupos em uma estrutura hierárquica. É melhor manter o sistema de códigos de tipo relativamente simples e direta.

Tipos de dados complexos

Finalmente, existem alguns tipos de dados comuns mais complexos – como números e telefone, endereços, informação de contato e cartões de crédito – que ocorrem em quase todo esquema de banco de dados. Tipicamente, esses registros precisam ser acessados de muitas tabelas do banco de dados. Em um sistema de e-commerce típico, por exemplo, pode ser necessário armazenar informações de contato para usuários, fornecedores, armazéns e administradores.

Ao invés de incluir esses atributo no respectivo usuário, fornecedor ou outro registro (e assim repetir esses campos por todo banco de dados), faz sentido criar uma tabela única para a informação de contato que é referenciado por uma chave estrangeira por todas as outras tabelas. Isso tem dois benefícios imediatos:

  • É mais fácil fazer alterações futuras na cardinalidade dos relacionamentos.
  • Localiza qualquer alteração futura em tipos de dados complexos.

Antecipar os atributos que provavelmente serão necessários para cada um desses tipos complexos é quase uma arte. Minha sugestão é tentar lidar com o problema por completo desde o início ao invés de ser ficar a mudanmdo o esquema de cada vez que um campo adicional se tornar necessário.
Uma amostra de possíveis atributos para um endereço postal seria:

  • Department
  • Company
  • Mail Stop
  • Address Line 1
  • Address Line 2
  • Address Line 3
  • City
  • State
  • Postal Code (“Zip”)
  • Country

Informação de contato completa deve incluir os seguintes campos:

  • Title
  • First Name
  • Middle Name (or Initial)
  • Last Name
  • Suffix (such as jr. or sr.)
  • Home address
  • Work address
  • Home Phone
  • Work Phone
  • Cell Phone
  • Fax
  • Pager
  • Email

Finalmente, números de telefone nunca devem ser considerados como números planos. De fato, eles se dividem nos seguintes campos:

  • CountryCode
  • AreaCode (Number Plan Area)
  • ExchangeCode (Prefix)
  • LineNumber (Suffix)
  • Extension

Em um número de telefone como 987-1234, o prefixo seria 987 e o sufixo 1234. A extensão é a única parte do número que é opcional. Provavelmente é suficiente usar char(4) para todas as colunas, mas pode-se considerar usar char(6) por segurança.

Dados sensíveis devem ser mantidos encriptados. Mesmo se o banco de dados seja comprometido, os dados ainda estarão protegidos de mal uso.  O exemplo mais famoso desse tipo de gerenciamento é o sistema de senhas do Unix que apenas armazena hashes para a senha do usuário ao invés das senhas em si. Alguns dados, como números de cartão de crédito, precisam ser encriptados de uma forma recuperável; porém, uma encriptação de uma via (como o do arquivo de senhas do Unix) não servirá. Isso leva ao problema do gerenciamento da chave de encriptação – claramente não deve ser armazenado no banco de dados, junto como os segredos, mas fornecido durante a inicialização, por exemplo.

Sumário e Recursos

Nesse artigo, discutimos algumas boas práticas para o desenvolvimento de bancos de dados relacionais, incluindo:

  • Os benefícios de manter os atributos da tabela sem relação com nenhuma regra do negócio para servir de surrogate keys.
  • A recomendação de não basear o funcionamento interno da base de dados em uma UUID.
  • O uso de um código de tipo centralizado facilita expressar atributos com faixas de valores finitos e pre-determinados.
  • Algumas considerações sobre tipos de dados complexos que são usados ao longo do esquema do banco de dados.

Na próxima metade desse artigo, cobriremos normalização e alguns usos adicionais para uma base de dados dentro de um projeto.

Recursos

  • Learn about the importance of the surrogate primary key when designing stable, flexible, and well-performing enterprise databases in Mike Lonigro’s article, “The Case for the Surrogate Key.”
  • Read the book, Translucent Databases, as Peter Wayner offers a better, deeper protection paradigm for databases, one that works by scrambling the data with encryption algorithms, using the minimal amount of encryption that ensures the database is still highly functional.
  • Search developerWorks‘ vast library for more than 1,000 articles on database design.
  • Discover how DB2 Relational Connect helps with information integration by allowing several databases to be viewed and manipulated as one.
  • Look at this white paper on Comprehensive, flexible backup and recovery for relational databases.

Traduzido de