Progettazione di database: oggetti diversi con tag condiviso


8

Il mio background è più nella programmazione web piuttosto che nell'amministrazione del database, quindi per favore correggimi se sto usando la terminologia sbagliata qui. Sto cercando di capire il modo migliore per progettare il database per un'applicazione che codificherò.

La situazione: ho rapporti in una tabella e raccomandazioni in un'altra tabella. Ogni rapporto può avere molti consigli. Ho anche una tabella separata per le parole chiave (per implementare la codifica). Tuttavia, desidero disporre di un solo set di parole chiave che viene applicato sia ai rapporti che ai consigli, in modo che la ricerca di parole chiave fornisca rapporti e consigli come risultati.

Ecco la struttura con cui ho iniziato:

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


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


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


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

Istintivamente, ritengo che questo non sia ottimale e che i miei oggetti taggable siano ereditati da un genitore comune e che tale genitore di commento sia taggato, il che darebbe la seguente struttura:

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)

Dovrei andare con questa seconda struttura? Mi sto perdendo qualche preoccupazione importante qui? Inoltre, se vado con il secondo, cosa dovrei usare come nome non generico per sostituire "Oggetto"?

Aggiornare:

Sto usando SQL Server per questo progetto. È un'applicazione interna con un numero limitato di utenti non concorrenti, quindi non prevedo un carico elevato. In termini di utilizzo, le parole chiave verranno probabilmente utilizzate con parsimonia. È praticamente solo a scopo di reportistica statistica. In tal senso, qualunque soluzione segua probabilmente influenzerà solo tutti gli sviluppatori che dovranno mantenere questo sistema in linea ... ma ho pensato che fosse buono implementare buone pratiche ogni volta che potevo. Grazie per tutte le informazioni!


Sembra che tu non abbia risposto alla domanda più importante - Come saranno accessibili i dati? - Per quali domande / dichiarazioni vuoi "mettere a punto" il tuo modello? - Come pensi di espandere la funzionalità? Penso che non ci siano buone pratiche generali: la soluzione dipende dalle risposte a queste domande. E inizia ad avere importanza anche nei modelli semplici come questo. Oppure potresti finire con un modello che segue alcuni principi più alti ma fa davvero schifo negli scenari più importanti - quelli visti dagli utenti del sistema.
Štefan Oravec,

Buon punto! Dovrò passare un po 'di tempo a pensarci!
matikin9

Risposte:


6

Il problema con il tuo primo esempio è la tabella tri-link. Richiederà che una delle chiavi esterne nel rapporto o nei consigli sia sempre NULL in modo che le parole chiave si colleghino solo in un modo o nell'altro?

Nel caso del tuo secondo esempio, l'unione dalla base alle tabelle derivate ora potrebbe richiedere l'uso del selettore del tipo o dei LEFT JOIN a seconda di come lo fai.

Detto questo, perché non renderlo esplicito ed eliminare tutti i NULL e LEIN JOIN?

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)

In questo scenario quando aggiungi qualcos'altro che deve essere taggato, aggiungi semplicemente la tabella delle entità e la tabella dei collegamenti.

Quindi i risultati della tua ricerca saranno così (vedi che c'è ancora una selezione di tipi in corso e li trasforma in generici a livello di risultati dell'oggetto se vuoi un singolo elenco di risultati):

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

Non importa cosa, da qualche parte ci sarà la selezione del tipo e una sorta di ramificazione in corso.

Se osservi come lo faresti nella tua opzione 1, è simile ma con un'istruzione CASE o LEFT JOINs e una COALESCE. Man mano che si espande l'opzione 2 con più elementi collegati, è necessario continuare ad aggiungere più JOIN SINISTRA in cui gli oggetti NON vengono generalmente trovati (un oggetto collegato può avere solo una tabella derivata valida).

Non credo che ci sia qualcosa di fondamentalmente sbagliato nella tua opzione 2, e potresti effettivamente farlo sembrare questa proposta con un uso di viste.

Nella tua opzione 1, ho qualche difficoltà a capire perché hai optato per la tabella tri-link.


La tabella tri-link che menzioni è stata probabilmente il risultato del mio essere mentalmente pigro ...: P Dopo aver letto le varie risposte, non penso che nessuna delle mie opzioni iniziali abbia senso. Avere tabelle ReportKeywords e Consigli separate KeyKey separate ha più senso pratico. Stavo prendendo in considerazione la scalabilità, in termini di potenzialmente avere più oggetti che necessitavano di parole chiave applicate, ma realisticamente probabilmente c'è solo un altro tipo di oggetto che potrebbe aver bisogno di parole chiave.
matikin9,

4

Innanzitutto, nota che la soluzione ideale dipende in una certa misura da quale RDBMS usi. Darò quindi sia la risposta standard che quella specifica di PostgreSQL.

Risposta standardizzata e normalizzata

La risposta standard è avere due tabelle di join.

Supponiamo di avere i nostri tavoli:

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

Questo approccio segue tutte le regole standard di normalizzazione e non viola i principi di normalizzazione del database tradizionale. Dovrebbe funzionare su qualsiasi RDBMS.

Risposta specifica per PostgreSQL, design N1NF

Innanzitutto, una parola sul perché PostgreSQL è diverso. PostgreSQL supporta una serie di modi molto utili per utilizzare gli indici su array, in particolare utilizzando quelli che sono noti come indici GIN. Questi possono essere utili per le prestazioni se usati qui correttamente. Poiché PostgreSQL può "raggiungere" i tipi di dati in questo modo, l'assunzione di base di atomicità e normalizzazione è alquanto problematica da applicare rigidamente qui. Quindi, per questo motivo, la mia raccomandazione sarebbe quella di infrangere la regola di atomicità della prima forma normale e fare affidamento sugli indici GIN per prestazioni migliori.

Una seconda nota qui è che mentre questo offre prestazioni migliori, aggiunge alcuni mal di testa perché avrai del lavoro manuale da fare per ottenere l'integrità referenziale per funzionare correttamente. Quindi il compromesso qui è la prestazione per il lavoro manuale.

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

Ora dobbiamo aggiungere i trigger per garantire che le parole chiave siano gestite correttamente.

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

In secondo luogo, dobbiamo decidere cosa fare quando viene rimossa una parola chiave. Allo stato attuale, una parola chiave rimossa dalla tabella delle parole chiave non passerà in cascata ai campi delle parole chiave. Forse questo è desiderabile e forse no. La cosa più semplice da fare è limitare sempre l'eliminazione e aspettarti che gestirai manualmente questo caso se si presenta (usa un grilletto per sicurezza qui). Un'altra opzione potrebbe essere quella di riscrivere il valore di ogni parola chiave in cui esiste la parola chiave per rimuoverla. Ancora una volta un grilletto sarebbe il modo di farlo.

Il grande vantaggio di questa soluzione è che puoi indicizzare ricerche molto veloci per parola chiave e puoi estrarre tutti i tag senza un join. Lo svantaggio è che rimuovere una parola chiave è una seccatura e non funzionerà bene nemmeno in una buona giornata. Questo può essere accettabile perché è un evento raro e potrebbe essere consegnato a un processo in background ma è un compromesso che vale la pena comprendere.

Criticare la tua prima soluzione

Il vero problema con la tua prima soluzione è che non hai nessuna chiave possibile su ObjectKeywords. Di conseguenza, si verifica un problema in cui non è possibile garantire che ogni parola chiave venga applicata a ciascun oggetto una sola volta.

La tua seconda soluzione è un po 'migliore. Se non ti piacciono le altre soluzioni offerte, suggerirei di seguirlo. Suggerirei comunque di sbarazzarsi di keyword_id e di unirmi al testo della parola chiave. Ciò elimina un join senza denormalizzare.


Sto usando MS SQL Server per questo progetto, ma grazie per le informazioni su PostgreSQL. Gli altri punti sollevati sull'eliminazione e sull'assicurarsi che le coppie oggetto-parola chiave si verifichino una sola volta. Anche se avessi le chiavi per ogni coppia oggetto-parola chiave, non dovrei comunque controllare prima di inserire? Per quanto riguarda avere un ID parola chiave separato ... Ho letto che per SQL Server, avere una stringa long ish potrebbe ridurre le prestazioni e probabilmente dovrò consentire agli utenti di inserire "frasi chiave" piuttosto che "parole chiave ".
matikin9,

0

Suggerirei due strutture separate:

report_keywords
---------------
  ID rapporto
  ID parola chiave

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

In questo modo non hai tutti i possibili ID entità nella stessa tabella (che non è molto scalabile e potrebbe essere fonte di confusione) e non hai una tabella con un "ID oggetto" generico che devi chiarire da qualche altra parte usando la base_objecttabella, che funzionerà, ma penso che complica eccessivamente il design.


Non sono in disaccordo sul fatto che ciò che stai suggerendo sia un'opzione praticabile, ma perché RI non può essere applicato con il progetto B dell'OP? (Suppongo che sia quello che stai dicendo).
ypercubeᵀᴹ

@ypercube: penso di aver perso la BaseObjectstabella nel mio primo read-through e ho pensato di vedere una descrizione per una tabella in cui object_idpuò puntare a un ID in qualsiasi tabella.
FrustratedWithFormsDesigner

-1

Nella mia esperienza questo è ciò che puoi fare.

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

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

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

E per la relazione tra parole chiave, rapporti e consigli puoi fare una delle due opzioni: Opzione A:

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

Ciò consente una relazione diretta dai rapporti alle raccomandazioni, alle parole chiave e infine alle parole chiave. Opzione B:

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

L'opzione A è la più facile da applicare e gestire poiché avrà i costrutti del database per gestire l'integrità dei dati e non consentirà l'inserimento di dati non validi.

L'opzione B, sebbene richieda un po 'più di lavoro poiché dovrai codificare l'identificazione della relazione. È più flessibile a lungo termine, se per caso in qualche momento in futuro è necessario aggiungere parole chiave a un altro elemento diverso dal rapporto o dalla raccomandazione, è sufficiente aggiungere l'identificazione e utilizzare direttamente la tabella.


Permettetemi di spiegare perché ho effettuato il downvoting: 1. Non è chiaro se si è a favore dell'opzione A, B o del 3o approccio. Sembra (per me) che tu dica che entrambi sono più o meno OK (con cui non sono d'accordo perché A ha diversi problemi che altri hanno delineato con le loro risposte. 2. Stai suggerendo di migliorare la progettazione di A (o B) "Non è nemmeno chiaro. Sarebbe anche bello avere gli FK definiti chiaramente, non è affatto ovvio quello che stai suggerendo. In totale mi piacciono le risposte che chiariscono le cose e le opzioni per qualsiasi visitatore futuro. Si prega di provare a modificare la risposta e Invertirò il mio voto.
ypercubeᵀᴹ
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.