Normalize seus dados! Ajude seu eu do futuro na jornada SQL.
Tenho um gosto pessoal por estudar as bases e teorias antes de fazer uma tarefa. Raramente me arrependo de “estudar demais” mas as vezes ainda faço as coisas apenas porque “não vejo o porquê de não fazer — NVPNF”.
Uma das coisas que fazia por NVPNF era aplicar as formas normais sempre que criava uma tabela para um banco de dados SQL. Ao meu olhar de iniciante parecia uma complexidade a mais, uma tabela a mais, um relacionamento a mais. E realmente era, até que os projetos começaram a crescer e eu agradeci muito por ter normalizado minhas tabelas.
Meu objetivo com esse artigo é explicar o que são cada uma das FNs (Forma Normal), como aplica-las, e comentar sobre os problemas que elas resolvem. Vale lembrar que nem tudo é absoluto, se seu objetivo é um projeto pequeno, é estudar um framework, criar um lista de compras, ou se você simplesmente não tem ideia de quais dados armazenar, talvez aplicar FNs pode ser uma perda de tempo e realmente aumentar a complexidade, ou até mesmo apostar num banco de dados noSQL podem trazer melhores resultados.
Modelos Normalizados
No livro “Django Design Patterns and Best Practices: Industry-standard web development techniques and solutions using Python”, o autor Arun Ravindran coloca a normalização dos modelos de dados como um pattern estrutural. Segundo o autor, em uma tradução livre:
“Por design, as instâncias de modelo têm dados duplicados que causam inconsistências. Divida seus modelos em modelos menores por meio da normalização.”
Remover as inconsistências e dados duplicados acabará simplificando as pesquisas e diminuindo o tempo de resposta. Na próxima seção usarei um exemplo de conhecimento comum: Pokémon!
Uma jornada Pokémon
Imagine que em nossa pokédex temos dados atualizados de todos os primeiros pokémons capturados de cada tipo como “nome”, “tipo”, “poder” e “primeiro uso em” , conforme a Tabela 01.
A fim de manter o exemplo simples, teremos apenas 3 pokémons capturados, mas só na primeira geração já seriam 251 linhas ou entradas na podédex. Como não é possível mesclar cédulas em um banco de dados SQL, deixei nosso banco de dados conforme a Tabela 02. Esse é apenas um ponto de partida e veremos que não é recomendável serializar dados como fiz.
Listando pokémons com o poder “growl”
Próximo a enfrentar Brock você deseja saber quantos pokémons com o poder “growl” você possui, se a pokédex for um banco de dados SQL, intuitivamente você faria:
SELECT * FROM pokedex.pokedex WHERE poder = 'growl';
Mais isso produziria o resultado da Tabela 02. Será que nenhum pokémon possui “growl”?
Isso aconteceu pois temos dados serializados na tabela, e nenhum dos pokémons possui apenas “growl” como poder.
Se procurássemos pelos poderes na ordem em que estão no banco, isso traria resultados, mas ainda não é o que desejamos.
SELECT * FROM pokedex.pokedex WHERE poder = 'vine whip | growl';
A pesquisa resultaria na Tabela 03. Dessa vez sem o charmander, pois afinal seus poderes são representado como ‘ember | growl’.
Para que ambos os pokémons sejam listados como resultado da pesquisa, uma das soluções seria utilizar expressões regulares que procurem pela letras ‘g’’r’’o’’w’’l’ na mesma ordem mas independente de onde essa sequencia ocorra:
SELECT * FROM pokedex.pokedex WHERE poder LIKE '%growl%';
Nesse caso o resultado seria o apresentado na Tabela 04. Essa solução ainda não é a ideal pois é dependente de expressões regulares.
Não teríamos o problema que acabamos de enfrentar caso tivéssemos obedecido a Primeira Forma Normal (1NF). Pense em como listar qual poder foi utilizado em ‘Viridian Forest’ e verá que não é uma tarefa das mais simples. Problemas como esses seriam frequentes e até mesmo impossíveis de resolver apenas com SQL.
Primeira Forma Normal (1NF)
Para encaixarmos nossa tabela na primeira forma normal, temos que obedecer duas regras:
- Nenhum atributo (célula) deve possuir mais de um valor;
- Uma chave primária deve ser definida por uma coluna ou uma série de colunas;
Nesse caso, as colunas “poder” e “primeiro_uso” não obedecem ao primeiro requisito. A tabela aplicada a forma normal deveria ser conforme a Tabela 05:
A tabela da Tabela 05 também obedece a segunda condição, porém com chave primária composta ‘nome-poder’. Isso não é tão simples de se perceber, mas será discutido nas próximas formas normais.
Listando “pokémons” com o poder “growl” — Novamente
Uma vez adotando a 1NF, vamos voltar ao nosso primeiro comando:
SELECT * FROM pokedex.pokedex WHERE poder = 'growl';
Retornaria a Tabela 06, como esperávamos da primeira vez.
Além disso, listar qual poder foi utilizado em Viridian Forest (que era uma tarefa um pouco complexa) agora pode ser obtida por:
SELECT * FROM pokedex.pokedex WHERE local = 'Viridian Forest';
Que produziria o resultado da Tabela 07. Sem o uso de expressões regulares, sem o uso de queries complexas.
Chave primária composta
Antes de aderirmos a segunda forma normal, é importante identificar nossa chave primária. Chave primária é o valor, ou conjunto de valores que identificam uma linha na tabela. Retornando a nossa pokédex atual, conforme ilustra a Tabela 08.
Se pesquisarmos pelo ‘nome’ de um pokémon:
SELECT * FROM pokedex.pokedex WHERE nome = 'bulbasaur';
Retornaria a Tabela 09, com duas entidades listadas.
A mesma resposta será dada à pesquisa:
SELECT * FROM pokedex.pokedex WHERE nome = 'bulbasaur' AND tipo = 'grass';
Porém, se pesquisarmos por ‘nome’ e ‘poder’, sempre teremos apenas uma linha como resposta. Então a pesquisa:
SELECT * FROM pokedex.pokedex WHERE nome = 'squirtle' AND poder = 'withdraw';
Produziria apenas 1 entidade na resposta, conforme a Tabela 10. Sendo assim consideramos ‘nome-poder’ como uma chave primária composta.
Agora que identificamos nossa chave primária, vamos as regras da segunda forma normal.
“Pokédex” na segunda forma normal (2NF)
Para trazer a pokédex para a segunda forma normal, primeiro devemos garantir que passamos pela primeira forma normal. Se todas as condições foram atendidas, a segunda forma normal estabelece que:
- Todas as colunas de chave não primária devem ser dependente de toda a chave primária.
Observando novamente nossa tabela, conforme a Tabela 11 ou Tabela 05, perceba que eu preciso indicar apenas o ‘nome’ do pokémon para obter seu ‘tipo’, não importando de que ‘poder’ estamos falando. Então, ‘tipo’ não é inteiramente dependente da chave ‘nome-poder’. Para passarmos nossa tabela para a segunda forma normal, vamos criar uma tabela separada chamada “tipo” conforme a Tabela 11.
Já a tabela pokédex (Tabela 12) agora obedece a segunda forma normal.
Podemos utilizar a pesquisa abaixo para saber qual o tipo do pokémon ‘charmander’ (observe que agora procuramos na tabela pokedex.tipo):
SELECT * FROM pokedex.tipo WHERE nome = 'charmander';
Ou até listar todos os pokémons que possuímos de um determinado tipo, conforme a Tabela 13. Nesse caso adicionei o pokémon Bellsprout para produzir mais de um resultado:
SELECT * FROM pokedex.tipo WHERE tipo = 'grass';
“Pokédex” na terceira forma normal (3NF)
Para trazer a pokédex para a terceira forma normal, devemos garantir que passamos pela primeira e segunda forma normal. Após isso devemos satisfazer a condição:
- Todas as colunas de chave não primária devem ser diretamente
dependentes de toda a chave primária e devem ser independentes entre si.
O truque é agrupar elementos semelhantes e observar os que podem ser gerados a partir de outros dados da mesma tabela. Se observamos novamente nossa pokédex conforme ilustrado na Tabela 14, podemos ver que ‘latitude’, ‘longitude’ e ‘local’ podem ser agrupados, assim como ‘data’ e ‘hora’.
A diferença entre os agrupamentos é que podemos determinar o ‘local’ a partir das coordenadas de ‘latitude’ e ‘longitude’ mas não podemos saber a ‘data’ apenas a partir da ‘hora’. Em resumo, a coluna ‘local’ é dependente de ‘latitude’ e ‘longitude’ e não atende os requisitos da 3NF.
Para resolvermos isso, criaremos uma nova tabela, denominada “local” (Tabela 15) e adicionaremos apenas a referência na tabela “pokedex” (Tabela 16). Chamamos essa referência de chave estrangeira.
Finalizando o banco de dados da nossa pokédex como mostra a Figura 01:
Deixamos assim nosso banco de dados na 3NF.
Apesar de agora termos três tabelas os dados são mais acessíveis, as queries mais simples, menor volume de dados para analisar (agilizando o tempo de resposta) e são independentes de expressões regulares. Quanto mais o banco crescer, mais essas vantagens serão perceptíveis.
Considerações finais
Devemos sempre nos lembrar que os computadores pensam de uma forma diferente de nós. A primeira vista pode não parecer interessante trocar uma tabela por três mas essa separação permite estabelecer queries menos complexas e mais intuitivas, facilitando a vida dos programadores (entre eles você) e das inteligências artificiais.
Fazer as coisas “por não ver o porquê de não fazer” não é algo necessariamente ruim, chamamos isso de “boas práticas”, apenas esteja atento ao seu objetivo no projeto. Novamente retorno ao ponto que só vi as vantagens da normalização de tabelas quando de fato tive que trabalhar com banco de dados grandes, e talvez esse seja o seu caso (e não há nada errado nisso).
Existem no total 11 formas normais² mas é considerado que um banco de dados está normalizado quando passa pelas 1NF, 2NF e 3NF. “Ignorar as demais formas normais resulta em um banco de dados imperfeito, mas não afeta sua funcionalidade”³.
A afirmação do parágrafo acima é identificável em nosso resultado final da Figura 01. Poderíamos atribuir “ids” únicos e numéricos a cada pokémon e melhorar a divisão das tabelas, mas o objetivo desse artigo era discutir a 1NF, 2NF e 3NF com a menor quantidade extra de informação possível.
— -
²Database normalization — Wikipedia
³Descrição da normalização do banco de dados — Office | Microsoft Learn
— -
Você utiliza SQL e tem contribuições com esse artigo? Não deixe de entrar em contato comigo para que possamos melhora-lo.