Est-il possible d'utiliser des listes dans une base de données relationnelle?

94

J'ai essayé de concevoir une base de données qui corresponde à un concept de projet et je me suis heurté à ce qui semble être une question très controversée. J'ai lu quelques articles et des réponses à Stack Overflow affirmant qu'il n'est jamais (ou presque jamais) de stocker une liste d'identifiants ou similaire dans un champ - toutes les données doivent être relationnelles, etc.

Le problème que je rencontre, cependant, est que j'essaie de faire un assignateur de tâches. Les gens vont créer des tâches, les attribuer à plusieurs personnes et les enregistrer dans la base de données.

Bien sûr, si je sauvegarde ces tâches individuellement dans "Personne", je devrai disposer de dizaines de colonnes "TaskID" factices et les gérer, car il peut y avoir 0 à 100 tâches attribuées à une personne, par exemple.

Là encore, si je sauvegarde les tâches dans un tableau "Tâches", il me faudra des douzaines de colonnes "PersonID" factices et les gérer, le même problème que précédemment.

Pour un problème comme celui-ci, est-il acceptable de sauvegarder une liste d'identifiants prenant une forme ou une autre ou ne pense-t-on pas d'une autre manière que cela est réalisable sans enfreindre les principes?

linus72982
la source
22
Je me rends compte que cela est étiqueté "base de données relationnelle", donc je vais le laisser comme un commentaire, pas une réponse, mais dans d'autres types de bases de données, il est logique de stocker des listes. On pense à Cassandra car elle n’a pas de jointure.
Captain Man
12
Bon travail dans la recherche et ensuite demander ici! En effet, la "recommandation" de ne jamais enfreindre la 1ère forme normale vous a vraiment bien servi, car vous devriez vraiment proposer une autre approche relationnelle, à savoir une relation "plusieurs-à-plusieurs", pour laquelle il existe un modèle standard. bases de données relationnelles à utiliser.
JimmyB
6
"Est-ce que ça va jamais" oui ... peu importe ce qui suit, la réponse est oui. Tant que vous avez une raison valable. Il y a toujours un cas d'utilisation qui vous oblige à enfreindre les meilleures pratiques, car il est logique de le faire. (Dans votre cas, cependant, vous ne devriez absolument pas le faire)
xyious
3
J'utilise actuellement un tableau ( pas une chaîne délimitée - a VARCHAR ARRAY) pour stocker une liste de balises. Ce n'est probablement pas la façon dont ils finiront par être stockés plus tard sur la ligne, mais les listes peuvent être extrêmement utiles pendant les étapes de prototypage, quand vous n'avez rien d'autre à signaler et que vous ne voulez pas construire le schéma de base de données complet avant de pouvoir le faire. faire autre chose.
Nic Hartley
3
@Ben " (bien qu'ils ne soient pas indexables) " - dans Postgres, plusieurs requêtes sur les colonnes JSON (et probablement XML, bien que je n'ai pas encore vérifié) sont indexables.
Nic Hartley

Réponses:

249

Le mot clé et le concept clé que vous devez étudier est la normalisation de la base de données .

Ce que vous feriez, plutôt que d'ajouter des informations sur les affectations aux tables de personnes ou de tâches, consiste à ajouter une nouvelle table avec ces informations d'attribution, avec des relations pertinentes.

Exemple, vous avez les tables suivantes:

Personnes:

+ −−−− + −−−−−−−−−−− + +
| ID | Nom |
+ ==== + =========== +
| 1 | Alfred |
| 2 | Jebediah |
| 3 | Jacob |
| 4 | Ezekiel |
+ −−−− + −−−−−−−−−−− + +

Les tâches:

+ −−−− + −−−−−−−−−−−−−−−−−−−− +
| ID | Nom |
+ ==== + ====================
| 1 | Nourrir les poulets |
| 2 | Charrue |
| 3 | Traire les vaches |
| 4 | Élever une grange |
+ −−−− + −−−−−−−−−−−−−−−−−−−− +

Vous créez ensuite une troisième table avec des affectations. Ce tableau modéliserait la relation entre les personnes et les tâches:

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

Nous aurions alors une contrainte de clé étrangère, telle que la base de données impose que les identifiants PersonId et TaskId doivent être des ID valides pour ces éléments étrangers. Pour la première rangée, nous pouvons voir PersonId is 1, donc Alfred , est assigné à TaskId 3, Traire les vaches .

Ce que vous devriez être capable de voir ici, c'est que vous pouvez avoir autant ou autant d'affectations que vous le souhaitez par tâche ou par personne. Dans cet exemple, aucune tâche n'est assignée à Ezekiel et Alfred est affecté à la tâche 2. Si vous avez une tâche avec 100 personnes, cela SELECT PersonId from Assignments WHERE TaskId=<whatever>;générera 100 lignes, avec différentes personnes affectées. Vous pouvez WHEREsur le PersonId pour trouver toutes les tâches assignées à cette personne.

Si vous souhaitez renvoyer des requêtes en remplaçant les ID par les noms et les tâches, vous devez apprendre à JOINDRE les tables.

comment s'appelle-t-il
la source
86
Le mot-clé que vous souhaitez rechercher pour en savoir plus est " relation plusieurs à plusieurs "
BlueRaja - Danny Pflughoeft
34
Pour en savoir un peu plus sur le commentaire Thierrys: Vous pouvez penser que vous n’avez pas besoin de normaliser car j’ai seulement besoin de X et c’est très simple de stocker la liste d’ID , mais vous regretterez de ne pas l’avoir normalisé pour tout système qui sera étendu ultérieurement. plus tôt. Toujours normaliser ; la seule question est à quelle forme normale
Jan Doggen
8
D'accord avec @Jan - contre mon meilleur jugement, j'ai permis à mon équipe de prendre un raccourci de conception il y a quelque temps, en stockant JSON à la place pour quelque chose qui "n'aura pas besoin d'être étendu". Cela a duré environ six mois, FML. Notre usine de traitement a ensuite eu une dure bataille pour faire migrer le JSON vers le schéma avec lequel nous aurions dû commencer. J'aurais vraiment dû savoir mieux.
Courses de légèreté en orbite
13
@Duplicator: il s'agit simplement d'une représentation d'une colonne de clé primaire entière à incrémentation automatique de type jardin. Trucs assez typiques.
Whatsisname
8
@whatsisname Sur la table Personnes ou Tâches, je suis d'accord avec vous. Sur une table de pont où le seul but est de représenter la relation plusieurs à plusieurs entre deux autres tables qui ont déjà des clés de substitution? Je n'en ajouterais pas sans une bonne raison. C'est simplement une surcharge car cela ne sera jamais utilisé dans des requêtes ou des relations.
jpmc26
35

Vous posez deux questions ici.

Tout d’abord, vous demandez s’il est acceptable de stocker des listes sérialisées dans une colonne. Oui ça ira. Si votre projet l’appelle. Un exemple pourrait être les ingrédients de produit pour une page de catalogue, dans lesquels vous ne souhaitez pas suivre chaque ingrédient individuellement.

Malheureusement, votre deuxième question décrit un scénario dans lequel vous devriez opter pour une approche plus relationnelle. Vous aurez besoin de 3 tables. Un pour les personnes, un pour les tâches et un qui conserve la liste des tâches assignées à chaque personne. Ce dernier serait vertical, une ligne par personne / combinaison de tâches, avec des colonnes pour votre clé primaire, identifiant de tâche et identifiant de personne.

Grand maître b
la source
9
L'exemple d'ingrédient que vous mentionnez est correct en surface; mais ce serait un texte en clair dans ce cas. Ce n'est pas une liste au sens de la programmation (à moins que vous ne vouliez dire que la chaîne est une liste de caractères que vous n'avez évidemment pas). Les OP qui décrivent leurs données comme "une liste d'identifiants" (ou même simplement "une liste de [..]") impliquent qu'ils traitent à un moment donné ces données en tant qu'objets individuels.
Flater
10
@Flater: Mais c'est une liste. Vous devez pouvoir le reformater en tant que liste (HTML), liste Markdown, liste JSON, etc. afin de vous assurer que les éléments sont correctement affichés dans une page Web, un document en texte brut, un document mobile. app ... et vous ne pouvez pas vraiment faire cela avec du texte brut.
Kevin
12
@ Kevin Si tel est votre objectif, vous pourrez l'atteindre beaucoup plus facilement en stockant les ingrédients dans un tableau! Sans parler de si, plus tard, les gens ... oh, je ne sais pas, par exemple, souhaiter des substituts recommandés , ou quelque chose d'aussi idiot que de rechercher toutes les recettes sans arachides, ni gluten, ni protéines animales ...
Dan Bron
10
@DanBron: YAGNI. Pour le moment, nous utilisons uniquement une liste car cela simplifie la logique de l'interface utilisateur. Si nous avons besoin ou aurons besoin comportement semblable à la liste dans la couche logique métier, alors il devrait être normalisé dans une table séparée. Les tables et les assemblages ne coûtent pas forcément cher, mais ils ne sont pas gratuits et ils posent des questions sur l'ordre des éléments ("Est-ce que nous nous soucions de l'ordre des ingrédients?") Et sur la normalisation ultérieure ("Allez-vous transformer '3 œufs'? dans ("oeufs", 3)? Qu'en est-il de "sel, pour goûter", est-ce ("sel", NULL)? ").
Kevin
7
@ Kevin: YAGNI a tout à fait tort ici. Vous avez vous-même fait valoir la nécessité de pouvoir transformer la liste de nombreuses manières (HTML, markdown, JSON) et plaidez donc que vous avez besoin des éléments individuels de la liste . Sauf si les applications de stockage de données et de "traitement de liste" sont deux applications développées indépendamment (et notez que des couches d'application distinctes! = Applications distinctes), la structure de la base de données doit toujours être créée pour stocker les données dans un format qui les laisse facilement disponibles. - en évitant une logique d'analyse / conversion supplémentaire.
Flater
22

Ce que vous décrivez est appelé relation "plusieurs à plusieurs", dans votre cas entre Personet Task. Il est généralement implémenté à l'aide d'une troisième table, parfois appelée table "lien" ou "référence croisée". Par exemple:

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)
);
Mike Partridge
la source
2
Vous pouvez également vouloir ajouter un index avec en task_idpremier, si vous exécutez des requêtes filtrées par tâche.
jpmc26
1
Aussi connu comme une table de bridge. Aussi, j'aimerais pouvoir vous donner un avantage supplémentaire pour ne pas avoir de colonne d'identité, bien que je recommande un index sur chaque colonne.
Jmoreno
13

... il n'est jamais (ou presque jamais) correct de stocker une liste d'identifiants ou autres dans un champ

Le seul moment où vous pouvez stocker plus d'un élément de données dans un seul champ est quand ce champ est que jamais utilisé comme une seule entité et jamais considérée comme étant composée de ces petits éléments. Un exemple pourrait être une image, stockée dans un champ BLOB. Il est composé de nombreux éléments plus petits (octets), mais ceux-ci ne signifient rien pour la base de données et ne peuvent être utilisés que dans leur ensemble (et attrayants pour un utilisateur final).

Etant donné qu'une "liste" est, par définition, composée d'éléments plus petits (éléments), ce n'est pas le cas ici et vous devez normaliser les données.

... si je sauvegarde ces tâches individuellement dans "Personne", il me faudra des douzaines de colonnes factices "TaskID" ...

Non, vous aurez quelques lignes dans une table d'intersection (ou entité faible) entre une personne et une tâche. Les bases de données sont vraiment efficaces pour travailler avec beaucoup de lignes; En fait, ils sont vraiment nuls au travail avec beaucoup de colonnes [répétées].

Bel exemple clair donné par whatsisname.

Phill W.
la source
4
Lors de la création de systèmes de la vie réelle, "ne jamais dire jamais" est une très bonne règle à respecter.
l0b0
1
Dans de nombreux cas, le coût par élément de la maintenance ou de l'extraction d'une liste sous forme normalisée peut largement dépasser le coût de la conservation des éléments sous forme de blob, car chaque élément de la liste devrait contenir l'identité de l'élément principal avec lequel il est associé. est associé et son emplacement dans la liste en plus des données réelles. Même dans les cas où le code pourrait bénéficier de la possibilité de mettre à jour certains éléments de la liste sans mettre à jour toute la liste, il peut être plus économique de tout stocker sous forme de blob et de tout réécrire chaque fois que l'on doit réécrire quoi que ce soit.
Supercat
4

Il peut être légitime dans certains champs pré-calculés.

Si certaines de vos requêtes sont coûteuses et que vous décidez d'utiliser des champs précalculés mis à jour automatiquement à l'aide de déclencheurs de base de données, il peut être légitime de conserver les listes dans une colonne.

Par exemple, dans l'interface utilisateur, vous souhaitez afficher cette liste à l'aide de la vue en grille, où chaque ligne peut ouvrir tous les détails (avec les listes complètes) après un double-clic sur:

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

Vous maintenez la deuxième colonne à jour par déclencheur lorsque le client visite le nouvel article ou par tâche planifiée.

Vous pouvez rendre un tel champ disponible même pour la recherche (en tant que texte normal).

Dans de tels cas, la tenue de listes est légitime. Il suffit de considérer le cas où la longueur de champ maximale pourrait être dépassée.


De plus, si vous utilisez Microsoft Access, les champs à plusieurs valeurs proposés constituent un autre cas d'utilisation spécial. Ils traitent automatiquement vos listes dans un champ.

Mais vous pouvez toujours revenir à la forme normalisée standard indiquée dans d'autres réponses.


Résumé: Les formes normales de base de données sont un modèle théorique nécessaire pour comprendre les aspects importants de la modélisation des données. Mais bien entendu, la normalisation ne prend pas en compte les performances ni les autres coûts d’extraction des données. Cela sort du cadre de ce modèle théorique. Toutefois, la mise en œuvre pratique nécessite souvent de stocker des listes ou d’autres doublons pré-calculés (et contrôlés).

À la lumière de ce qui précède, dans la mise en œuvre pratique, préférerions-nous une requête reposant sur une forme normale parfaite et une durée de 20 secondes ou une requête équivalente reposant sur des valeurs précalculées qui prennent 0,08 s? Personne n'aime que leur logiciel soit accusé de lenteur.

Miroxlav
la source
1
Cela peut être légitime même sans trucs précalculés. Je l'ai déjà fait plusieurs fois, lorsque les données sont stockées correctement, mais il est utile, pour des raisons de performances, d'insérer quelques résultats mis en cache dans les enregistrements principaux.
Loren Pechtel
@LorenPechtel - Oui, merci, à mon utilisation de terme précalculée I comprennent également les cas de valeurs mises en cache stockées en cas de besoin. Dans les systèmes avec des dépendances complexes, ils permettent de maintenir des performances normales. Et si elles sont programmées avec un savoir-faire adéquat, ces valeurs sont fiables et toujours synchronisées. Je ne voulais tout simplement pas ajouter de cas de mise en cache à la réponse pour que celle-ci reste simple et sûre. De toute façon, il a été rejeté. :)
miroxlav
@LorenPechtel En fait, ce serait toujours une mauvaise raison ... les données de cache devraient être conservées dans un cache store, et tant que le cache est toujours valide, cette requête ne doit jamais atteindre la base de données principale.
Tezra
1
@ Tezra Non, je dis que parfois, il est nécessaire de disposer d'une donnée d'une table secondaire assez souvent pour qu'il soit logique de mettre une copie dans l'enregistrement principal. (Exemple que j'ai fait - la table des employés inclut la dernière heure d'arrivée et la dernière heure. Elles sont utilisées uniquement à des fins d'affichage. Tout calcul réel provient de la table avec les enregistrements d'horloge d'entrée et de sortie.)
Loren Pechtel
0

Étant donné deux tables; nous les appellerons Personne et Tâche, chacun avec son propre ID (PersonID, TaskID) ... L'idée de base est de créer une troisième table pour les lier. Nous appellerons cette table PersonToTask. Au minimum, il devrait avoir son propre identifiant, ainsi que les deux autres identifiants. Donc, quand il s'agit d'affecter quelqu'un à une tâche; vous n'aurez plus besoin de METTRE À JOUR la table Personne, il vous suffira d'insérer une nouvelle ligne dans la table PersonToTaskTable. Et la maintenance devient plus facile: le besoin de supprimer une tâche devient simplement un DELETE basé sur TaskID, plus aucune mise à jour de la table Person et son analyse associée

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 diriez-vous d'un simple rapport ou de tous ceux qui sont affectés à une tâche?

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

Vous pourriez bien sûr en faire beaucoup plus; un TimeReport peut être effectué si vous avez ajouté des champs DateTime pour TaskAssigned et TaskCompleted. Cela ne tient qu'à toi

Mad Myche
la source
0

Cela peut fonctionner si vous avez des clés primaires lisibles par l'homme et souhaitez une liste de tâches sans avoir à gérer la nature verticale d'une structure de table. c'est à dire beaucoup plus facile à lire le premier tableau.

------------------------  
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
------------------------

La question serait alors: si la liste de tâches devait être stockée ou générée à la demande, cela dépendrait en grande partie de critères tels que: combien de fois la liste est-elle nécessaire, quel est le nombre exact de lignes de données, comment les données seront-elles utilisées, etc. .. après quoi l'analyse des compromis en fonction de l'expérience utilisateur et du respect des exigences doit être effectuée.

Par exemple, comparez le temps nécessaire pour rappeler les 2 lignes par rapport à l’exécution d’une requête qui générerait les 2 lignes. Si cela prend beaucoup de temps et que l'utilisateur n'a pas besoin de la liste la plus récente (* attend moins de 1 modification par jour), elle peut être stockée.

Ou si l'utilisateur a besoin d'un historique des tâches qui lui sont assignées, il serait également judicieux que la liste soit stockée. Donc, cela dépend vraiment de ce que vous faites, ne dites jamais jamais.

Double E CPU
la source
Comme vous le dites, tout dépend de la manière dont les données doivent être récupérées. Si vous / seulement / jamais interrogez cette table par nom d'utilisateur, le champ "liste" convient parfaitement. Cependant, comment pouvez-vous interroger une telle table pour savoir qui travaille sur la tâche n ° 1234567 tout en restant performante? À peu près tous les types de fonctions String "find-X-any-in-the-field" provoqueront une telle requête vers / Table Scan /, ralentissant ainsi le déroulement d'une analyse. Avec des données correctement normalisées et correctement indexées, cela ne se produit tout simplement pas.
Phill W.
0

Vous prenez ce qui devrait être une autre table, vous la tournez de 90 degrés et vous la frayez dans une autre table.

C'est comme avoir une table de commandes où vous avez itemProdcode1, itemQuantity1, itemPrice1 ... itemProdcode37, itemQuantity37, itemPrice37. En plus d'être délicat à gérer par programmation, vous pouvez garantir que demain, quelqu'un voudra commander 38 articles.

Je ne le ferais qu'à votre façon si la "liste" n'est pas vraiment une liste, c'est-à-dire qu'elle se trouve dans son ensemble et que chaque élément de campagne individuel ne fait pas référence à une entité claire et indépendante. Dans ce cas, insérez simplement le tout dans un type de données suffisamment volumineux.

Donc, un ordre est une liste, un Bill of Materials est une liste (ou une liste de listes, ce qui serait encore plus un cauchemar à mettre en œuvre "de côté"). Mais une note / commentaire et un poème ne le sont pas.

Bloke Down Le Pub
la source
0

Si c'est "pas ok", il est assez mauvais que chaque site Wordpress ait une liste dans wp_usermeta avec wp_capabilities dans une rangée, une liste de licenciés_wp_pointers dans une rangée, et d'autres ...

En fait, dans des cas comme celui-ci, la vitesse pourrait être meilleure car vous voudrez presque toujours la liste . Mais Wordpress n'est pas connu pour être l'exemple parfait des meilleures pratiques.

NoBugs
la source