Va mai bene usare gli elenchi in un database relazionale?


94

Ho cercato di progettare un database in linea con un concetto di progetto e mi sono imbattuto in quello che sembra un problema molto dibattuto. Ho letto alcuni articoli e alcune risposte Stack Overflow che affermano che non è mai (o quasi mai) accettabile memorizzare un elenco di ID o simili in un campo - tutti i dati dovrebbero essere relazionali, ecc.

Il problema in cui mi imbatto, tuttavia, è che sto cercando di creare un assegnatore di attività. Le persone creeranno attività, le assegneranno a più persone e queste verranno salvate nel database.

Naturalmente, se salvo queste attività singolarmente in "Persona", dovrò avere dozzine di colonne fittizie "TaskID" e gestirle in modo micro perché possono essere assegnate da 0 a 100 attività a una persona, per esempio.

Inoltre, se salvo le attività in una tabella "Attività", dovrò avere dozzine di colonne fittizie "PersonID" e gestirle in modo microscopico, lo stesso problema di prima.

Per un problema come questo, va bene salvare un elenco di ID che assumono una forma o l'altra o non sto pensando a un altro modo in cui ciò è possibile senza infrangere i principi?


22
Mi rendo conto che è etichettato "database relazionale" quindi mi limiterò a lasciare un commento, non una risposta, ma in altri tipi di basi di dati che lo fa senso per memorizzare elenchi. Mi viene in mente Cassandra poiché non ha unioni.
Captain Man,

12
Ottimo lavoro nella ricerca e poi nel chiedere qui! In effetti, la 'raccomandazione' di non violare mai la prima forma normale ha fatto davvero bene per te, perché dovresti davvero trovare un altro approccio relazionale, vale a dire una relazione "molti-a-molti", per la quale esiste un modello standard in banche dati relazionali che dovrebbero essere utilizzate.
JimmyB,

6
"Va sempre bene" sì .... qualunque cosa segua, la risposta è sì. Finché hai un motivo valido. C'è sempre un caso d'uso che ti obbliga a violare le migliori pratiche perché ha senso farlo. (Nel tuo caso, però, non dovresti assolutamente farlo)
xyious

3
Attualmente sto usando un array ( non una stringa delimitata - a VARCHAR ARRAY) per memorizzare un elenco di tag. Probabilmente non è così che finiranno per essere archiviati successivamente, ma gli elenchi possono essere estremamente utili durante le fasi di prototipazione, quando non hai nient'altro a cui puntare e non vuoi costruire l'intero schema del database prima che tu possa fare qualcos'altro.
Nic Hartley,

3
@Ben " (anche se non saranno indicizzabili) " - in Postgres, molte query su colonne JSON (e probabilmente XML, anche se non ho controllato) sono indicizzabili.
Nic Hartley,

Risposte:


249

La parola chiave e il concetto chiave che è necessario esaminare è la normalizzazione del database .

Quello che faresti, invece di aggiungere informazioni sulle assegnazioni alla persona o alle tabelle delle attività, è aggiungere una nuova tabella con le informazioni di tale assegnazione, con relazioni pertinenti.

Esempio, hai le seguenti tabelle:

persone:

+ ---- + ----------- +
| ID | Nome |
+ + ==== =========== +
| 1 | Alfred |
| 2 | Jebediah |
| 3 | Jacob |
| 4 | Ezechiele |
+ ---- + ----------- +

Compiti:

+ ---- + -------------------- +
| ID | Nome |
+ + ==== ==================== +
| 1 | Dai da mangiare ai polli |
| 2 | Aratro |
| 3 | Mucche da latte |
| 4 | Alza una stalla |
+ ---- + -------------------- +

Dovresti quindi creare una terza tabella con Assegnazioni. Questa tabella modellerebbe la relazione tra le persone e le attività:

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

Avremmo quindi un vincolo di chiave esterna, in modo tale che il database imponga che PersonId e TaskId debbano essere ID validi per quegli elementi esterni. Per la prima fila, possiamo vedere PersonId is 1, così Alfred , è assegnato a TaskId 3, Mucche da latte .

Quello che dovresti essere in grado di vedere qui è che potresti avere un numero di incarichi pari o inferiore a quello che desideri. In questo esempio, a Ezechiele non viene assegnato alcun compito e ad Alfred viene assegnato 2. Se hai un compito con 100 persone, ciò SELECT PersonId from Assignments WHERE TaskId=<whatever>;produrrà 100 righe, con una varietà di Persone diverse assegnate. È possibile WHEREsu PersonId per trovare tutte le attività assegnate a quella persona.

Se si desidera restituire query in sostituzione degli ID con i nomi e le attività, si arriva a imparare come ISCRIVERSI alle tabelle.


86
La parola chiave che vuoi cercare per saperne di più è "relazione molti-a-molti "
BlueRaja - Danny Pflughoeft

34
Per elaborare un po 'il commento di Thierrys: potresti pensare di non aver bisogno di normalizzare perché ho solo bisogno di X ed è molto semplice memorizzare l'elenco ID , ma per qualsiasi sistema che può essere esteso in seguito ti pentirai di non averlo normalizzato in precedenza. Normalizza sempre ; l'unica domanda è quale forma normale
Jan Doggen,

8
D'accordo con @Jan - a dispetto del mio miglior giudizio, ho permesso al mio team di prendere una scorciatoia di progettazione un po 'di tempo fa, memorizzando JSON invece per qualcosa che "non dovrà essere esteso". Che è durato come sei mesi FML. Il nostro aggiornamento ha quindi avuto una brutta lotta tra le mani per migrare il JSON nello schema che avremmo dovuto iniziare. Avrei davvero dovuto saperlo meglio.
Corse di leggerezza in orbita,

13
@Deduplicator: è solo una rappresentazione di una colonna chiave primaria intera di tipo giardino con incremento automatico. Roba abbastanza tipica.
whatsisname

8
@whatsisname Nella tabella Persone o Attività, sono d'accordo con te. Su una tabella bridge in cui l'unico scopo è rappresentare la relazione molti-a-molti tra altre due tabelle che dispongono già di chiavi surrogate? Non ne aggiungerei uno senza una buona ragione. È solo un sovraccarico poiché non verrà mai utilizzato in query o relazioni.
jpmc26,

35

Stai facendo due domande qui.

Innanzitutto, chiedi se è ok per memorizzare elenchi serializzati in una colonna. Sì, va bene. Se il tuo progetto lo richiede. Un esempio potrebbero essere gli ingredienti di un prodotto per una pagina del catalogo, in cui non si desidera provare a rintracciare ciascun ingrediente singolarmente.

Sfortunatamente la tua seconda domanda descrive uno scenario in cui dovresti optare per un approccio più relazionale. Avrai bisogno di 3 tavoli. Uno per le persone, uno per le attività e uno che mantiene l'elenco di quali attività sono assegnate a quali persone. L'ultima sarebbe verticale, una riga per persona / combinazione di attività, con colonne per la chiave primaria, ID attività e ID persona.


9
L'esempio di ingrediente a cui fai riferimento è corretto in superficie; ma sarebbe chiaro in quel caso. Non è un elenco nel senso della programmazione (a meno che tu non intenda che la stringa sia un elenco di caratteri che ovviamente non fai). OP che descrive i loro dati come "un elenco di ID" (o anche solo "un elenco di [..]") implica che a un certo punto gestiscono questi dati come singoli oggetti.
Flater,

10
@Flater: Ma è un elenco. Devi essere in grado di riformattarlo come (in vari modi) un elenco HTML, un elenco Markdown, un elenco JSON, ecc. Al fine di garantire che gli elementi vengano visualizzati correttamente in (variamente) una pagina Web, un documento di testo semplice, un dispositivo mobile app ... e non puoi farlo con un semplice testo.
Kevin,

12
@Kevin Se questo è il tuo obiettivo, allora è molto più facile e facile da raggiungere conservando gli ingredienti in una tabella! Per non parlare del fatto che, in seguito, le persone ... oh, non so, per esempio, desiderare sostituti raccomandati o qualcosa di stupido come cercare tutte le ricette senza arachidi, glutine o proteine ​​animali ...
Dan Bron,

10
@DanBron: YAGNI. Al momento stiamo usando solo un elenco perché semplifica la logica dell'interfaccia utente. Se abbiamo bisogno o avremo bisogno di un comportamento simile a un elenco nel livello di logica aziendale, allora dovrebbe essere normalizzato in una tabella separata. Le tabelle e i join non sono necessariamente costosi, ma non sono gratuiti e fanno domande sull'ordine degli elementi ("Ci teniamo all'ordine degli ingredienti?") E sull'ulteriore normalizzazione ("Girerai '3 uova' into ('eggs', 3)? Che dire di 'Salt, a piacere', è ('salt', NULL)? ").
Kevin,

7
@Kevin: YAGNI è abbastanza sbagliato qui. Tu stesso hai sostenuto la necessità di poter trasformare l'elenco in molti modi (HTML, markdown, JSON) e quindi stai sostenendo che hai bisogno dei singoli elementi dell'elenco . A meno che le applicazioni di archiviazione dei dati e di "gestione dell'elenco" non siano due applicazioni sviluppate in modo indipendente (e si noti che livelli di applicazione separati! = Applicazioni separate), la struttura del database dovrebbe sempre essere creata per archiviare i dati in un formato che li lasci prontamente disponibili - evitando logiche di analisi / conversione aggiuntive.
Flater,

22

Quello che stai descrivendo è noto come una relazione "molti a molti", nel tuo caso tra Persone Task. In genere viene implementato utilizzando una terza tabella, a volte chiamata tabella "link" o "riferimento incrociato". Per esempio:

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

2
È inoltre possibile aggiungere prima un indice task_id, se si stanno eseguendo query filtrate per attività.
jpmc26,

1
Conosciuto anche come tavolo da bridge. Inoltre, vorrei poterti dare un vantaggio in più per non avere una colonna identità, anche se consiglierei un indice su ogni colonna.
jmoreno,

13

... non è mai (o quasi mai) accettabile memorizzare un elenco di ID o simili in un campo

L'unica volta che si potrebbe memorizzare più di un elemento di dati in un singolo campo è quando quel campo è solo mai usato come una singola entità ed è mai considerato come essendo costituito da quegli elementi più piccoli. Un esempio potrebbe essere un'immagine, memorizzata in un campo BLOB. È composto da un sacco di elementi più piccoli (byte) ma questi non significano nulla per il database e possono essere usati tutti insieme (e sembrano belli per un Utente finale).

Poiché un "elenco" è, per definizione, composto da elementi più piccoli (elementi), questo non è il caso qui e dovresti normalizzare i dati.

... se salvo queste attività singolarmente in "Persona", dovrò avere dozzine di colonne fittizie "TaskID" ...

No. Avrai alcune righe in una tabella di intersezione (nota anche come entità debole) tra persona e attività. I database sono davvero bravi a lavorare con molte righe; in realtà sono piuttosto spazzatura nel lavorare con molte colonne [ripetute].

Bel esempio chiaro fornito da whatsisname.


4
Quando si creano sistemi di vita reale "non dire mai mai" è una regola molto buona da rispettare.
l0b0

1
In molti casi, il costo per elemento di mantenimento o recupero di un elenco in forma normalizzata può superare notevolmente il costo di conservazione degli articoli come BLOB, poiché ciascun elemento dell'elenco dovrebbe contenere l'identità dell'elemento principale con il quale esso è associato e la sua posizione all'interno dell'elenco oltre ai dati effettivi. Anche nei casi in cui il codice potrebbe trarre vantaggio dalla possibilità di aggiornare alcuni elementi dell'elenco senza aggiornare l'intero elenco, potrebbe essere più economico archiviare tutto come un BLOB e riscriverlo ogni volta che si deve riscrivere qualcosa.
supercat

4

Può essere legittimo in alcuni campi precalcolati.

Se alcune delle tue query sono costose e decidi di andare con i campi pre-calcolati aggiornati automaticamente utilizzando i trigger del database, potrebbe essere legittimo mantenere gli elenchi all'interno di una colonna.

Ad esempio, nell'interfaccia utente si desidera mostrare questo elenco utilizzando la vista griglia, in cui ogni riga può aprire tutti i dettagli (con elenchi completi) dopo aver fatto doppio clic:

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

Stai mantenendo la seconda colonna aggiornata dal trigger quando il cliente visita un nuovo articolo o per attività pianificata.

È possibile rendere tale campo disponibile anche per la ricerca (come testo normale).

In tali casi, la conservazione delle liste è legittima. Devi solo considerare il caso in cui è possibile superare la lunghezza massima del campo.


Inoltre, se si utilizza Microsoft Access, i campi multivalore offerti sono un altro caso d'uso speciale. Gestiscono automaticamente le tue liste in un campo.

Ma puoi sempre ricorrere al modulo normalizzato standard mostrato in altre risposte.


Riepilogo: le forme normali di database sono un modello teorico necessario per comprendere aspetti importanti della modellazione dei dati. Ma ovviamente la normalizzazione non tiene conto delle prestazioni o di altri costi per il recupero dei dati. È al di fuori di questo modello teorico. Tuttavia, l'implementazione pratica richiede spesso la memorizzazione di elenchi o altri duplicati precalcolati (e controllati).

Alla luce di quanto sopra, nell'implementazione pratica, preferiremmo una query basata su una forma normale perfetta e in esecuzione 20 secondi o una query equivalente basata su valori precalcolati che impiegano 0,08 s? A nessuno piace che il loro prodotto software sia accusato di lentezza.


1
Può essere legittimo anche senza roba precalcolata. L'ho fatto un paio di volte in cui i dati sono archiviati correttamente, ma per motivi di prestazioni è utile inserire alcuni risultati memorizzati nella cache nei record principali.
Loren Pechtel,

@LorenPechtel - Sì, grazie, nel mio uso del termine pre-calcolato includo anche i casi di valori memorizzati nella cache memorizzati dove necessario. Nei sistemi con dipendenze complesse, sono il modo per mantenere le prestazioni normali. E se programmati con un adeguato know-how, questi valori sono affidabili e sempre sincronizzati. Non volevo aggiungere il caso di memorizzazione nella cache nella risposta per mantenere la risposta semplice e sicura. È stato comunque retrocesso. :)
miroxlav,

@LorenPechtel In realtà, sarebbe comunque una cattiva ragione ... i dati della cache dovrebbero essere conservati in un archivio di cache intermedio, e mentre la cache è ancora valida, quella query non dovrebbe mai colpire il DB principale.
Tezra,

1
@Tezra No, sto dicendo che a volte un pezzo di dati da una tabella secondaria è necessario abbastanza spesso da avere senso mettere una copia nel record principale. (Esempio che ho fatto - la tabella dei dipendenti include l'ultima volta in entrata e l'ultima in pausa. Sono utilizzate solo a scopo di visualizzazione, qualsiasi calcolo effettivo proviene dalla tabella con i record di accesso / uscita.)
Loren Pechtel

0

Dato due tavoli; li chiameremo Person and Task, ognuno con il proprio ID (PersonID, TaskID) ... l'idea di base è quella di creare una terza tabella per collegarli insieme. Chiameremo questo tavolo PersonToTask. Come minimo dovrebbe avere il proprio ID, così come gli altri due Quindi, quando si tratta di assegnare qualcuno a un'attività; non dovrai più AGGIORNARE la tabella Person, devi solo INSERIRE una nuova riga nella PersonToTaskTable. E la manutenzione diventa più semplice: la necessità di eliminare un'attività diventa solo una ELIMINA basata su TaskID, non è più necessario aggiornare la tabella Person e l'analisi associata

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

Che ne dici di un semplice report o di chi è stato assegnato a un'attività?

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

Ovviamente potresti fare molto di più; un TimeReport potrebbe essere eseguito se si aggiungono i campi DateTime per TaskAssigned e TaskCompleted. Dipende tutto da te


0

Può funzionare se si dice che si dispone di chiavi primarie leggibili dall'uomo e si desidera un elenco di attività # senza dover affrontare la natura verticale di una struttura di tabella. cioè molto più facile da leggere prima tabella.

------------------------  
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 domanda sarebbe quindi: se l'elenco delle attività fosse archiviato o generato su richiesta, il che dipenderebbe in gran parte da requisiti quali: quanto spesso è necessario l'elenco, quanto precisa esiste quante righe di dati, come verranno utilizzati i dati, ecc. .. dopodiché dovrebbe essere effettuata l'analisi dei compromessi per l'esperienza dell'utente e il rispetto dei requisiti.

Ad esempio, confrontando il tempo necessario per richiamare le 2 righe rispetto all'esecuzione di una query che genererebbe le 2 righe. Se impiega molto tempo e l'utente non ha bisogno dell'elenco più aggiornato (* si aspetta meno di 1 cambio al giorno), potrebbe essere memorizzato.

Oppure, se l'utente ha bisogno di un registro storico delle attività assegnate, avrebbe anche senso se l'elenco fosse archiviato. Quindi dipende davvero da quello che stai facendo, non dire mai mai.


Come dici tu, tutto dipende da come i dati devono essere recuperati. Se / solo / hai mai interrogato questa tabella per Nome Utente, il campo "elenco" è perfettamente adeguato. Tuttavia, come è possibile eseguire una query su una tabella di questo tipo per scoprire chi sta lavorando sull'attività # 1234567 e mantenerla ancora performante? Quasi ogni tipo di funzione di stringa "trova-X-ovunque-nel-campo" causerà tale query a / Scansione tabella /, rallentando le cose a una ricerca per indicizzazione. Con dati adeguatamente normalizzati e adeguatamente indicizzati, ciò non accade.
Phill W.,

0

Stai prendendo quello che dovrebbe essere un altro tavolo, ruotandolo di 90 gradi e scivolandolo su un altro tavolo.

È come avere una tabella degli ordini in cui hai ItemProdcode1, itemQuantity1, itemPrice1 ... itemProdcode37, itemQuantity37, itemPrice37. Oltre ad essere scomodo da gestire a livello di programmazione, puoi garantire che domani qualcuno vorrà ordinare 38 cose.

Farei a modo tuo solo se l '"elenco" non è in realtà un elenco, ovvero dove si trova nel suo insieme e ogni singolo elemento pubblicitario non si riferisce a qualche entità chiara e indipendente. In tal caso basta inserire tutto in un tipo di dati sufficientemente grande.

Quindi un ordine è un elenco, un elenco di materiali è un elenco (o un elenco di elenchi, che sarebbe ancora più un incubo per implementare "lateralmente"). Ma una nota / commento e una poesia non lo sono.


0

Se "non va bene", è abbastanza brutto che ogni sito Wordpress abbia mai un elenco in wp_usermeta con wp_capabilities in una riga, un elenco digeded_wp_pointers in una riga e altri ...

In effetti in casi come questo potrebbe essere meglio per la velocità in quanto quasi sempre si desidera l'elenco . Ma Wordpress non è noto per essere l'esempio perfetto delle migliori pratiche.

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.