2011-10-03 3 views
4

que je cherchais sur le net et demander aux gens à titre indicatif, mais personne ne semble connaître la bonne solution (relativement rapide) au problème:MySQL many-to-many complément mis

J'ai trois tables, classique many-to-many solution:

  • entries: id (int), titre (varchar [255]), le contenu (texte)
  • tags: id (int), nom (varchar [255]) , slug (varchar [255])
  • entries_tags: id (int), entry_id (int), tag_id (int)

Rien d'extraordinaire à ce jour. Maintenant, nous allons dire que j'ai des données de test dans les balises (je garde des limaces car ils ne sont pas importants):

ID | name 
1. | one 
2. | two 
3. | three 
4. | four 
5. | five 

J'ai aussi trois entrées:

ID | title 
1. | Something 
2. | Blah blah blah 
3. | Yay! 

et relations:

ID | entry_id | tag_id 
1. | 1  | 1 
2. | 1  | 2 
3. | 2  | 1 
4. | 2  | 3 
5. | 3  | 1 
6. | 3  | 2 
7. | 3  | 3 
8. | 4  | 1 
9. | 4  | 4 

OK, nous avons nos données de test. Je veux savoir comment obtenir toutes les entrées qui ont le tag One, mais n'a pas tag Three (ce serait les entrées 1 et 4).

Je sais comment faire avec sous-requête, le problème est, il faut beaucoup de temps (avec 100k entrées, il a fallu environ 10-15 secondes). Y a-t-il un moyen de le faire avec JOINs? Ou est-ce que je manque quelque chose? Je suppose que j'aurais dû mentionner que j'ai besoin d'une solution qui fonctionne avec des ensembles de données plutôt que des étiquettes simples, donc remplacez 'Un' dans ma question par 'Un', 'Deux' et 'Deux' avec 'Trois', 'Quatre'

edit2 La réponse fournie est correcte, mais elle est trop lente pour être utilisée pratiquement. Je suppose que la seule façon de le faire fonctionner est d'utiliser un moteur de recherche tiers comme Lucene ou ElasticSearch.

Répondre

3

Le script suivant sélectionne les entrées qui ont des étiquettes One et Two et n'ont pas de balises Three et Four:

SELECT DISTINCT 
    et.entry_id 
FROM entries_tags et 
    INNER JOIN tags t1 ON et.tag_id = t1.id AND t1.name IN ('One', 'Two') 
    LEFT JOIN tags t2 ON et.tag_id = t2.id AND t2.name IN ('Three', 'Four') 
WHERE t2.id IS NULL 

Autre solution: le INNER JOIN est remplacé par WHERE EXISTS, ce qui nous permet de se débarrasser du (plutôt cher) DISTINCT:

SELECT 
    et.entry_id 
FROM entries_tags et 
    LEFT JOIN tags t2 ON et.tag_id = t2.id AND t2.name IN ('Three', 'Four') 
WHERE t2.id IS NULL 
    AND EXISTS (
    SELECT * 
    FROM tags t1 
    WHERE t1.id = et.tag_id 
     AND t1.name IN ('One', 'Two') 
) 
+0

Cela l'a fait dans 1/3rd de la fois précédente (~ 5 secondes) et je suppose que c'est le plus que vous pouvez obtenir sans mettre en cache le résultat et faire une sorte de tours de magie vaudou. Merci beaucoup! – d4rky

+0

Toujours le bienvenu! En fait, il y a une autre idée, et j'ai déjà mis à jour ma réponse avec sa mise en œuvre. Pourriez-vous essayer? –

1

Cela devrait faire ce que vous voulez.

(Il peut ou ne peut pas être plus rapide que la solution sous-requête, je vous suggère de comparer les plans de requête)

SELECT DISTINCT e.* 
FROM tags t1 
INNER JOIN entries_tags et1 ON t1.id=et1.tag_id 
INNER JOIN entries e ON e.entry_id=et1.entry_id 
INNER JOIN tags t2 on t2.name='three' 
INNER JOIN tags t3 on t3.name='four' 
LEFT JOIN entries_tags et2 ON (et1.entryid=et2.entryid AND t2.id = et2.tag_id) 
     OR (et1.entryid=et2.entryid AND t3.id = et2.tag_id) 
WHERE t1.name IN ('one','two') AND et2.name is NULL 

Par GAUCHE Rejoindre la table entries_tags et2 (les données que vous ne voulez pas), vous peut alors seulement sélectionner les enregistrements où et2.name IS NULL (où l'enregistrement et2 n'existe pas).

+0

Je ne suis pas sûr de ce que vous avez essayé de faire ici, mais cette requête est complètement cassée. Ou peut-être que je corrige mal (votre table et nom de domaine semble un peu aléatoire) – d4rky

+0

J'ai modifié ma question un peu, gardez cela à l'esprit :) – d4rky

+0

ok J'ai mis à jour la réponse pour vous –

0

Vous avez mentionné l'essai d'une sous-requête. Est-ce que c'est ce que tu as essayé?

SELECT entries.id, entries.content 
FROM entries 
    LEFT JOIN entries_tags ON entries.id=entries_tags.entries_id 
    LEFT JOIN tags ON entries_tags.tag_id=tags.id 
WHERE tag.id=XX 
    and entries.id NOT IN (
    SELECT entries.id 
    FROM entries 
     LEFT JOIN entries_tags ON entries.id=entries_tags.entries_id 
     LEFT JOIN tags ON entries_tags.tag_id=tags.id 
    WHERE tag.id=YY 
) 

(où XX est la balise que vous ne voulez et YY est la balise que vous ne voulez pas)

Avec des indices sur les champs d'identification, qui ne devrait pas être aussi lent que vous le dites. Cela dépendra de l'ensemble de données, mais il devrait être bon avec les indices (et avec les comparaisons de chaînes omises).

+0

Après les changements (était cassé en quelques endroits): 'SELECT count (entrées.id) FROM entrées LEFT JOIN entrées_tags ON entrées.id = entries_tags.entry_id Étiquettes LEFT JOIN ON entries_tags.tag_id = tags.id O tag tags.id IN (1,2) et entrées .id NOT IN (SELECT entries.id FROM entrées entrées LEFT JOIN entrées_tags ON entrées.id = entries_tags.entry_id Étiquettes LEFT JOIN ON entries_tags.tag_id = tags.id O tag tags.id IN (3,4)); '. J'ai pris 13 secondes sur ma base de données de test. – d4rky

+0

Avez-vous des index sur les ID? Vous pouvez vérifier avec SHOW CREATE TABLE [Table]. En outre, combien d'articles ont des étiquettes 3 ou 4? –

+0

[Table dump de structure] (http://pastebin.com/B9L680wb). Et nous parlons de 1 mln d'entrées et de 1,172 mln de entries_tags relations. – d4rky