Datenbankdesign - verschiedene Objekte mit gemeinsam genutzten Tags

8

Mein Hintergrund liegt eher in der Webprogrammierung als in der Datenbankverwaltung. Bitte korrigieren Sie mich, wenn ich hier die falsche Terminologie verwende. Ich versuche herauszufinden, wie ich die Datenbank am besten für eine Anwendung entwerfen kann, die ich codieren werde.

Die Situation: Ich habe Berichte in einer Tabelle und Empfehlungen in einer anderen Tabelle. Jeder Bericht kann viele Empfehlungen enthalten. Ich habe auch eine separate Tabelle für Schlüsselwörter (um das Tagging zu implementieren). Ich möchte jedoch nur einen Satz von Schlüsselwörtern haben, der sowohl auf Berichte als auch auf Empfehlungen angewendet wird, sodass Sie bei der Suche nach Schlüsselwörtern Berichte und Empfehlungen als Ergebnisse erhalten.

Hier ist die Struktur, mit der ich angefangen habe:

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


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


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


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

Instinktiv bin ich der Meinung, dass dies nicht optimal ist und dass ich meine taggbaren Objekte von einem gemeinsamen übergeordneten Objekt erben und diesen übergeordneten Kommentar mit Tags versehen lassen sollte, was die folgende Struktur ergeben würde:

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)

Soll ich mit dieser zweiten Struktur gehen? Vermisse ich hier wichtige Bedenken? Was sollte ich als nicht generischen Namen verwenden, um "Objekt" zu ersetzen, wenn ich mich für den zweiten Namen entscheide?

Aktualisieren:

Ich verwende SQL Server für dieses Projekt. Es ist eine interne Anwendung mit einer kleinen Anzahl nicht gleichzeitiger Benutzer, daher erwarte ich keine hohe Auslastung. In Bezug auf die Verwendung werden die Schlüsselwörter wahrscheinlich sparsam verwendet. Es ist so ziemlich nur für statistische Berichtszwecke. In diesem Sinne wirkt sich jede Lösung wahrscheinlich nur auf Entwickler aus, die dieses System später warten müssen ... aber ich dachte, es ist gut, bewährte Verfahren zu implementieren, wann immer ich kann. Danke für all den Einblick!

matikin9
quelle
Anscheinend haben Sie nicht die wichtigste Frage beantwortet - Wie wird auf Daten zugegriffen? - Für welche Abfragen / Anweisungen möchten Sie Ihr Modell "optimieren"? - Wie wollen Sie die Funktionalität erweitern? Ich denke, dass es keine allgemeinen Best Practices gibt - die Lösung hängt von der Beantwortung dieser Fragen ab. Und selbst bei einfachen Modellen wie diesem spielt es eine Rolle. Oder Sie erhalten ein Modell, das einigen höheren Prinzipien folgt, aber die wichtigsten Szenarien - die von Benutzern des Systems gesehen werden - wirklich in den Griff bekommt.
Štefan Oravec
Guter Punkt! Ich muss einige Zeit damit verbringen, darüber nachzudenken!
Matikin9

Antworten:

6

Das Problem mit Ihrem ersten Beispiel ist die Tri-Link-Tabelle. Ist es erforderlich, dass einer der Fremdschlüssel in Berichten oder Empfehlungen immer NULL ist, damit Schlüsselwörter nur auf die eine oder andere Weise verknüpft werden?

Im Fall Ihres zweiten Beispiels erfordert die Verknüpfung von der Basis zu den abgeleiteten Tabellen möglicherweise die Verwendung des Typselektors oder der LEFT JOINs, je nachdem, wie Sie dies tun.

Warum nicht einfach explizit machen und alle NULL- und LEFT-JOINs eliminieren?

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)

Wenn Sie in diesem Szenario etwas anderes hinzufügen, das markiert werden muss, fügen Sie einfach die Entitätstabelle und die Verknüpfungstabelle hinzu.

Dann sehen Ihre Suchergebnisse folgendermaßen aus (siehe, es wird noch eine Typauswahl durchgeführt und sie werden auf der Ebene der Objektergebnisse in Generika umgewandelt, wenn Sie eine einzelne Ergebnisliste wünschen):

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 + '%'

Egal was passiert, irgendwo wird es eine Typauswahl und eine Art Verzweigung geben.

Wenn Sie sich ansehen, wie Sie dies in Ihrer Option 1 tun würden, ist es ähnlich, aber entweder mit einer CASE-Anweisung oder LEFT JOINs und einer COALESCE. Wenn Sie Ihre Option 2 erweitern, indem mehr Dinge verknüpft werden, müssen Sie immer mehr LEFT JOINs hinzufügen, bei denen Dinge normalerweise NICHT gefunden werden (ein verknüpftes Objekt kann nur eine abgeleitete Tabelle haben, die gültig ist).

Ich glaube nicht, dass irgendetwas an Ihrer Option 2 grundlegend falsch ist, und Sie könnten es tatsächlich so aussehen lassen, als ob dieser Vorschlag unter Verwendung von Ansichten.

Bei Ihrer Option 1 habe ich einige Schwierigkeiten zu verstehen, warum Sie sich für die Tri-Link-Tabelle entschieden haben.

Cade Roux
quelle
Die von Ihnen erwähnte Tri-Link-Tabelle war wahrscheinlich darauf zurückzuführen, dass ich geistig faul war ...: P Nachdem ich die verschiedenen Antworten gelesen habe, denke ich, dass keine meiner anfänglichen Optionen sinnvoll ist. Es ist praktischer, separate separate ReportKeywords- und RecommendationKeywords-Tabellen zu haben. Ich habe über Skalierbarkeit nachgedacht, um möglicherweise mehr Objekte zu verwenden, für die Schlüsselwörter erforderlich sind, aber realistisch gesehen gibt es wahrscheinlich nur noch einen Objekttyp, für den Schlüsselwörter erforderlich sein könnten.
Matikin9
4

Beachten Sie zunächst, dass die ideale Lösung in gewissem Maße davon abhängt, welches RDBMS Sie verwenden. Ich werde dann sowohl die Standard- als auch die PostgreSQL-spezifische Antwort geben.

Normalisierte Standardantwort

Die Standardantwort besteht darin, zwei Verknüpfungstabellen zu haben.

Angenommen, wir haben unsere Tabellen:

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

Dieser Ansatz folgt allen Standardregeln für die Normalisierung und verstößt nicht gegen die traditionellen Prinzipien der Datenbanknormalisierung. Es sollte auf jedem RDBMS funktionieren.

PostgreSQL-spezifische Antwort, N1NF-Design

Zunächst ein Wort darüber, warum PostgreSQL anders ist. PostgreSQL unterstützt eine Reihe sehr nützlicher Methoden zur Verwendung von Indizes über Arrays, insbesondere unter Verwendung sogenannter GIN-Indizes. Diese können die Leistung erheblich verbessern, wenn sie hier richtig eingesetzt werden. Da PostgreSQL auf diese Weise in Datentypen "greifen" kann, ist die Grundannahme von Atomizität und Normalisierung etwas problematisch, um sie hier starr anzuwenden. Aus diesem Grund würde ich empfehlen, die Atomizitätsregel der ersten Normalform zu brechen und sich für eine bessere Leistung auf GIN-Indizes zu verlassen.

Ein zweiter Hinweis hier ist, dass dies zwar eine bessere Leistung bietet, jedoch einige Kopfschmerzen verursacht, da Sie einige manuelle Arbeiten ausführen müssen, damit die referenzielle Integrität richtig funktioniert. Der Kompromiss hier ist also die Leistung für manuelle Arbeit.

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[]  
);

Jetzt müssen wir Trigger hinzufügen, um sicherzustellen, dass die Schlüsselwörter ordnungsgemäß verwaltet werden.

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();

Zweitens müssen wir entscheiden, was zu tun ist, wenn ein Schlüsselwort entfernt wird. Derzeit wird ein aus der Schlüsselworttabelle entferntes Schlüsselwort nicht in die Schlüsselwortfelder kaskadiert. Vielleicht ist das wünschenswert und vielleicht auch nicht. Am einfachsten ist es, das Löschen immer einzuschränken und zu erwarten, dass Sie diesen Fall manuell behandeln, wenn er auftritt (verwenden Sie hier aus Sicherheitsgründen einen Auslöser). Eine andere Möglichkeit besteht darin, jeden Schlüsselwortwert, in dem das Schlüsselwort vorhanden ist, neu zu schreiben, um ihn zu entfernen. Auch hier wäre ein Auslöser der Weg, dies zu tun.

Der große Vorteil dieser Lösung besteht darin, dass Sie für sehr schnelle Suchvorgänge nach Schlüsselwörtern indizieren und alle Tags ohne Join abrufen können. Der Nachteil ist, dass das Entfernen eines Keywords schmerzhaft ist und selbst an einem guten Tag keine gute Leistung erbringt. Dies kann akzeptabel sein, da es sich um ein seltenes Ereignis handelt, das einem Hintergrundprozess unterzogen werden könnte, aber es ist ein Kompromiss, der es wert ist, verstanden zu werden.

Kritisieren Sie Ihre erste Lösung

Das eigentliche Problem bei Ihrer ersten Lösung ist, dass Sie keinen möglichen Schlüssel für ObjectKeywords haben. Folglich haben Sie ein Problem, bei dem Sie nicht garantieren können, dass jedes Schlüsselwort nur einmal auf jedes Objekt angewendet wird.

Ihre zweite Lösung ist etwas besser. Wenn Ihnen die anderen angebotenen Lösungen nicht gefallen, würde ich vorschlagen, sie zu verwenden. Ich würde jedoch vorschlagen, keyword_id loszuwerden und sich einfach dem Keyword-Text anzuschließen. Dadurch entfällt eine Verknüpfung ohne Denormalisierung.

Chris Travers
quelle
Ich verwende MS SQL Server für dieses Projekt, aber danke für die Informationen zu PostgreSQL. Die anderen Punkte, die Sie zum Löschen und Sicherstellen von Objekt-Schlüsselwort-Paaren angesprochen haben, kommen jeweils nur einmal vor. Selbst wenn ich Schlüssel für jedes Objekt-Schlüsselwort-Paar hätte, müsste ich das nicht vor dem Einfügen überprüfen? Was eine separate Schlüsselwort-ID betrifft ... Ich habe gelesen, dass eine lange Zeichenfolge für SQL Server die Leistung beeinträchtigen kann, und ich muss Benutzern wahrscheinlich erlauben, "Schlüsselphrasen" anstatt nur "Schlüsselwörter" einzugeben ".
Matikin9
0

Ich würde zwei getrennte Strukturen vorschlagen:

report_keywords
---------------
  Berichts-ID
  Schlüsselwort-ID

Empfehlung_ Schlüsselwörter
-----------------------
  Empfehlung_ID
  keyword_id

Auf diese Weise haben Sie nicht alle möglichen Entitäts-IDs in derselben Tabelle (was nicht sehr skalierbar ist und verwirrend sein kann), und Sie haben keine Tabelle mit einer generischen "Objekt-ID", die Sie an anderer Stelle eindeutig unterscheiden müssen Verwenden der base_objectTabelle, die funktionieren wird, aber ich denke, das Design wird zu kompliziert.

FrustratedWithFormsDesigner
quelle
Ich bin nicht anderer Meinung, dass das, was Sie vorschlagen, eine praktikable Option ist, aber warum kann RI nicht mit OPs Design B durchgesetzt werden? (Ich nehme an, das sagen Sie).
Ypercubeᵀᴹ
@ypercube: Ich glaube, ich habe die BaseObjectsTabelle beim ersten Durchlesen verpasst und dachte, ich würde eine Beschreibung für eine Tabelle sehen, object_iddie auf eine ID in einer beliebigen Tabelle verweisen kann .
FrustratedWithFormsDesigner
-1

Nach meiner Erfahrung können Sie dies tun.

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

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

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

Für die Beziehung zwischen Schlüsselwörtern, Berichten und Empfehlungen haben Sie zwei Möglichkeiten: Option A:

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

Dies ermöglicht eine direkte Beziehung von Berichten zu Empfehlungen, zu Schlüsselwörtern und schließlich zu Schlüsselwörtern. Option B:

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

Die Option A ist einfacher anzuwenden und zu verwalten, da sie über die Einschränkungen der Datenbank verfügt, um die Datenintegrität zu gewährleisten, und das Einfügen ungültiger Daten nicht zulässt.

Die Option B erfordert zwar etwas mehr Arbeit, da Sie die Identifikation der Beziehung codieren müssen. Ist auf lange Sicht flexibler, wenn Sie zu einem späteren Zeitpunkt zufällig Schlüsselwörter zu einem anderen Element als dem Bericht oder der Empfehlung hinzufügen müssen, müssen Sie nur die Identifikation hinzufügen und die Tabelle direkt verwenden.

Erxgli
quelle
Lassen Sie mich erklären, warum ich abgelehnt habe: 1. Es ist nicht klar, ob Sie für Option A, B oder einen dritten Ansatz sind. Es scheint (für mich), dass Sie sagen, dass beide mehr oder weniger in Ordnung sind (was ich nicht zustimme, weil A mehrere Probleme hat, die andere mit ihren Antworten umrissen haben. 2. Schlagen Sie vor, das Design von A (oder B) zu verbessern? Es ist auch nicht klar. Es wäre auch gut, wenn die FKs klar definiert wären. Es ist überhaupt nicht offensichtlich, was Sie vorschlagen. Insgesamt mag ich Antworten, die Dinge und Optionen für jeden zukünftigen Besucher klarstellen. Bitte versuchen Sie, Ihre Antwort zu bearbeiten und Ich werde meine Stimme umkehren.
ypercubeᵀᴹ