Conception de base de données - différents objets avec marquage partagé

8

Mon expérience est plus dans la programmation Web que dans l'administration de base de données, alors corrigez-moi si j'utilise la mauvaise terminologie ici. J'essaie de trouver la meilleure façon de concevoir la base de données pour une application que je vais coder.

La situation: j'ai des rapports dans un tableau et des recommandations dans un autre tableau. Chaque rapport peut contenir de nombreuses recommandations. J'ai également un tableau séparé pour les mots clés (pour implémenter le balisage). Cependant, je veux avoir un seul ensemble de mots clés qui s'applique aux rapports et aux recommandations afin que la recherche sur les mots clés vous donne des rapports et des recommandations en tant que résultats.

Voici la structure avec laquelle j'ai commencé:

Reports
----------
ReportID
ReportName


Recommendations
----------
RecommendationID
RecommendationName
ReportID (foreign key)


Keywords
----------
KeywordID
KeywordName


ObjectKeywords
----------
KeywordID (foreign key)
ReportID (foreign key)
RecommendationID (foreign key)

Instinctivement, j'ai l'impression que ce n'est pas optimal et que je devrais faire hériter mes objets étiquetables d'un parent commun et faire étiqueter ce parent de commentaire, ce qui donnerait la structure suivante:

BaseObjects
----------
ObjectID (primary key)
ObjectType


Reports
----------
ObjectID_Report (foreign key)
ReportName


Recommendations
----------
ObjectID_Recommendation (foreign key)
RecommendationName
ObjectID_Report (foreign key)


Keywords
----------
KeywordID (primary key)
KeywordName


ObjectKeywords
----------
ObjectID (foreign key)
KeywordID (foreign key)

Dois-je aller avec cette deuxième structure? Suis-je en train de manquer des préoccupations importantes ici? De plus, si je choisis le second, que dois-je utiliser comme nom non générique pour remplacer "Object"?

Mise à jour:

J'utilise SQL Server pour ce projet. Il s'agit d'une application interne avec un petit nombre d'utilisateurs non simultanés, donc je ne prévois pas une charge élevée. En termes d'utilisation, les mots clés seront probablement utilisés avec parcimonie. C'est à peu près juste à des fins de rapports statistiques. En ce sens, quelle que soit la solution que j'utilise, cela n'affectera probablement que les développeurs qui devront maintenir ce système en ligne ... mais je me suis dit qu'il était bon de mettre en œuvre de bonnes pratiques chaque fois que je le pouvais. Merci pour toute la perspicacité!

matikin9
la source
Il semble que vous n'ayez pas répondu à la question la plus importante - Comment les données seront-elles accessibles? - Pour quelles requêtes / instructions souhaitez-vous "régler" votre modèle? - Comment prévoyez-vous d'étendre la fonctionnalité? Je pense qu'il n'y a pas de meilleure pratique générale - la solution dépend des réponses à ces questions. Et cela commence à compter même dans les modèles simples comme celui-ci. Ou vous pouvez vous retrouver avec un modèle qui suit certains principes plus élevés mais qui aspire vraiment dans les scénarios les plus importants - ceux vus par les utilisateurs du système.
Štefan Oravec
Bon point! Je vais devoir y réfléchir un peu!
matikin9

Réponses:

6

Le problème avec votre premier exemple est la table tri-link. Cela nécessitera-t-il que l'une des clés étrangères du rapport ou des recommandations soit toujours NULL pour que les mots clés ne soient liés que dans un sens ou dans l'autre?

Dans le cas de votre deuxième exemple, la jointure de la base aux tables dérivées peut maintenant nécessiter l'utilisation du sélecteur de type ou de LEFT JOIN selon la façon dont vous le faites.

Compte tenu de cela, pourquoi ne pas simplement le rendre explicite et éliminer tous les NULL et LEFT JOINs?

Reports
----------
ReportID
ReportName


Recommendations
----------
RecommendationID
RecommendationName
ReportID (foreign key)


Keywords
----------
KeywordID
KeywordName


ReportKeywords
----------
KeywordID (foreign key)
ReportID (foreign key)

RecommendationKeywords
----------
KeywordID (foreign key)
RecommendationID (foreign key)

Dans ce scénario, lorsque vous ajoutez quelque chose d'autre qui doit être balisé, vous ajoutez simplement la table d'entités et la table de liaison.

Ensuite, vos résultats de recherche ressemblent à ceci (voyez qu'il y a toujours une sélection de type en cours et les transformer en génériques au niveau des résultats de l'objet si vous voulez une seule liste de résultats):

SELECT CAST('REPORT' AS VARCHAR(15)) AS ResultType
    ,Reports.ReportID AS ObjectID
    ,Reports.ReportName AS ObjectName
FROM Keywords
INNER JOIN ReportKeywords
    ON ReportKeywords.KeywordID = Keywords.KeywordID
INNER JOIN Reports
    ON Reports.ReportID = ReportKeywords.ReportID
WHERE Keywords.KeywordName LIKE '%' + @SearchCriteria + '%'
UNION ALL
SELECT 'RECOMMENDATION' AS ResultType
    ,Recommendations.RecommendationID AS ObjectID
    ,Recommendations.RecommendationName AS ObjectName
FROM Keywords
INNER JOIN RecommendationKeywords
    ON RecommendationKeywords.KeywordID = Keywords.KeywordID
INNER JOIN Recommendations
    ON Recommendations.RecommendationID = RecommendationKeywords.ReportID
WHERE Keywords.KeywordName LIKE '%' + @SearchCriteria + '%'

Quoi qu'il en soit, quelque part il y aura une sélection de type et une sorte de branchement en cours.

Si vous regardez comment vous le feriez dans votre option 1, c'est similaire, mais avec une instruction CASE ou LEFT JOINs et un COALESCE. Au fur et à mesure que vous développez votre option 2 avec plus de choses liées, vous devez continuer à ajouter plus de JOINTS GAUCHE là où les choses ne sont généralement PAS trouvées (un objet lié ne peut avoir qu'une seule table dérivée qui est valide).

Je ne pense pas qu'il y ait quelque chose de fondamentalement mauvais avec votre option 2, et vous pourriez réellement la faire ressembler à cette proposition en utilisant des vues.

Dans votre option 1, j'ai du mal à comprendre pourquoi vous avez opté pour la table tri-link.

Cade Roux
la source
Le tableau à trois liens que vous mentionnez est probablement dû au fait que je suis mentalement paresseux ...: P Après avoir lu les différentes réponses, je pense qu'aucune de mes options initiales n'a de sens. Il est plus pratique d'avoir des tables ReportKeywords et RecommendationKeywords séparées. J'envisageais l'évolutivité, en termes de potentiellement avoir plus d'objets qui nécessitaient des mots clés appliqués, mais en réalité, il n'y a probablement qu'un seul type d'objet de plus qui pourrait avoir besoin de mots clés.
matikin9
4

Tout d'abord, notez que la solution idéale dépend dans une certaine mesure du SGBDR que vous utilisez. Je vais alors donner à la fois la réponse standard et la réponse spécifique à PostgreSQL.

Réponse normalisée et standard

La réponse standard est d'avoir deux tables de jointure.

Supposons que nous ayons nos tables:

CREATE TABLE keywords (
     kword text
);

CREATE TABLE reports (
     id serial not null unique,
     ...
);

CREATE TABLE recommendations (
     id serial not null unique,
     ...
);

CREATE TABLE report_keywords (
     report_id int not null references reports(id),
     keyword text not null references keyword(kword),
     primary key (report_id, keyword)
);

CREATE TABLE recommendation_keywords (
     recommendation_id int not null references recommendation(id),
     keyword text not null references keyword(kword),
     primary key (recommendation_id, keyword)
);

Cette approche suit toutes les règles de normalisation standard et ne rompt pas les principes de normalisation de base de données traditionnels. Il devrait fonctionner sur n'importe quel SGBDR.

Réponse spécifique à PostgreSQL, conception N1NF

Tout d'abord, un mot sur la raison pour laquelle PostgreSQL est différent. PostgreSQL prend en charge un certain nombre de façons très utiles d'utiliser des index sur des tableaux, notamment en utilisant ce que l'on appelle les index GIN. Ceux-ci peuvent améliorer considérablement les performances s'ils sont utilisés correctement ici. Étant donné que PostgreSQL peut "atteindre" les types de données de cette manière, l'hypothèse de base d'atomicité et de normalisation est quelque peu problématique à appliquer de manière rigide ici. Donc, pour cette raison, ma recommandation serait de briser la règle d'atomicité de la première forme normale et de s'appuyer sur les index GIN pour de meilleures performances.

Une deuxième remarque ici est que bien que cela donne de meilleures performances, cela ajoute quelques maux de tête car vous aurez un travail manuel à faire pour que l'intégrité référentielle fonctionne correctement. Donc, le compromis ici est la performance pour le travail manuel.

CREATE TABLE keyword (
    kword text primary key
);

CREATE FUNCTION check_keywords(in_kwords text[]) RETURNS BOOL LANGUAGE SQL AS $$

WITH kwords AS ( SELECT array_agg(kword) as kwords FROM keyword),
     empty AS (SELECT count(*) = 0 AS test FROM unnest($1)) 
SELECT bool_and(val = ANY(kwords.kwords))
  FROM unnest($1) val
 UNION
SELECT test FROM empty WHERE test;
$$;

CREATE TABLE reports (
     id serial not null unique,
     ...
     keywords text[]   
);

CREATE TABLE recommendations (
     id serial not null unique,
     ...
     keywords text[]  
);

Nous devons maintenant ajouter des déclencheurs pour garantir la bonne gestion des mots clés.

CREATE OR REPLACE FUNCTION trigger_keyword_check() RETURNS TRIGGER
LANGUAGE PLPGSQL AS
$$
BEGIN
    IF check_keywords(new.keywords) THEN RETURN NEW
    ELSE RAISE EXCEPTION 'unknown keyword entered'
    END IF;
END;
$$;

CREATE CONSTRAINT TRIGGER check_keywords AFTER INSERT OR UPDATE TO reports
WHEN (old.keywords <> new.keywords)
FOR EACH ROW EXECUTE PROCEDURE trigger_keyword_check();

CREATE CONSTRAINT TRIGGER check_keywords AFTER INSERT OR UPDATE 
TO recommendations
WHEN (old.keywords <> new.keywords)
FOR EACH ROW EXECUTE PROCEDURE trigger_keyword_check();

Deuxièmement, nous devons décider quoi faire lorsqu'un mot clé est supprimé. Dans l'état actuel des choses, un mot clé supprimé de la table des mots clés ne se répercutera pas en cascade dans les champs des mots clés. C'est peut-être souhaitable et peut-être pas. La chose la plus simple à faire est de restreindre toujours la suppression et de vous attendre à ce que vous gériez manuellement ce cas s'il se produit (utilisez un déclencheur pour plus de sécurité ici). Une autre option peut être de réécrire chaque valeur de mot-clé là où le mot-clé existe pour le supprimer. Encore une fois, un déclencheur serait le moyen de le faire également.

Le grand avantage de cette solution est que vous pouvez indexer des recherches très rapides par mot-clé et que vous pouvez extraire toutes les balises sans jointure. L'inconvénient est que la suppression d'un mot-clé est pénible et ne fonctionnera pas bien même par une bonne journée. Cela peut être acceptable car il s'agit d'un événement rare et pourrait être consigné dans un processus d'arrière-plan, mais c'est un compromis qui mérite d'être compris.

Critiquer votre première solution

Le vrai problème avec votre première solution est que vous n'avez aucune clé possible sur ObjectKeywords. Par conséquent, vous avez un problème où vous ne pouvez pas garantir que chaque mot clé n'est appliqué à chaque objet qu'une seule fois.

Votre deuxième solution est un peu meilleure. Si vous n'aimez pas les autres solutions proposées, je vous suggère d'y aller. Je suggérerais cependant de se débarrasser de keyword_id et de simplement se joindre au texte du mot clé. Cela élimine une jointure sans dénormalisation.

Chris Travers
la source
J'utilise MS SQL Server pour ce projet, mais merci pour les informations sur PostgreSQL. Les autres points que vous avez soulevés concernant la suppression et la vérification que les paires objet-mot-clé n'apparaissent chacune qu'une seule fois. Même si j'avais des clés pour chaque paire objet-mot-clé, ne devrais-je pas encore vérifier avant l'insertion? Quant à avoir un identifiant de mot clé distinct ... J'ai lu que pour SQL Server, avoir une chaîne longue pourrait réduire les performances, et je vais probablement devoir autoriser les utilisateurs à saisir des "phrases clés" plutôt que des "mots clés" ".
matikin9
0

Je suggérerais deux structures distinctes:

report_keywords
---------------
  ID du rapport
  ID de mot clé

recommendation_keywords
-----------------------
  recommendation_id
  keyword_id

De cette façon, vous n'avez pas tous les identifiants d'entité possibles dans la même table (ce qui n'est pas très évolutif et pourrait être déroutant), et vous n'avez pas de table avec un "id d'objet" générique que vous devez lever l'ambiguïté ailleurs en utilisant la base_objecttable, qui fonctionnera, mais je pense que la conception est trop compliquée.

FrustratedWithFormsDesigner
la source
Je ne suis pas en désaccord avec le fait que ce que vous proposez est une option viable, mais pourquoi le RI ne peut-il pas être appliqué avec la conception B de l'OP? (Je suppose que c'est ce que vous dites).
ypercubeᵀᴹ
@ypercube: Je pense que j'ai raté la BaseObjectstable lors de ma première lecture, et j'ai pensé que je voyais une description d'une table où object_idpeut pointer vers un ID dans n'importe quelle table.
FrustratedWithFormsDesigner
-1

D'après mon expérience, c'est ce que vous pouvez faire.

Reports
----------
Report_id (primary_key)
Report_name

Recommendations
----------------
Recommendation_id (primary key)
Recommendation_name
Report_id (foreign key)

Keywords
----------
Keyword_id (primary key)
Keyword

Et pour la relation entre les mots clés, les rapports et les recommandations, vous pouvez faire l'une des deux options suivantes: Option A:

Recommendation_keywords
------------------------
Recommendation_id(foreign_key)
keyword_id (foreign_key)

Cela permet une relation directe des rapports aux recommandations, aux mots-clés et enfin aux mots-clés. Option B:

object_keywords
---------------
Object_id
Object_type
Keyword_id(foreign_key)

L'option A est plus facile à appliquer et à gérer car elle aura les architectures de la base de données pour gérer l'intégrité des données et ne permettra pas l'insertion de données invalides.

L'option B nécessite cependant un peu plus de travail car vous devrez coder l'identification de la relation. Est plus flexible à long terme si, par hasard, à un moment donné dans le futur, vous devez ajouter des mots clés à un autre élément que le rapport ou la recommandation, il vous suffit d'ajouter l'identification et d'utiliser directement le tableau.

Erxgli
la source
Permettez-moi d'expliquer pourquoi j'ai rétrogradé: 1. Il n'est pas clair si vous êtes en faveur de l'option A, B ou d'une 3ème approche. Il me semble que vous dites que les deux sont plus ou moins OK (avec lesquels je suis en désaccord parce que A a plusieurs problèmes que d'autres ont décrits avec leurs réponses. 2. Suggérez-vous d'apporter des améliorations à la conception de A (ou B) "Ce n'est pas clair non plus. Il serait également bon de définir clairement les FK, ce n'est pas du tout évident ce que vous proposez. Au total, j'aime les réponses qui clarifient les choses et les options pour tout futur visiteur. Essayez de modifier votre réponse et Je vais inverser mon vote.
ypercubeᵀᴹ