O uso de várias chaves estrangeiras separadas por vírgulas está errado e, em caso afirmativo, por quê?

31

Existem duas tabelas: Deale DealCategories. Uma oferta pode ter muitas categorias de ofertas.

Portanto, a maneira correta deve ser criar uma tabela chamada DealCategoriescom a seguinte estrutura:

DealCategoryId (PK)
DealId (FK)
DealCategoryId (FK)

No entanto, nossa equipe de terceirização armazenou as várias categorias no Deal tabela da seguinte maneira:

DealId (PK)
DealCategory -- In here they store multiple deal ids separated by commas like this: 18,25,32.

Sinto que o que eles fizeram está errado, mas não sei como explicar claramente por que isso não está certo.

Como devo explicar a eles que isso está errado? Ou talvez eu seja a pessoa que está errada e isso é aceitável?

Sarawut Positwinyu
fonte
20
Você está certo. O armazenamento de uma lista separada por vírgulas em uma coluna do banco de dados é realmente tão ruim? . Resposta curta: Sim, é tão ruim assim.
ypercubeᵀᴹ
7
fogo que equipe terceirizada imediatamente antes de fazer qualquer mais mal ... (-_-)
Rafa

Respostas:

49

Sim, é uma péssima ideia.

Ao invés de ir:

SELECT Deal.Name, DealCategory.Name
FROM Deal
  INNER JOIN
     DealCategories ON Deal.DealID = DealCategories.DealID
  INNER JOIN
     DealCategory ON DealCategories.DealCategoryID = DealCategory.DealCategoryID
WHERE Deal.DealID = 1234

Agora você precisa ir:

SELECT Deal.ID, Deal.Name, DealCategories
FROM Deal
WHERE Deal.DealID = 1234

Em seguida, é necessário fazer algumas coisas no código do aplicativo para dividir a lista de vírgulas em números individuais e, em seguida, consultar o banco de dados separadamente:

SELECT DealCategory.Name
FROM DealCategory
WHERE DealCategory.DealCategoryID IN (<<that list from before>>)

Esse antipadrão de design deriva de um completo mal-entendido da modelagem relacional (você não precisa ter medo de tabelas. As tabelas são suas amigas. Use-as) ou uma crença estranhamente equivocada de que é mais rápido pegar uma lista separada por vírgula e dividi-la no código do aplicativo do que é adicionar uma tabela de links ( nunca é). A terceira opção é que eles não são confiantes / competentes o suficiente com o SQL para poder configurar chaves estrangeiras, mas se for esse o caso, eles não devem ter nada a ver com o design de um modelo relacional.

Antipatterns do SQL (Karwin, 2010) dedica um capítulo inteiro a esse antipattern (que ele chama de 'Jaywalking'), páginas 15-23. Além disso, o autor postou uma pergunta semelhante no SO . Os principais pontos que ele observa (conforme aplicado a este exemplo) são:

  • A consulta a todas as transações em uma categoria específica é bastante complicada (a maneira mais fácil de resolver esse problema é uma expressão regular, mas uma expressão regular é um problema por si só).
  • Você não pode impor integridade referencial sem relacionamentos de chave estrangeira. Se você excluir o DealCategory nr. # 26, você deve, no código do aplicativo, passar por cada transação procurando referências à categoria # 26 e excluí-las. Isso é algo que deve ser tratado na camada de dados, e ter que lidar com isso em seu aplicativo é uma coisa muito ruim .
  • Consultas agregadas ( COUNT, SUMetc), mais uma vez, variam de 'complicado' para 'quase impossível'. Pergunte aos desenvolvedores como eles obteriam uma lista de todas as categorias com uma contagem do número de transações nessa categoria. Com um design adequado, são quatro linhas de SQL.
  • As atualizações se tornam muito mais difíceis (ou seja, você tem um acordo em cinco categorias, mas deseja remover duas e adicionar outras três). São três linhas de SQL com um design adequado.
  • Eventualmente, você terá VARCHARlimitações de tamanho da lista. Embora se você tiver uma lista separada por vírgulas com mais de 4000 caracteres, é provável que esteja analisando que o monstro será lento como o inferno de qualquer maneira.
  • Retirar uma lista do banco de dados, dividi-la e depois retornar ao banco de dados para outra consulta é intrinsecamente mais lento que uma consulta.

TLDR: é um design fundamentalmente defeituoso, não aumenta com escala, introduz complexidade adicional até para as consultas mais simples e, logo após a instalação, torna o aplicativo mais lento.

Simon Righarts
fonte
1
Simon, alguém fez a mesma pergunta ( dba.stackexchange.com/questions/17824/… ), mas não sei ao certo por que os mesmos FK e PK estão na mesma tabela, que freiam o 3FN.
Jcho360
2
Eu não tinha muita certeza se eles queriam ter um relacionamento muitos para muitos entre Ofertas e Categorias, ou algum tipo de hierarquia de Categorias. De qualquer forma, era uma linha lateral do ponto principal, que ser campos delimitados por vírgula em vez de uma tabela de links é uma má idéia.
Simon Righarts
4

No entanto, nossa equipe de terceirização armazenou as várias categorias na tabela Deal da seguinte maneira:

DealId (PK) DealCategory - aqui eles armazenam vários IDs de transações separados por vírgulas como este: 18,25,32.

Na verdade, é um bom design se você precisar apenas consultar as categorias para um determinado negócio.

Mas é terrível se você quiser conhecer todas as ofertas em uma determinada categoria.

E também torna muito difícil e propenso a erros fazer qualquer outra coisa - como atualizações, contagens, associações, etc.

A desnormalização tem seu lugar, mas você deve ter em mente que ela otimiza para um tipo de consulta às custas de todas as outras que você pode fazer com os mesmos dados. Se você souber que sempre estará consultando um padrão, poderá ser uma vantagem usar o design desnormalizado. Mas se houver alguma chance de você precisar de mais flexibilidade nos tipos de consultas, use um design normalizado.

Como qualquer outra forma de otimização, você precisa saber quais consultas serão executadas antes de decidir se a desnormalização é justificada.

Bill Karwin
fonte
1
Você realmente acha que uma sequência com IDs filhos separados por vírgula é útil? Quero dizer, o aplicativo teve que ler primeiro, depois analisar os IDs e consultar todas as crianças, como select * from DealCategories where DealId in (1,2,3,4,...). Você tem mais experiência em relação ao design do banco de dados do que eu; talvez, em alguns casos, tenha boas razões para esse "ajuste extremo" em casos muito específicos. Minha única idéia para justificar isso é uma selectcarga muito alta em Deal / DealCategory. Isso me parece muito com alguma equipe de terceirização sem nenhum conhecimento em design de banco de dados, além de criar tabelas, a criou.
Erik Hart
1
@ ErikHart, isso é desnormalização, e pode ser útil, mas o que quero dizer é que depende inteiramente das consultas que você precisa executar. Você está certo de que a desnormalização piora todas as consultas, exceto a consulta otimizada. Se você só precisa executar essa consulta e não se importa com as outras, é uma vitória. Mas esses são casos raros, porque normalmente queremos flexibilidade para consultar os dados de várias maneiras.
Bill Karwin
1
@ErikHart, se essa equipe de terceirização recebesse especificações de projeto que incluíssem apenas uma consulta nesses dados, ela poderia ter projetado uma otimização para essa consulta específica. Em outras palavras, "você pediu, conseguiu". Mas o provedor de terceirização não tem motivos para planejar usos futuros dos dados - eles implementam o aplicativo conforme a letra do que está escrito nas especificações.
Bill Karwin
1

Vários valores em uma coluna são contra a 1ª forma normal.

Também não há ganho de velocidade, pois as tabelas devem ser vinculadas no banco de dados. Você deve ler e analisar uma string primeiro e, em seguida, selecione todas as categorias para o "Negócio".

A implementação correta seria uma tabela de junção como "DealDealCategories", com DealId e DealCategoryId.

Implementação de má hierarquia?

Além disso, um FK em DealCategories para outra DealCategory parece uma implementação ruim de uma hierarquia / árvore de DealCategories. Trabalhar com árvores por meio de uma relação de identificação dos pais (chamada lista de adjacências) é um problema!

Verifique se há conjuntos aninhados (de boa leitura, mas difíceis de modificar) e tabelas de fechamento (melhor desempenho geral, mas possivelmente alto uso de memória - provavelmente não muito para suas DealCategories) ao implementar hierarquias!

Erik Hart
fonte