É sempre bom usar listas em um banco de dados relacional?


94

Eu tenho tentado criar um banco de dados para ir com um conceito de projeto e me deparei com o que parece ser um problema muito debatido. Eu li alguns artigos e algumas respostas do Stack Overflow que afirmam que nunca (ou quase nunca) é bom armazenar uma lista de IDs ou algo semelhante em um campo - todos os dados devem ser relacionais etc.

O problema que estou enfrentando, no entanto, é que estou tentando criar um atribuidor de tarefas. As pessoas criarão tarefas, as atribuirão a várias pessoas e elas serão salvas no banco de dados.

É claro que, se eu salvar essas tarefas individualmente em "Pessoa", terei que ter dezenas de colunas fictícias "TaskID" e gerenciá-las micro porque podem haver de 0 a 100 tarefas atribuídas a uma pessoa, por exemplo.

Por outro lado, se eu salvar as tarefas em uma tabela "Tarefas", precisarei ter dezenas de colunas fictícias "PersonID" e gerenciá-las de forma micro - o mesmo problema de antes.

Para um problema como esse, não há problema em salvar uma lista de IDs de uma forma ou de outra ou simplesmente não estou pensando em outra maneira de conseguir isso sem violar princípios?


22
Sei que isso está marcado como "banco de dados relacional", portanto, deixarei como comentário e não como resposta, mas em outros tipos de banco de dados, faz sentido armazenar listas. Cassandra vem à mente, uma vez que não tem junções.
Capitão Man

12
Bom trabalho em pesquisar e depois perguntar aqui! De fato, a 'recomendação' de nunca violar a 1ª forma normal foi muito boa para você, porque você realmente deveria ter uma outra abordagem relacional, a saber, uma relação "muitos para muitos", para a qual existe um padrão padrão. bancos de dados relacionais que devem ser usados.
JimmyB

6
"Está tudo bem" sim .... o que quer que se segue, a resposta é sim. Contanto que você tenha um motivo válido. Sempre há um caso de uso que o obriga a violar as práticas recomendadas, porque faz sentido fazê-lo. (No seu caso, porém, você definitivamente não deve)
xyious

3
Atualmente, estou usando uma matriz ( não uma string delimitada - a VARCHAR ARRAY) para armazenar uma lista de tags. Provavelmente não é assim que elas acabam sendo armazenadas posteriormente, mas as listas podem ser extremamente úteis durante os estágios de prototipagem, quando você não tem mais nada para apontar e não deseja criar todo o esquema do banco de dados antes de poder faça qualquer outra coisa.
Nic Hartley

3
@ Ben " (embora não sejam indexáveis) " - no Postgres, várias consultas em colunas JSON (e provavelmente XML, embora eu não tenha verificado) são indexáveis.
Nic Hartley

Respostas:


249

A palavra-chave e o conceito-chave que você precisa investigar é a normalização do banco de dados .

O que você faria, em vez de adicionar informações sobre as atribuições à pessoa ou às tabelas de tarefas, é adicionar uma nova tabela com essas informações de atribuição, com relacionamentos relevantes.

Exemplo, você tem as seguintes tabelas:

Pessoas:

+ −−−− + −−−−−−−−−−− +
| ID Nome
+ ==== + =========== +
| 1 | Alfred
| 2 Jebediah
| 3 Jacob
| 4 Ezequiel
+ −−−− + −−−−−−−−−−− +

Tarefas:

+ −−−− + −−−−−−−−−−−−−−−--
| ID Nome
+ ==== + ==================== +
| 1 | Alimente as galinhas |
| 2 Arado
| 3 Vacas leiteiras |
| 4 Criar um celeiro |
+ −−−− + −−−−−−−−−−−−−−−--

Você criaria uma terceira tabela com atribuições. Esta tabela modelaria o relacionamento entre as pessoas e as tarefas:

+ −−−− + −−−−−−−−−− + −−−−−−−− +
| ID PersonId TaskId |
+ ==== + =========== + ========= +
| 1 | 1 | 3
| 2 3 2
| 3 2 1 |
| 4 1 | 4
+ −−−− + −−−−−−−−−− + −−−−−−−− +

Teríamos, então, uma restrição de chave estrangeira, de modo que o banco de dados imponha que o PersonId e o TaskIds tenham que ser IDs válidos para esses itens estrangeiros. Para a primeira linha, podemos ver PersonId is 1, portanto , Alfred , está atribuído a TaskId 3, Vacas leiteiras .

O que você deve ser capaz de ver aqui é que você pode ter poucas ou tantas atribuições por tarefa ou por pessoa quanto desejar. Neste exemplo, Ezekiel não recebeu nenhuma tarefa e Alfred recebeu 2. Se você tiver uma tarefa com 100 pessoas, a tarefa SELECT PersonId from Assignments WHERE TaskId=<whatever>;produzirá 100 linhas, com uma variedade de Pessoas diferentes atribuídas. Você pode encontrar WHEREno PersonId todas as tarefas atribuídas a essa pessoa.

Se você quiser retornar consultas substituindo os IDs pelos Nomes e pelas tarefas, aprenderá como JOIN tables.


86
A palavra-chave que você deseja pesquisar para saber mais é "relacionamento muitos-para-muitos "
BlueRaja - Danny Pflughoeft

34
Para elaborar um pouco sobre o comentário de Thierrys: Você pode pensar que não precisa se normalizar, porque eu só preciso do X e é muito simples armazenar a lista de IDs , mas para qualquer sistema que possa ser estendido mais tarde, você se arrependerá de não a ter normalizado. mais cedo. Sempre normalize ; a única questão é o que forma normal
Jan Doggen

8
Concordei com @Jan - contra meu melhor julgamento, permiti que minha equipe retirasse um atalho de design há algum tempo, armazenando JSON para algo que "não precisará ser estendido". Isso durou seis meses no FML. Nosso atualizador teve uma luta desagradável nas mãos para migrar o JSON para o esquema que deveríamos ter iniciado. Eu realmente deveria saber melhor.
Lightness Races em órbita

13
@ Reduplicador: é apenas uma representação de uma coluna de chave primária de número inteiro com incremento automático de jardim. Coisas bastante típicas.
Whatsisname

8
@whatsisname Na tabela Pessoas ou tarefas, eu concordo com você. Em uma tabela de ponte em que o único objetivo é representar o relacionamento muitos-para-muitos entre duas outras tabelas que já possuem chaves substitutas? Eu não adicionaria um sem uma boa razão. É apenas uma sobrecarga, pois nunca será usado em consultas ou relacionamentos.
Jpmc26

35

Você está fazendo duas perguntas aqui.

Primeiro, você pergunta se não há problema em armazenar listas serializadas em uma coluna. Sim, tudo bem. Se o seu projeto exige isso. Um exemplo pode ser ingredientes do produto para uma página de catálogo, onde você não deseja tentar rastrear cada ingrediente individualmente.

Infelizmente, sua segunda pergunta descreve um cenário em que você deve optar por uma abordagem mais relacional. Você precisará de 3 mesas. Um para as pessoas, um para as tarefas e um que mantém a lista de qual tarefa está atribuída a quais pessoas. Essa última seria vertical, uma linha por combinação de pessoa / tarefa, com colunas para sua chave primária, ID da tarefa e ID da pessoa.


9
O exemplo de ingrediente que você menciona está correto na superfície; mas seria texto simples nesse caso. Não é uma lista no sentido de programação (a menos que você queira dizer que a string é uma lista de caracteres que você obviamente não faz). O OP que descreve seus dados como "uma lista de IDs" (ou mesmo apenas "uma lista de [..]") implica que eles estão, em algum momento, manipulando esses dados como objetos individuais.
Flater

10
@ Flater: Mas é uma lista. Você precisa ser capaz de reformatá-lo como (variadamente) uma lista HTML, uma lista Markdown, uma lista JSON etc. para garantir que os itens sejam exibidos corretamente em (variadamente) uma página da Web, um documento de texto sem formatação, um dispositivo móvel app ... e você realmente não pode fazer isso com texto simples.
Kevin

12
@ Kevin Se esse é seu objetivo, é muito mais fácil e fácil de armazenar os ingredientes em uma mesa! Sem mencionar se, mais tarde, as pessoas ... ah, eu não sei, digamos, desejar substitutos recomendados , ou algo bobo como procurar todas as receitas sem amendoins, glúten ou proteínas animais ...
Dan Bron

10
@DanBron: YAGNI. No momento, estamos usando apenas uma lista, pois isso facilita a lógica da interface do usuário. Se precisa ou precisará comportamento lista semelhante na camada de lógica de negócios, em seguida, ele deve ser normalizado em uma tabela separada. Mesas e junções não são necessariamente caras, mas não são gratuitas, e trazem perguntas sobre a ordem dos elementos ("Preocupamo-nos com a ordem dos ingredientes?") E mais normalização ("Você vai transformar '3 ovos' into ('eggs', 3)? E quanto a 'Salt, a gosto', é isso ('salt', NULL)? ").
Kevin

7
@Kevin: YAGNI está completamente errado aqui. Você mesmo argumentou a necessidade de poder transformar a lista de várias maneiras (HTML, markdown, JSON) e, portanto, está argumentando que precisa dos elementos individuais da lista . A menos que os aplicativos de armazenamento de dados e "manipulação de lista" sejam dois aplicativos desenvolvidos de forma independente (e observe que as camadas de aplicativos são separadas! = Aplicações separadas), a estrutura do banco de dados deve sempre ser criada para armazenar os dados em um formato que os deixe prontamente disponíveis - evitando lógica adicional de análise / conversão.
Flater

22

O que você está descrevendo é conhecido como um relacionamento "muitos para muitos", no seu caso entre Persone Task. Geralmente, é implementado usando uma terceira tabela, às vezes chamada de tabela "link" ou "referência cruzada". Por exemplo:

create table person (
    person_id integer primary key,
    ...
);

create table task (
    task_id integer primary key,
    ...
);

create table person_task_xref (
    person_id integer not null,
    task_id integer not null,
    primary key (person_id, task_id),
    foreign key (person_id) references person (person_id),
    foreign key (task_id) references task (task_id)
);

2
Você também pode adicionar um índice task_idprimeiro, se estiver fazendo consultas filtradas por tarefa.
jpmc26

1
Também conhecida como tabela de bridge. Além disso, gostaria de lhe dar uma vantagem extra por não ter uma coluna de identidade, embora eu recomende um índice em cada coluna.
jmoreno

13

... nunca (ou quase nunca) é bom armazenar uma lista de IDs ou algo semelhante em um campo

O único momento em que você pode armazenar mais de um item de dados em um único campo é quando esse campo é usado apenas como uma única entidade e nunca é considerado como sendo composto por esses elementos menores. Um exemplo pode ser uma imagem, armazenada em um campo BLOB. Ele é composto por muitos e muitos elementos menores (bytes), mas estes não significam nada para o banco de dados e só podem ser usados ​​todos juntos (e parecem muito para um Usuário Final).

Como uma "lista" é, por definição, composta de elementos menores (itens), esse não é o caso aqui e você deve normalizar os dados.

... se eu salvar essas tarefas individualmente em "Pessoa", terei que ter dezenas de colunas fictícias "TaskID" ...

Não. Você terá algumas linhas em uma Tabela de interseção (também conhecida como entidade fraca) entre Pessoa e Tarefa. Os bancos de dados são realmente bons em trabalhar com muitas linhas; eles são realmente um lixo ao trabalhar com muitas colunas [repetidas].

Belo exemplo claro dado por whatsisname.


4
Ao criar sistemas da vida real "nunca diga nunca", é uma regra muito boa de se viver.
L0b0

1
Em muitos casos, o custo por elemento de manter ou recuperar uma lista na forma normalizada pode exceder amplamente o custo de manter os itens como um blob, já que cada item da lista teria que manter a identidade do item mestre com o qual está associado e sua localização na lista, além dos dados reais. Mesmo nos casos em que o código pode se beneficiar da capacidade de atualizar alguns elementos da lista sem atualizar a lista inteira, pode ser mais barato armazenar tudo como um blob e reescrever tudo sempre que for necessário reescrever algo.
Supercat

4

Pode ser legítimo em certos campos pré-calculados.

Se algumas de suas consultas forem caras e você decidir optar por campos pré-calculados atualizados automaticamente usando acionadores de banco de dados, pode ser legítimo manter as listas em uma coluna.

Por exemplo, na interface do usuário você deseja mostrar esta lista usando a exibição em grade, em que cada linha pode abrir detalhes completos (com listas completas) após clicar duas vezes:

REGISTERED USER LIST
+------------------+----------------------------------------------------+
|Name              |Top 3 most visited tags                             |
+==================+====================================================+
|Peter             |Design, Fitness, Gifts                              |
+------------------+----------------------------------------------------+
|Lucy              |Fashion, Gifts, Lifestyle                           |
+------------------+----------------------------------------------------+

Você mantém a segunda coluna atualizada por acionador quando o cliente visita um novo artigo ou por tarefa agendada.

Você pode disponibilizar esse campo mesmo para pesquisa (como texto normal).

Para tais casos, manter listas é legítimo. Você só precisa considerar o caso de possivelmente exceder o tamanho máximo do campo.


Além disso, se você estiver usando o Microsoft Access, os campos com vários valores oferecidos são outro caso de uso especial. Eles lidam com suas listas em um campo automaticamente.

Mas você sempre pode voltar ao formulário normalizado padrão mostrado em outras respostas.


Resumo: Formas normais de banco de dados são modelo teórico necessário para entender aspectos importantes da modelagem de dados. Mas é claro que a normalização não leva em consideração o desempenho ou outro custo da recuperação dos dados. Está fora do escopo desse modelo teórico. Mas o armazenamento de listas ou outras duplicatas pré-calculadas (e controladas) geralmente é exigido pela implementação prática.

À luz do exposto acima, na implementação prática, preferiríamos que a consulta dependesse da forma normal perfeita e executasse 20 segundos ou consulta equivalente, dependendo de valores pré-calculados que levam 0,08 s? Ninguém gosta que seu produto de software seja acusado de lentidão.


1
Pode ser legítimo mesmo sem coisas pré-calculadas. Eu já fiz isso algumas vezes em que os dados são armazenados corretamente, mas por razões de desempenho, é útil inserir alguns resultados em cache nos registros principais.
Loren Pechtel

@ LorenPechtel - Sim, obrigado. No uso do termo pré-calculado , também incluo casos de valores em cache armazenados onde necessário. Em sistemas com dependências complexas, eles são o caminho para manter o desempenho normal. E se programados com o know-how adequado, esses valores são confiáveis ​​e sempre sincronizados. Eu simplesmente não queria adicionar caso de armazenamento em cache à resposta para manter a resposta simples e segura. Foi votado de qualquer maneira. :)
miroxlav

@LorenPechtel Na verdade, isso ainda seria um mau motivo ... os dados do cache devem ser mantidos em um armazenamento intermediário e, embora o cache ainda seja válido, essa consulta nunca deve atingir o banco de dados principal.
Tezra

1
@ Tezra Não, estou dizendo que, às vezes, um dado de uma tabela secundária é necessário com frequência suficiente para fazer sentido colocar uma cópia no registro principal. (Exemplo que eu fiz - a tabela de funcionários inclui a última vez em que entrou e a última vez que expirou. Eles são usados ​​apenas para fins de exibição; qualquer cálculo real vem da tabela com os registros de entrada / saída).
Loren Pechtel

0

Dadas duas tabelas; nós os chamaremos de Person e Task, cada um com seu próprio ID (PersonID, TaskID) ... a idéia básica é criar uma terceira tabela para uni-los. Vamos chamar essa tabela de PersonToTask. No mínimo, ele deve ter seu próprio ID, assim como os outros dois. Portanto, quando se trata de designar alguém para uma tarefa; não será mais necessário atualizar a tabela Person, basta inserir uma nova linha na PersonToTaskTable. E a manutenção se torna mais fácil - a necessidade de excluir uma tarefa se torna DELETE com base no TaskID, não é mais necessário atualizar a tabela Person e a análise associada

CREATE TABLE dbo.PersonToTask (
    pttID INT IDENTITY(1,1) NOT NULL,
    PersonID INT NULL,
    TaskID   INT NULL
)

CREATE PROCEDURE dbo.Task_Assigned (@PersonID INT, @TaskID INT)
AS
BEGIN
    INSERT PersonToTask (PersonID, TaskID)
    VALUES (@PersonID, @TaskID)
END

CREATE PROCEDURE dbo.Task_Deleted (@TaskID INT)
AS
BEGIN
    DELETE PersonToTask  WHERE TaskID = @TaskID
    DELETE Task          WHERE TaskID = @TaskID
END

Que tal um relatório simples ou quem está atribuído a uma tarefa?

CREATE PROCEDURE dbo.Task_CurrentAssigned (@TaskID INT)
AS
BEGIN
    SELECT PersonName
    FROM   dbo.Person
    WHERE  PersonID IN (SELECT PersonID FROM dbo.PersonToTask WHERE TaskID = @TaskID)
END

É claro que você poderia fazer muito mais; um TimeReport pode ser feito se você adicionar os campos DateTime para TaskAssigned e TaskCompleted. Está tudo nas tuas mãos


0

Pode funcionar se você tiver chaves primárias legíveis por humanos e desejar uma lista de tarefas sem precisar lidar com a natureza vertical de uma estrutura de tabela. ou seja, é muito mais fácil ler a primeira tabela.

------------------------  
Employee Name | Task 
Jack          |  1,2,5
Jill          |  4,6,7
------------------------

------------------------  
Employee Name | Task 
Jack          |  1
Jack          |  2
Jack          |  5
Jill          |  4
Jill          |  6
Jill          |  7
------------------------

A questão seria: a lista de tarefas deve ser armazenada ou gerada sob demanda, o que dependeria em grande parte de requisitos como: com que frequência a lista é necessária, com que precisão existem quantas linhas de dados, como os dados serão usados ​​etc. .. após o qual a análise das trocas para a experiência do usuário e o cumprimento dos requisitos devem ser feitos.

Por exemplo, comparando o tempo que levaria para recuperar as 2 linhas versus executar uma consulta que geraria as 2 linhas. Se demorar e o usuário não precisar da lista mais atualizada (* esperando menos de 1 alteração por dia), ela poderá ser armazenada.

Ou, se o usuário precisar de um registro histórico de tarefas atribuídas a ele, também faria sentido se a lista fosse armazenada. Portanto, isso realmente depende do que você está fazendo, nunca diga nunca.


Como você diz, tudo depende de como os dados devem ser recuperados. Se você / somente / alguma vez consultar esta tabela por Nome de Usuário, o campo "lista" será perfeitamente adequado. No entanto, como você pode consultar uma tabela para descobrir quem está trabalhando na tarefa # 1234567 e ainda manter seu desempenho? Praticamente todos os tipos de funções de string "encontrar-X-em qualquer lugar-no-campo" farão com que essa consulta seja / Table Scan /, atrasando o rastreamento. Com dados adequadamente normalizados e indexados, isso simplesmente não acontece.
Phill W.

0

Você está pegando o que deveria ser outra mesa, girando 90 graus e colocando-o em outra mesa.

É como ter uma tabela de pedidos onde você possui itemProdcode1, itemQuantity1, itemPrice1 ... itemProdcode37, itemQuantity37, itemPrice37. Além de ser complicado de lidar programaticamente, você pode garantir que amanhã alguém deseje pedir 38 coisas.

Eu faria do seu jeito apenas se a 'lista' não for realmente uma lista, ou seja, onde ela se encontra como um todo e cada item de linha individual não se refere a alguma entidade clara e independente. Nesse caso, basta colocar tudo em algum tipo de dado que seja grande o suficiente.

Portanto, um pedido é uma lista, uma lista de materiais é uma lista (ou uma lista de listas, o que seria ainda mais um pesadelo para implementar "de lado"). Mas uma nota / comentário e um poema não são.


0

Se "não estiver ok", é bastante ruim que todo site Wordpress tenha uma lista em wp_usermeta com wp_capabilities em uma linha, lista de demitidos_wp_pointers em uma linha e outros ...

De fato, em casos como esse, pode ser melhor para a velocidade, pois você quase sempre deseja a lista . Mas o Wordpress não é conhecido por ser o exemplo perfeito de melhores práticas.

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.