Comment stocker une liste dans une colonne d'une table de base de données

117

Donc, selon la réponse de Mehrdad à une question connexe , je comprends qu'une colonne de table de base de données "appropriée" ne stocke pas de liste. Au lieu de cela, vous devez créer une autre table qui contient effectivement les éléments de ladite liste, puis créer un lien vers elle directement ou via une table de jonction. Cependant, le type de liste que je souhaite créer sera composé d'éléments uniques (contrairement au fruit de la question liéeexemple). De plus, les éléments de ma liste sont explicitement triés - ce qui signifie que si je stockais les éléments dans une autre table, je devrais les trier chaque fois que j'y accédais. Enfin, la liste est fondamentalement atomique en ce sens que chaque fois que je souhaite accéder à la liste, je veux accéder à la liste entière plutôt qu'à un morceau de celle-ci - il semble donc ridicule de devoir émettre une requête de base de données pour rassembler des éléments de la liste.

La solution d'AKX (lien ci-dessus) est de sérialiser la liste et de la stocker dans une colonne binaire. Mais cela semble également gênant car cela signifie que je dois me soucier de la sérialisation et de la désérialisation.

Y a-t-il une meilleure solution? S'il n'y a pas de solution meilleure, alors pourquoi? Il semble que ce problème devrait survenir de temps en temps.

... juste un peu plus d'informations pour vous dire d'où je viens. Dès que je venais de commencer à comprendre SQL et les bases de données en général, je me suis tourné vers LINQ to SQL, et maintenant je suis un peu gâté car je compte gérer mon modèle d'objet de programmation sans avoir à réfléchir à la façon dont les objets sont interrogés ou stockés dans la base de données.

Merci a tous!

John

MISE À JOUR: Donc, dans la première vague de réponses que j'obtiens, je vois "vous pouvez aller sur la route CSV / XML ... mais NE PAS!". Alors maintenant, je cherche des explications sur pourquoi. Montrez-moi quelques bonnes références.

Aussi, pour vous donner une meilleure idée de ce que je fais: Dans ma base de données, j'ai une table Function qui contiendra une liste de paires (x, y). (Le tableau contiendra également d'autres informations sans conséquence pour notre discussion.) Je n'aurai jamais besoin de voir une partie de la liste des paires (x, y). Je vais plutôt les prendre tous et les tracer à l’écran. Je vais permettre à l'utilisateur de faire glisser les nœuds pour changer les valeurs de temps en temps ou ajouter plus de valeurs au tracé.

JnBrymn
la source

Réponses:

183

Non, il n'y a pas de «meilleure» façon de stocker une séquence d'éléments dans une seule colonne. Les bases de données relationnelles sont conçues spécifiquement pour stocker une valeur par combinaison ligne / colonne. Afin de stocker plus d'une valeur, vous devez sérialiser votre liste en une seule valeur pour le stockage, puis la désérialiser lors de la récupération. Il n'y a pas d'autre moyen de faire ce dont vous parlez (car ce dont vous parlez est une mauvaise idée qui, en général, ne devrait jamais être faite ).

Je comprends que vous pensez qu'il est idiot de créer une autre table pour stocker cette liste, mais c'est exactement ce que font les bases de données relationnelles. Vous menez une bataille ardue et enfreignez l'un des principes les plus fondamentaux de la conception de bases de données relationnelles sans raison valable. Puisque vous déclarez que vous venez d'apprendre SQL, je vous conseillerais fortement d'éviter cette idée et de vous en tenir aux pratiques recommandées par des développeurs SQL plus expérimentés.

Le principe que vous enfreignez est appelé la première forme normale , qui est la première étape de la normalisation de la base de données.

Au risque de trop simplifier les choses, la normalisation de la base de données est le processus de définition de votre base de données en fonction de ce que les données est , de sorte que vous pouvez écrire des requêtes sensibles, cohérentes contre et être en mesure de maintenir facilement. La normalisation est conçue pour limiter les incohérences logiques et la corruption dans vos données, et elle comporte de nombreux niveaux. L'article de Wikipedia sur la normalisation des bases de données est en fait assez bon.

Fondamentalement, la première règle (ou forme) de normalisation stipule que votre table doit représenter une relation. Cela signifie que:

  • Vous devez être capable de différencier une ligne de n'importe quelle autre ligne (en d'autres termes, votre table doit avoir quelque chose qui peut servir de clé primaire. Cela signifie également qu'aucune ligne ne doit être dupliquée.
  • Tout ordre des données doit être défini par les données, pas par l'ordre physique des lignes (SQL est basé sur l'idée d'un ensemble, ce qui signifie que le seul ordre sur lequel vous devez vous fier est celui que vous définissez explicitement dans votre requête)
  • Chaque intersection ligne / colonne doit contenir une et une seule valeur

Le dernier point est évidemment le point saillant ici. SQL est conçu pour stocker vos ensembles pour vous, et non pour vous fournir un «compartiment» pour que vous puissiez stocker un ensemble vous-même. Oui, c'est possible de le faire. Non, le monde ne s'arrêtera pas. Cependant, vous vous êtes déjà paralysé dans la compréhension de SQL et des meilleures pratiques qui l'accompagnent en vous jetant immédiatement dans l'utilisation d'un ORM. LINQ to SQL est fantastique, tout comme les calculatrices graphiques. Dans le même ordre d'idées, cependant, ils ne devraient pas être utilisés comme un substitut pour savoir comment les processus qu'ils emploient fonctionnent réellement.

Votre liste est peut-être entièrement "atomique" maintenant, et cela ne changera peut-être pas pour ce projet. Mais vous prendrez cependant l'habitude de faire des choses similaires dans d'autres projets, et vous finirez par (probablement rapidement) tomber sur un scénario dans lequel vous ajustez maintenant votre liste dans une colonne rapide et facile. approche là où elle est totalement inappropriée. Il n'y a pas beaucoup de travail supplémentaire pour créer la table correcte pour ce que vous essayez de stocker, et vous ne serez pas ridiculisé par les autres développeurs SQL lorsqu'ils verront la conception de votre base de données. De plus, LINQ to SQL va voir votre relation et vous donner automatiquement l' interface orientée objet appropriée à votre liste . Pourquoi abandonneriez-vous la commodité qui vous est offerte par l'ORM afin de pouvoir effectuer un piratage de base de données non standard et peu judicieux?

Adam Robinson
la source
17
Vous croyez donc fermement que stocker une liste dans une colonne est une mauvaise idée, mais vous oubliez de dire pourquoi. Puisque je ne fais que commencer avec SQL, un peu du «pourquoi» serait vraiment très utile. Par exemple, vous dites que je «mène une bataille difficile et que je viole l'un des principes les plus fondamentaux de la conception de bases de données relationnelles sans raison valable» ... alors quel est le principe? Pourquoi les raisons que j'ai citées sont-elles «pas bonnes»? (plus précisément, la nature triée et atomique de mes listes)
JnBrymn
6
Fondamentalement, cela se résume à des années d'expérience condensées dans les meilleures pratiques. Le principe de base en question est connu sous le nom de 1ère forme normale .
Toby
1
Merci Adam. Très instructif. Bon point avec votre dernière question.
JnBrymn
8
"[…] Et vous ne serez pas ridiculisé par les autres développeurs SQL lorsqu'ils verront la conception de votre base de données." Il y a de très bonnes raisons de respecter la première forme normale (et votre réponse les mentionne), mais la pression des pairs / «c'est ainsi que les choses se font ici» n'en fait pas partie.
Lynn le
5
Nous stockons déjà des lots de listes dans des colonnes de base de données chaque jour. Ils sont appelés "char" et "varchar". Bien sûr, dans Postgres, ils sont également appelés texte. Ce que le 1NF dit vraiment, c'est que vous ne devriez jamais vouloir diviser les informations dans aucun champ en champs plus petits, et si vous faites cela, vous avez fait une gaffe. Ainsi, vous ne stockez pas le nom, vous stockez le nom personnel, les prénoms et les noms de famille (selon la localisation), et vous les assemblez. Sinon, nous ne stockerions pas du tout les chaînes de texte. D'un autre côté, tout ce qu'il veut, c'est une chaîne de cordes. Et il existe des moyens de le faire.
Haakon Løtveit
15

Vous pouvez simplement oublier SQL tous ensemble et opter pour une approche "NoSQL". RavenDB , MongoDB et CouchDB viennent à l'esprit en tant que solutions possibles. Avec une approche NoSQL, vous n'utilisez pas le modèle relationnel. Vous n'êtes même pas contraint de schémas.

jaltiere
la source
11

Voici ce que j'ai vu beaucoup de gens faire (ce n'est peut-être pas la meilleure approche, corrigez-moi si je me trompe):

Le tableau que j'utilise dans l'exemple est donné ci-dessous (le tableau comprend les surnoms que vous avez donnés à vos petites amies spécifiques. Chaque petite amie a un identifiant unique):

nicknames(id,seq_no,names)

Supposons que vous souhaitiez stocker de nombreux surnoms sous un identifiant. C'est pourquoi nous avons inclus un seq_nochamp.

Maintenant, remplissez ces valeurs dans votre table:

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

Si vous voulez trouver tous les noms que vous avez donnés à votre petite amie id 1, vous pouvez utiliser:

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

Réponse simple: Si, et seulement si, vous êtes certain que la liste sera toujours utilisée comme une liste, joignez la liste de votre côté avec un caractère (tel que '\ 0') qui ne sera pas utilisé dans le texte jamais, et stockez-le. Ensuite, lorsque vous le récupérez, vous pouvez le diviser par '\ 0'. Il existe bien sûr d'autres façons de procéder, mais celles-ci dépendent de votre fournisseur de base de données spécifique.

À titre d'exemple, vous pouvez stocker JSON dans une base de données Postgres. Si votre liste est du texte et que vous voulez juste la liste sans autre tracas, c'est un compromis raisonnable.

D'autres ont osé des suggestions de sérialisation, mais je ne pense pas vraiment que la sérialisation soit une bonne idée: une partie de la chose intéressante à propos des bases de données est que plusieurs programmes écrits dans des langues différentes peuvent se parler. Et les programmes sérialisés au format Java ne feraient pas très bien si un programme Lisp voulait le charger.

Si vous voulez un bon moyen de faire ce genre de chose, il existe généralement des types de tableaux ou similaires disponibles. Postgres, par exemple, propose un tableau comme type et vous permet de stocker un tableau de texte, si c'est ce que vous voulez , et il existe des astuces similaires pour MySql et MS SQL utilisant JSON, et DB2 d'IBM propose également un type de tableau (dans leur propre documentation utile ). Ce ne serait pas si courant s'il n'y avait pas besoin de cela.

Ce que vous perdez en empruntant cette voie, c'est la notion de liste comme un ensemble de choses en séquence. Au moins nominalement, les bases de données traitent les champs comme des valeurs uniques. Mais si c'est tout ce que vous voulez, alors vous devriez y aller. C'est un jugement de valeur que vous devez faire pour vous-même.

Haakon Løtveit
la source
3

En plus de ce que tout le monde a dit, je vous suggère d'analyser votre approche à plus long terme que maintenant. Il est actuellement le cas que les éléments sont uniques. Il est actuellement le cas que le recours aux éléments nécessiterait une nouvelle liste. Il est presque obligatoire que la liste soit actuellement courte. Même si je n'ai pas les spécificités du domaine, il n'est pas difficile de penser que ces exigences pourraient changer. Si vous sérialisez votre liste, vous cuisez dans une rigidité qui n'est pas nécessaire dans une conception plus normalisée. Btw, cela ne signifie pas nécessairement une relation Many: Many complète. Vous pouvez simplement avoir une seule table enfant avec une clé étrangère vers le parent et une colonne de caractères pour l'élément.

Si vous souhaitez continuer à sérialiser la liste, vous pouvez envisager de stocker la liste au format XML. Certaines bases de données telles que SQL Server ont même un type de données XML. La seule raison pour laquelle je suggère XML est que, presque par définition, cette liste doit être courte. Si la liste est longue, la sérialiser en général est une approche horrible. Si vous utilisez la route CSV, vous devez tenir compte des valeurs contenant le délimiteur, ce qui signifie que vous êtes obligé d'utiliser des identificateurs entre guillemets. En supposant que les listes sont courtes, le fait d'utiliser CSV ou XML ne changera probablement pas grand-chose.

Thomas
la source
+1 pour anticiper les changements futurs - concevez toujours votre modèle de données pour qu'il soit extensible.
coolgeek
2

Je le stockerais simplement au format CSV, si ce sont des valeurs simples, cela devrait être tout ce dont vous avez besoin (XML est très verbeux et sérialiser vers / à partir de celui-ci serait probablement excessif, mais ce serait également une option).

Voici une bonne réponse pour savoir comment extraire des CSV avec LINQ.

David Neale
la source
J'ai pensé à ça. Cela signifie toujours que je devrais sérialiser et désérialiser ... mais je suppose que c'est faisable. J'aurais aimé qu'il y ait un moyen toléré de faire ce que je veux, mais je soupçonne qu'il n'y en a pas.
JnBrymn
capnproto.org est un moyen de ne pas avoir à sérialiser et désérialiser, tout aussi rapide (par rapport à csv ou xml) au cas où capnproto ne serait pas pris en charge dans la langue de votre choix msgpack.org/index.html
VoronoiPotato
2

Si vous devez interroger la liste, stockez-la dans une table.

Si vous voulez toujours la liste, vous pouvez la stocker sous forme de liste délimitée dans une colonne. Même dans ce cas, sauf si vous avez des raisons TRÈS spécifiques de ne pas le faire, stockez-le dans une table de consultation.

à domicile
la source
1

Une seule option n'est pas mentionnée dans les réponses. Vous pouvez dé-normaliser votre conception de base de données. Vous avez donc besoin de deux tables. Une table contient la liste appropriée, un élément par ligne, une autre table contient la liste entière dans une colonne (séparée par des virgules, par exemple).

Ici, c'est la conception DB `` traditionnelle '':

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

Ici, il s'agit d'un tableau dénormalisé:

Lists(ListID, ListContent)

L'idée ici - vous gérez la table des listes à l'aide de déclencheurs ou de code d'application. Chaque fois que vous modifiez le contenu List_Item, les lignes appropriées dans les listes sont mises à jour automatiquement. Si vous lisez principalement des listes, cela pourrait fonctionner très bien. Avantages - vous pouvez lire les listes en une seule déclaration. Inconvénients - les mises à jour prennent plus de temps et d'efforts.

Alsin
la source
0

Si vous voulez vraiment le stocker dans une colonne et le rendre interrogeable, de nombreuses bases de données prennent désormais en charge XML. Sinon, vous pouvez les stocker en tant que valeurs séparées par des virgules et les analyser avec une fonction lorsque vous en avez besoin. Je suis d'accord avec tout le monde cependant si vous cherchez à utiliser une base de données relationnelle, une grande partie de la normalisation est la séparation des données comme ça. Je ne dis pas que toutes les données correspondent à une base de données relationnelle. Vous pouvez toujours consulter d'autres types de bases de données si une grande partie de vos données ne correspond pas au modèle.

David Daniel
la source
0

Je pense que dans certains cas, vous pouvez créer une FAKE "liste" d'articles dans la base de données, par exemple, la marchandise a quelques images pour montrer ses détails, vous pouvez concaténer tous les identifiants d'images divisés par une virgule et stocker la chaîne dans la base de données, il vous suffit d'analyser la chaîne lorsque vous en avez besoin. Je travaille actuellement sur un site Web et je prévois de l'utiliser de cette façon.

Nen
la source
0

J'étais très réticent à choisir la voie que je décide finalement d'emprunter à cause de nombreuses réponses. Bien qu'ils ajoutent plus de compréhension à ce qu'est SQL et à ses principes, j'ai décidé de devenir un hors-la-loi. J'ai également hésité à publier mes conclusions car pour certains, il est plus important d'exprimer la frustration de quelqu'un qui enfreint les règles plutôt que de comprendre qu'il y a très peu de vérités universelles.

Je l'ai testé de manière approfondie et, dans mon cas spécifique, il était bien plus efficace que d'utiliser le type de tableau (généreusement offert par PostgreSQL) ou d'interroger une autre table.

Voici ma réponse: j'ai implémenté avec succès une liste dans un seul champ dans PostgreSQL, en utilisant la longueur fixe de chaque élément de la liste. Supposons que chaque élément soit une couleur en tant que valeur hexadécimale ARVB, cela signifie 8 caractères. Ainsi, vous pouvez créer votre tableau de 10 éléments maximum en multipliant par la longueur de chaque élément:

ALTER product ADD color varchar(80)

Si la longueur des éléments de votre liste diffère, vous pouvez toujours remplir le remplissage avec \ 0

NB: Evidemment ce n'est pas forcément la meilleure approche pour le nombre hexadécimal car une liste d'entiers consommerait moins de stockage mais c'est juste dans le but d'illustrer cette idée de tableau en utilisant une longueur fixe allouée à chaque élément.

La raison pour laquelle: 1 / Très pratique: récupérer l'élément i à la sous-chaîne i * n, (i +1) * n. 2 / Pas de surcharge des requêtes de tables croisées. 3 / Plus efficace et plus économique côté serveur. La liste est comme un mini blob que le client devra fractionner.

Bien que je respecte les gens qui suivent des règles, de nombreuses explications sont très théoriques et omettent souvent de reconnaître que, dans certains cas spécifiques, en particulier lorsque l'on vise un coût optimal avec des solutions à faible latence, certains ajustements mineurs sont plus que bienvenus.

"Dieu nous en préserve qu'il viole un principe sacré sacré de SQL": Adopter une approche plus ouverte d'esprit et pragmatique avant de réciter les règles est toujours la voie à suivre. Sinon, vous pourriez finir comme un fanatique candide récitant les trois lois de la robotique avant d'être effacé par Skynet

Je ne prétends pas que cette solution soit une percée, ni qu'elle soit idéale en termes de lisibilité et de flexibilité de base de données, mais elle peut certainement vous donner un avantage en matière de latence.

Antonin GAVREL
la source
Mais c'est un cas très particulier: un nombre fixe d'éléments de longueur fixe. Même dans ce cas, il rend une recherche simple comme "tous les produits ayant au moins une couleur x" plus difficile que le SQL standard.
Gert Arnold
Comme je l'ai dit plusieurs fois, je ne l'utilise pas pour la couleur, le champ dans
lequel
Je sais, j'essaie d'indiquer que c'est très précis. Si une petite exigence supplémentaire se faufile, cela devient rapidement plus gênant que les solutions standard. La grande majorité des gens qui sont tentés de stocker des listes dans un seul champ de base de données feraient probablement mieux de ne pas le faire.
Gert Arnold
0

De nombreuses bases de données SQL permettent à une table de contenir une sous-table en tant que composant. La méthode habituelle consiste à autoriser le domaine de l'une des colonnes à être une table. Cela s'ajoute à l'utilisation d'une convention telle que CSV pour coder la sous-structure d'une manière inconnue du SGBD.

Lorsque Ed Codd développait le modèle relationnel en 1969-1970, il a spécifiquement défini une forme normale qui interdirait ce type d'imbrication de tables. La forme normale a ensuite été appelée première forme normale. Il a ensuite montré que pour chaque base de données, il existe une base de données dans la première forme normale qui exprime les mêmes informations.

Pourquoi s'embêter avec ça? Eh bien, les bases de données dans la première forme normale permettent un accès par clé à toutes les données. Si vous fournissez un nom de table, une valeur de clé dans cette table et un nom de colonne, la base de données contiendra au plus une cellule contenant un élément de données.

Si vous autorisez une cellule à contenir une liste, un tableau ou toute autre collection, vous ne pouvez plus fournir un accès par clé aux sous-éléments, sans retravailler complètement l'idée d'une clé.

L'accès par clé à toutes les données est fondamental pour le modèle relationnel. Sans ce concept, le modèle n'est pas relationnel. Quant à savoir pourquoi le modèle relationnel est une bonne idée, et quelles pourraient être les limites de cette bonne idée, vous devez examiner les 50 années d'expérience accumulées avec le modèle relationnel.

Walter Mitty
la source
-1

vous pouvez le stocker sous forme de texte qui ressemble à une liste et créer une fonction qui peut renvoyer ses données sous forme de liste réelle. exemple:

base de données:

 _____________________
|  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]'___|

Et la fonction de compilation de liste (écrite en python, mais elle devrait être facilement traduisible dans la plupart des autres langages de programmation). TEXT représente le texte chargé à partir de la table sql. renvoie la liste des chaînes de la chaîne contenant la liste. si vous voulez qu'il renvoie des entiers au lieu de chaînes, rendez le mode égal à «int». De même avec '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

Voici également une fonction de liste à chaîne au cas où vous en auriez besoin.

def list_to_string(lst):
    string = "["
    for i in lst:
        string += str(i) + ","
    if string[-1] == ',':
        string = string[:-1] + "]"
    else:
        string += "]"
    return string
personne l'humain
la source