Como armazenar uma lista em uma coluna de uma tabela de banco de dados

114

Portanto, de acordo com a resposta de Mehrdad a uma pergunta relacionada , entendi que uma coluna de tabela de banco de dados "adequada" não armazena uma lista. Em vez disso, você deve criar outra tabela que contenha efetivamente os elementos dessa lista e, em seguida, vincular a ela diretamente ou por meio de uma tabela de junção. No entanto, o tipo de lista que desejo criar será composto de itens únicos (ao contrário do fruto da pergunta vinculadaexemplo). Além disso, os itens da minha lista são classificados explicitamente - o que significa que se eu armazenasse os elementos em outra tabela, teria que classificá-los sempre que os acessasse. Finalmente, a lista é basicamente atômica, pois sempre que eu quiser acessar a lista, vou querer acessar a lista inteira em vez de apenas um pedaço dela - então parece bobo ter que emitir uma consulta de banco de dados para reunir pedaços de a lista.

A solução de AKX (link acima) é serializar a lista e armazená-la em uma coluna binária. Mas isso também parece inconveniente porque significa que preciso me preocupar com a serialização e a desserialização.

Existe alguma solução melhor? Se não é nenhuma solução melhor, então por quê? Parece que esse problema deve surgir de vez em quando.

... apenas mais algumas informações para que você saiba de onde estou vindo. Assim que comecei a entender SQL e bancos de dados em geral, fui ligado para LINQ to SQL e agora estou um pouco estragado porque espero lidar com meu modelo de objeto de programação sem ter que pensar sobre como os objetos são consultados ou armazenados no banco de dados.

Obrigado a todos!

John

ATUALIZAÇÃO: Então, na primeira onda de respostas que estou recebendo, vejo "você pode seguir a rota CSV / XML ... mas NÃO!". Agora estou procurando explicações do porquê. Mostre-me algumas boas referências.

Além disso, para lhe dar uma ideia melhor do que estou fazendo: Em meu banco de dados, tenho uma tabela de funções que terá uma lista de pares (x, y). (A tabela também terá outras informações que não têm importância para nossa discussão.) Nunca precisarei ver parte da lista de pares (x, y). Em vez disso, pegarei todos eles e os representarei na tela. Permitirei que o usuário arraste os nós para alterar os valores ocasionalmente ou adicionar mais valores ao gráfico.

JnBrymn
fonte

Respostas:

182

Não, não existe uma maneira "melhor" de armazenar uma sequência de itens em uma única coluna. Os bancos de dados relacionais são projetados especificamente para armazenar um valor por combinação de linha / coluna. Para armazenar mais de um valor, você deve serializar sua lista em um único valor para armazenamento e, em seguida, desserializar na recuperação. Não há outra maneira de fazer o que você está falando (porque o que você está falando é uma má ideia que, em geral, nunca deveria ser feita ).

Eu entendo que você ache bobagem criar outra tabela para armazenar essa lista, mas isso é exatamente o que os bancos de dados relacionais fazem. Você está travando uma batalha difícil e violando um dos princípios mais básicos do projeto de banco de dados relacional sem um bom motivo. Já que você afirma que está apenas aprendendo SQL, eu recomendo fortemente que você evite essa ideia e siga as práticas recomendadas por desenvolvedores de SQL mais experientes.

O princípio que você está violando é chamado de primeira forma normal , que é a primeira etapa na normalização do banco de dados.

Correndo o risco de simplificar demais as coisas, a normalização de banco de dados é o processo de definição de seu banco de dados com base no que os dados é , de modo que você pode escrever, consultas consistentes sensatas contra ele e ser capaz de mantê-lo facilmente. A normalização é projetada para limitar inconsistências lógicas e corrupção em seus dados, e há muitos níveis nela. O artigo da Wikipedia sobre normalização de banco de dados é realmente muito bom.

Basicamente, a primeira regra (ou forma) de normalização afirma que sua tabela deve representar uma relação. Isso significa que:

  • Você deve ser capaz de diferenciar uma linha de qualquer outra linha (em outras palavras, sua tabela deve ter algo que possa servir como uma chave primária. Isso também significa que nenhuma linha deve ser duplicada.
  • Qualquer ordem dos dados deve ser definida pelos dados, não pela ordem física das linhas (SQL é baseado na ideia de um conjunto, o que significa que a única ordem na qual você deve confiar é aquela que você definir explicitamente em sua consulta)
  • Cada intersecção de linha / coluna deve conter um e apenas um valor

O último ponto é obviamente o ponto saliente aqui. O SQL é projetado para armazenar seus conjuntos para você, não para fornecer um "balde" para você mesmo armazenar um conjunto. Sim, é possível fazer. Não, o mundo não vai acabar. No entanto, você já se prejudicou no entendimento de SQL e das melhores práticas que o acompanham, começando imediatamente a usar um ORM. LINQ to SQL é fantástico, assim como as calculadoras gráficas. Na mesma linha, entretanto, eles não devem ser usados ​​como um substituto para saber como os processos que eles empregam realmente funcionam.

Sua lista pode ser inteiramente "atômica" agora, e isso não pode mudar para este projeto. Mas você irá, no entanto, adquirir o hábito de fazer coisas semelhantes em outros projetos e, eventualmente (provavelmente rapidamente) se deparará com um cenário em que agora você está ajustando sua lista rápida e fácil em uma coluna abordagem onde é totalmente inapropriado. Não há muito trabalho adicional na criação da tabela correta para o que você está tentando armazenar, e você não será ridicularizado por outros desenvolvedores de SQL quando virem o design do seu banco de dados. Além disso, o LINQ to SQL verá sua relação e fornecerá a interface orientada a objetos apropriada para sua lista automaticamente . Por que você desistiria da conveniência oferecida a você pelo ORM para que possa executar hackeamentos de banco de dados fora do padrão e imprudentes?

Adam Robinson
fonte
17
Então, você acredita firmemente que armazenar uma lista em uma coluna é uma má ideia, mas não menciona o porquê. Já que estou começando com o SQL, um pouco do "por que" seria muito útil. Por exemplo, você diz que estou "travando uma batalha difícil e violando um dos princípios mais básicos do projeto de banco de dados relacional sem um bom motivo" ... então, qual é o princípio? Por que as razões que citei são "ruins"? (especificamente, a natureza classificada e atômica de minhas listas)
JnBrymn
6
Basicamente, tudo se resume a anos de experiência condensada nas melhores práticas. O princípio básico em questão é conhecido como 1ª Forma Normal .
Toby de
1
Obrigado Adam. Muito informativo. Bom ponto com sua última pergunta.
JnBrymn
8
“[...] e você não será ridicularizado por outros desenvolvedores de SQL quando virem o design do seu banco de dados.” Há boas razões para respeitar a Primeira Forma Normal (e sua resposta as menciona), mas a pressão dos colegas / “é assim que as coisas são feitas por aqui” não está entre elas.
Lynn
5
Já armazenamos muitas listas em colunas de banco de dados todos os dias. Eles são chamados de "char" e "varchar". Claro que no Postgres, eles também são chamados de texto. O que a 1NF realmente diz é que você não deve nunca querer dividir as informações em qualquer campo em campos menores e, se fizer isso, estará enganado. Assim, você não armazena nome, armazena nome pessoal, nomes do meio e nomes de família (dependendo da localização) e os junta. Caso contrário, não armazenaríamos strings de texto. Por outro lado, tudo o que ele quer é um cordão. E existem maneiras de fazer isso.
Haakon Løtveit
15

Você pode simplesmente esquecer o SQL e escolher uma abordagem "NoSQL". RavenDB , MongoDB e CouchDB vêm à mente como soluções possíveis. Com uma abordagem NoSQL, você não está usando o modelo relacional ... você nem mesmo está restrito a esquemas.

Jaltiere
fonte
11

O que tenho visto muitas pessoas fazerem é isto (pode não ser a melhor abordagem, corrija-me se eu estiver errado):

A tabela que estou usando no exemplo é fornecida abaixo (a tabela inclui apelidos que você deu a suas namoradas específicas. Cada namorada tem um id único):

nicknames(id,seq_no,names)

Suponha que você queira armazenar muitos apelidos sob um id. É por isso que incluímos um seq_nocampo.

Agora, preencha estes valores em sua tabela:

(1,1,'sweetheart'), (1,2,'pumpkin'), (2,1,'cutie'), (2,2,'cherry pie')

Se você quiser encontrar todos os nomes que deu à sua namorada id 1, você pode usar:

select names from nicknames where id = 1;
H. Pauwelyn
fonte
5

Resposta simples: Se, e somente se, você tiver certeza de que a lista sempre será usada como uma lista, junte a lista no seu final com um caractere (como '\ 0') que não será usado no texto sempre e armazene-o. Então, quando você recuperá-lo, você pode dividir por '\ 0'. É claro que existem outras maneiras de fazer isso, mas dependem do fornecedor específico do banco de dados.

Como exemplo, você pode armazenar JSON em um banco de dados Postgres. Se a sua lista for de texto e você quiser apenas a lista sem maiores problemas, esse é um meio-termo razoável.

Outros aventuraram-se a sugerir a serialização, mas eu realmente não acho que serializar seja uma boa ideia: parte do bom dos bancos de dados é que vários programas escritos em linguagens diferentes podem conversar entre si. E os programas serializados usando o formato Java não fariam muito bem se um programa Lisp quisesse carregá-lo.

Se você deseja uma boa maneira de fazer esse tipo de coisa, normalmente existem tipos de array ou similares disponíveis. Postgres, por exemplo, oferece array como um tipo e permite que você armazene um array de texto, se for isso que você deseja , e existem truques semelhantes para MySql e MS SQL usando JSON, e o DB2 da IBM oferece um tipo de array também (em seu própria documentação útil ). Isso não seria tão comum se não houvesse necessidade.

O que você perde ao seguir esse caminho é a noção da lista como um monte de coisas em sequência. Pelo menos nominalmente, os bancos de dados tratam os campos como valores únicos. Mas se isso é tudo que você quer, então você deve ir em frente. É um julgamento de valor que você deve fazer por si mesmo.

Haakon Løtveit
fonte
3

Além do que todos disseram, sugiro que você analise sua abordagem em termos mais longos do que agora. Isto é actualmente o caso que os itens são únicos. É actualmente o caso que recorrer os itens exigiria uma nova lista. É quase obrigatório que a lista atualmente seja curta. Mesmo que eu não tenha as especificações do domínio, não é muito forçado pensar que esses requisitos possam mudar. Se você serializar sua lista, estará trabalhando com uma inflexibilidade que não é necessária em um design mais normalizado. A propósito, isso não significa necessariamente um relacionamento Muitos: Muitos completo. Você poderia ter apenas uma única tabela filho com uma chave estrangeira para o pai e uma coluna de caracteres para o item.

Se você ainda deseja seguir este caminho de serializar a lista, considere armazenar a lista em XML. Alguns bancos de dados, como o SQL Server, possuem até mesmo um tipo de dados XML. A única razão pela qual sugiro XML é que, quase por definição, essa lista precisa ser curta. Se a lista for longa, serializá-la em geral é uma abordagem terrível. Se você seguir a rota CSV, precisará levar em conta os valores que contêm o delimitador, o que significa que você é obrigado a usar identificadores entre aspas. Presumindo que as listas são curtas, provavelmente não fará muita diferença se você usar CSV ou XML.

Thomas
fonte
+1 para antecipar mudanças futuras - sempre projete seu modelo de dados para ser extensível.
coolgeek de
2

Eu apenas armazenaria como CSV, se forem valores simples, então deve ser tudo que você precisa (XML é muito prolixo e serializar de / para ele provavelmente seria um exagero, mas essa também seria uma opção).

Esta é uma boa resposta sobre como extrair CSVs com LINQ.

David Neale
fonte
Eu pensei sobre isso. Ainda significa que eu teria que serializar e desserializar ... mas suspeito que isso seja possível. Gostaria que houvesse uma maneira tolerada de fazer o que eu quero, mas suspeito que não.
JnBrymn de
capnproto.org é uma maneira de não ter que serializar e desserializar, de forma similarmente rápida (em comparação com csv ou xml) caso capnproto não seja suportado em seu idioma de escolha msgpack.org/index.html
VoronoiPotato
2

Se você precisar consultar na lista, armazene-o em uma tabela.

Se você sempre quiser a lista, poderá armazená-la como uma lista delimitada em uma coluna. Mesmo nesse caso, a menos que você tenha motivos MUITO específicos para não fazê-lo, armazene-o em uma tabela de pesquisa.

hometoast
fonte
1

Apenas uma opção não mencionada nas respostas. Você pode desnormalizar seu projeto de banco de dados. Portanto, você precisa de duas tabelas. Uma tabela contém a lista apropriada, um item por linha, outra tabela contém a lista inteira em uma coluna (separada por vírgulas, por exemplo).

Aqui está o design de banco de dados 'tradicional':

List(ListID, ListName) 
Item(ItemID,ItemName) 
List_Item(ListID, ItemID, SortOrder)

Aqui está a tabela desnormalizada:

Lists(ListID, ListContent)

A ideia aqui - você mantém a tabela Lists usando gatilhos ou código do aplicativo. Cada vez que você modifica o conteúdo de List_Item, as linhas apropriadas nas Listas são atualizadas automaticamente. Se você lê principalmente listas, pode funcionar muito bem. Prós - você pode ler listas em uma declaração. Contras - as atualizações exigem mais tempo e esforços.

Alsin
fonte
0

Se você realmente deseja armazená-lo em uma coluna e torná-lo consultável, muitos bancos de dados suportam XML agora. Se não estiver consultando, você pode armazená-los como valores separados por vírgula e analisá-los com uma função quando precisar deles separados. Eu concordo com todo mundo, entretanto, se você está procurando usar um banco de dados relacional, uma grande parte da normalização é a separação de dados como esse. Não estou dizendo que todos os dados cabem em um banco de dados relacional. Você sempre pode olhar para outros tipos de banco de dados se muitos dos seus dados não se encaixarem no modelo.

David Daniel
fonte
0

Acho que em certos casos, você pode criar uma "lista" FALSICA de itens no banco de dados, por exemplo, a mercadoria tem algumas fotos para mostrar seus detalhes, você pode concatenar todos os IDs de fotos divididos por vírgula e armazenar a string em o banco de dados, então você só precisa analisar a string quando precisar. Estou trabalhando em um site agora e estou planejando usar dessa forma.

Nen
fonte
0

Eu estava muito relutante em escolher o caminho que finalmente decidi seguir devido às muitas respostas. Enquanto eles adicionam mais compreensão ao que é SQL e seus princípios, decidi me tornar um fora da lei. Também hesitei em postar minhas descobertas, pois para alguns é mais importante desabafar com alguém que está quebrando as regras do que entender que existem poucas verdades universais.

Eu testei extensivamente e, no meu caso específico, era muito mais eficiente do que usar o tipo de array (generosamente oferecido pelo PostgreSQL) ou consultar outra tabela.

Aqui está minha resposta: Eu implementei com sucesso uma lista em um único campo no PostgreSQL, fazendo uso do comprimento fixo de cada item da lista. Digamos que cada item seja uma cor como um valor hexadecimal ARGB, que significa 8 caracteres. Portanto, você pode criar sua matriz de no máximo 10 itens, multiplicando pelo comprimento de cada item:

ALTER product ADD color varchar(80)

Caso o comprimento dos itens da lista seja diferente, você pode preencher o preenchimento com \ 0

NB: Obviamente, esta não é necessariamente a melhor abordagem para o número hexadecimal, uma vez que uma lista de inteiros consumiria menos armazenamento, mas isso é apenas para ilustrar essa ideia de array, fazendo uso de um comprimento fixo alocado para cada item.

O motivo: 1 / Muito conveniente: recupere o item i na substring i * n, (i +1) * n. 2 / Sem sobrecarga de consultas entre tabelas. 3 / Mais eficiente e econômico no lado do servidor. A lista é como um mini blob que o cliente terá que dividir.

Embora eu respeite as pessoas que seguem regras, muitas explicações são muito teóricas e muitas vezes deixam de reconhecer que, em alguns casos específicos, especialmente quando se busca o custo ideal com soluções de baixa latência, alguns pequenos ajustes são mais do que bem-vindos.

"Deus me livre que isso viole algum princípio sagrado do SQL": adotar uma abordagem mais aberta e pragmática antes de recitar as regras é sempre o caminho a percorrer. Caso contrário, você pode acabar como um fanático sincero recitando as Três Leis da Robótica antes de ser obliterado pela Skynet

Não pretendo que esta solução seja uma inovação, nem que seja ideal em termos de legibilidade e flexibilidade de banco de dados, mas certamente pode lhe dar uma vantagem quando se trata de latência.

Antonin GAVREL
fonte
Mas este é um caso muito específico: um número fixo de itens de comprimento fixo. Mesmo assim, torna uma pesquisa simples como "todos os produtos com pelo menos a cor x" mais difícil do que o SQL padrão faria.
Gert Arnold
Como afirmei várias vezes, não o uso para cores, o campo para o qual uso não deve ser indexado nem usado como uma condição e, ainda assim, é crítico
Antonin GAVREL
Eu sei, estou tentando indicar que isso é altamente específico. Se qualquer pequeno requisito adicional surgir, rapidamente se tornará mais complicado do que as soluções padrão. A grande maioria das pessoas que fica tentada a armazenar listas em um campo de banco de dados provavelmente não o faz.
Gert Arnold
0

Muitos bancos de dados SQL permitem que uma tabela contenha uma subtabela como um componente. O método usual é permitir que o domínio de uma das colunas seja uma tabela. Isso além de usar alguma convenção como CSV para codificar a subestrutura de maneiras desconhecidas para o SGBD.

Quando Ed Codd estava desenvolvendo o modelo relacional em 1969-1970, ele definiu especificamente uma forma normal que não permitiria esse tipo de aninhamento de tabelas. A forma normal foi posteriormente chamada de Primeira Forma Normal. Em seguida, ele mostrou que, para cada banco de dados, existe um banco de dados na primeira forma normal que expressa as mesmas informações.

Por que se preocupar com isso? Bem, os bancos de dados na primeira forma normal permitem o acesso por chave a todos os dados. Se você fornecer um nome de tabela, um valor-chave para essa tabela e um nome de coluna, o banco de dados conterá no máximo uma célula contendo um item de dados.

Se você permitir que uma célula contenha uma lista ou uma tabela ou qualquer outra coleção, agora você não pode fornecer acesso por chave aos subitens, sem retrabalhar completamente a ideia de uma chave.

O acesso por chave a todos os dados é fundamental para o modelo relacional. Sem esse conceito, o modelo não é relacional. Quanto ao motivo pelo qual o modelo relacional é uma boa ideia e quais podem ser as limitações dessa boa ideia, você deve olhar para os 50 anos de experiência acumulada com o modelo relacional.

Walter Mitty
fonte
-1

você pode armazená-lo como um texto que se parece com uma lista e criar uma função que pode retornar seus dados como uma lista real. exemplo:

base de dados:

 _____________________
|  word  | letters    |
|   me   | '[m, e]'   |
|  you   |'[y, o, u]' |  note that the letters column is of type 'TEXT'
|  for   |'[f, o, r]' |
|___in___|_'[i, n]'___|

E a função do compilador de lista (escrita em python, mas deve ser facilmente traduzível para a maioria das outras linguagens de programação). TEXT representa o texto carregado da tabela sql. retorna a lista de strings da string que contém a lista. se você quiser que ele retorne ints em vez de strings, torne o modo igual a 'int'. Da mesma forma com 'string', 'bool' ou 'float'.

def string_to_list(string, mode):
    items = []
    item = ""
    itemExpected = True
    for char in string[1:]:
        if itemExpected and char not in [']', ',', '[']:
            item += char
        elif char in [',', '[', ']']:
            itemExpected = True
            items.append(item)
            item = ""
    newItems = []
    if mode == "int":
        for i in items:
            newItems.append(int(i))

    elif mode == "float":
        for i in items:
            newItems.append(float(i))

    elif mode == "boolean":
        for i in items:
            if i in ["true", "True"]:
                newItems.append(True)
            elif i in ["false", "False"]:
                newItems.append(False)
            else:
                newItems.append(None)
    elif mode == "string":
        return items
    else:
        raise Exception("the 'mode'/second parameter of string_to_list() must be one of: 'int', 'string', 'bool', or 'float'")
    return newItems

Também aqui está uma função list-to-string, caso você precise.

def list_to_string(lst):
    string = "["
    for i in lst:
        string += str(i) + ","
    if string[-1] == ',':
        string = string[:-1] + "]"
    else:
        string += "]"
    return string
pessoa o humano
fonte