Corrispondenza molti-a-molti SQL
-
09-06-2019 - |
Domanda
Sto implementando un sistema di tagging per un sito web.Sono presenti più tag per oggetto e più oggetti per tag.Ciò si ottiene mantenendo una tabella con due valori per record, uno per gli ID dell'oggetto e il tag.
Sto cercando di scrivere una query per trovare gli oggetti che corrispondono a un determinato insieme di tag.Supponiamo di avere i seguenti dati (nel formato [oggetto] -> [tag]*)
apple -> fruit red food
banana -> fruit yellow food
cheese -> yellow food
firetruck -> vehicle red
Se voglio abbinare (rosso), dovrei prendere la mela e il camion dei pompieri.Se voglio abbinare (frutta, cibo) dovrei prendere (mela, banana).
Come scrivo una query SQL per fare quello che voglio?
@Jeremy Ruten,
Grazie per la tua risposta.La notazione utilizzata è stata utilizzata per fornire alcuni dati di esempio: il mio database ha una tabella con 1 ID oggetto e 1 tag per record.
In secondo luogo, il mio problema è che devo ottenere tutti gli oggetti che corrispondono a tutti i tag.Sostituendo il tuo OR con un AND in questo modo:
SELECT object WHERE tag = 'fruit' AND tag = 'food';
Non produce risultati durante l'esecuzione.
Soluzione
Dato:
- tabella degli oggetti (ID chiave primaria)
- tabella objecttags (chiavi esterne objectId, tagid)
tabella dei tag (ID chiave primaria)
SELECT distinct o.* from object o join objecttags ot on o.Id = ot.objectid join tags t on ot.tagid = t.id where t.Name = 'fruit' or t.name = 'food';
Questo sembra al contrario, dal momento che vuoi e, ma il problema è che 2 tag non sono sulla stessa riga e quindi an e non produce nulla, poiché 1 singola riga non può essere sia un frutto che un alimento.Questa query produrrà solitamente duplicati, perché otterrai 1 riga di ciascun oggetto, per tag.
Se desideri davvero fare un e in questo caso, avrai bisogno di a group by
, e a having count = <number of ors>
nella tua query, ad esempio.
SELECT distinct o.name, count(*) as count
from object o join objecttags ot on o.Id = ot.objectid
join tags t on ot.tagid = t.id
where t.Name = 'fruit' or t.name = 'food'
group by o.name
having count = 2;
Altri suggerimenti
Oh Dio, potrei aver interpretato male il tuo commento originale.
Il modo più semplice per farlo in SQL sarebbe avere tre tabelle:
1) Tags ( tag_id, name )
2) Objects (whatever that is)
3) Object_Tag( tag_id, object_id )
Quindi puoi porre praticamente qualsiasi domanda tu voglia sui dati in modo rapido, semplice ed efficiente (a condizione di indicizzarli in modo appropriato).Se vuoi essere fantasioso, puoi anche consentire tag con più parole (c'è un modo elegante e un modo meno elegante, mi viene in mente).
Presumo che sia quello che hai, quindi questo SQL qui sotto funzionerà:
Il modo letterale:
SELECT obj
FROM object
WHERE EXISTS( SELECT *
FROM tags
WHERE tag = 'fruit'
AND oid = object_id )
AND EXISTS( SELECT *
FROM tags
WHERE tag = 'Apple'
AND oid = object_id )
Ci sono anche altri modi per farlo, come ad esempio:
SELECT oid
FROM tags
WHERE tag = 'Apple'
INTERSECT
SELECT oid
FROM tags
WHERE tag = 'Fruit'
@Kyle:La tua query dovrebbe essere più simile a:
SELECT object WHERE tag IN ('fruit', 'food');
La tua query cercava righe in cui il tag era sia frutta che cibo, il che è impossibile visto che il campo può avere un solo valore, non entrambi contemporaneamente.
Combina il suggerimento di Steve M. con quello di Jeremy otterrai un unico disco con quello che cerchi:
select object
from tblTags
where tag = @firstMatch
and (
@secondMatch is null
or
(object in (select object from tblTags where tag = @secondMatch)
)
Ora, questo non si adatta molto bene, ma otterrà ciò che stai cercando.Penso che ci sia un modo migliore per farlo in modo da poter facilmente avere N numero di elementi corrispondenti senza un grande impatto sul codice, ma al momento mi sfugge.
Ti consiglio il seguente schema.
Objects: objectID, objectName
Tags: tagID, tagName
ObjectTag: objectID,tagID
Con la seguente interrogazione.
select distinct
objectName
from
ObjectTab ot
join object o
on o.objectID = ot.objectID
join tabs t
on t.tagID = ot.tagID
where
tagName in ('red','fruit')
Suggerirei di fare in modo che la tua tabella abbia 1 tag per record, in questo modo:
apple -> fruit
apple -> red
apple -> food
banana -> fruit
banana -> yellow
banana -> food
Allora potresti semplicemente
SELECT object WHERE tag = 'fruit' OR tag = 'food';
Se vuoi davvero farlo a modo tuo, potresti farlo in questo modo:
SELECT object WHERE tag LIKE 'red' OR tag LIKE '% red' OR tag LIKE 'red %' OR tag LIKE '% red %';