2009-03-01 18 views
1

J'ai la structure de données et les données suivantes:Comment effectuer un AND avec une jointure?

CREATE TABLE `parent` (
    `id` int(11) NOT NULL auto_increment, 
    `name` varchar(10) NOT NULL, 
    PRIMARY KEY (`id`) 
) ENGINE=MyISAM DEFAULT CHARSET=latin1; 

INSERT INTO `parent` VALUES(1, 'parent 1'); 
INSERT INTO `parent` VALUES(2, 'parent 2'); 

CREATE TABLE `other` (
    `id` int(11) NOT NULL auto_increment, 
    `name` varchar(10) NOT NULL, 
    PRIMARY KEY (`id`) 
) ENGINE=MyISAM DEFAULT CHARSET=latin1; 

INSERT INTO `other` VALUES(1, 'other 1'); 
INSERT INTO `other` VALUES(2, 'other 2'); 

CREATE TABLE `relationship` (
    `id` int(11) NOT NULL auto_increment, 
    `parent_id` int(11) NOT NULL, 
    `other_id` int(11) NOT NULL, 
    PRIMARY KEY (`id`) 
) ENGINE=MyISAM DEFAULT CHARSET=latin1; 

INSERT INTO `relationship` VALUES(1, 1, 1); 
INSERT INTO `relationship` VALUES(2, 1, 2); 
INSERT INTO `relationship` VALUES(3, 2, 1); 

Je veux trouver les dossiers de parents avec les deux autres de 1 & 2.

Voici ce que j'ai pensé, mais je suis se demandant s'il y a un meilleur moyen:

SELECT p.id, p.name 
FROM parent AS p 
    LEFT JOIN relationship AS r1 ON (r1.parent_id = p.id) 
    LEFT JOIN relationship AS r2 ON (r2.parent_id = p.id) 
WHERE r1.other_id = 1 AND r2.other_id = 2; 

Le résultat est 1, "parent 1" qui est correct. Le problème est qu'une fois que vous obtenez une liste de 5 + jointures, il devient désordonné et que la table de la relation se développe, il devient lent.

Y a-t-il un meilleur moyen? J'utilise MySQL et PHP, mais c'est probablement assez générique. J'utilise MySQL et PHP.

Répondre

4

Ok, j'ai testé ça. Les requêtes du meilleur au pire étaient les suivants:

Requête 1: Relie (0.016s, essentiellement instant)

SELECT p.id, name 
FROM parent p 
JOIN relationship r1 ON p.id = r1.parent_id AND r1.other_id = 100 
JOIN relationship r2 ON p.id = r2.parent_id AND r2.other_id = 101 
JOIN relationship r3 ON p.id = r3.parent_id AND r3.other_id = 102 
JOIN relationship r4 ON p.id = r4.parent_id AND r4.other_id = 103 

Requête 2: EXISTE (0.625s)

SELECT id, name 
FROM parent p 
WHERE EXISTS (SELECT 1 FROM relationship WHERE parent_id = p.id AND other_id = 100) 
AND EXISTS (SELECT 1 FROM relationship WHERE parent_id = p.id AND other_id = 101) 
AND EXISTS (SELECT 1 FROM relationship WHERE parent_id = p.id AND other_id = 102) 
AND EXISTS (SELECT 1 FROM relationship WHERE parent_id = p.id AND oth 

Requête 3: Agrégat (1.016s)

SELECT p.id, p.name p à partir de parent WHERE (COUNT SELECT (*) à partir de relation où parent_id = p.id ET other_id IN (100.101.102.103))

Query 4: UNION Aggregate (2.39s)

SELECT id, name FROM (
    SELECT p1.id, p1.name 
    FROM parent AS p1 LEFT JOIN relationship as r1 ON(r1.parent_id=p1.id) 
    WHERE r1.other_id = 100 
    UNION ALL 
    SELECT p2.id, p2.name 
    FROM parent AS p2 LEFT JOIN relationship as r2 ON(r2.parent_id=p2.id) 
    WHERE r2.other_id = 101 
    UNION ALL 
    SELECT p3.id, p3.name 
    FROM parent AS p3 LEFT JOIN relationship as r3 ON(r3.parent_id=p3.id) 
    WHERE r3.other_id = 102 
    UNION ALL 
    SELECT p4.id, p4.name 
    FROM parent AS p4 LEFT JOIN relationship as r4 ON(r4.parent_id=p4.id) 
    WHERE r4.other_id = 103 
) a 
GROUP BY id, name 
HAVING count(*) = 4 

en fait, ce qui précède produisait les mauvaises données de sorte qu'il est soit mal ou je l'ai fait quelque chose de mal avec elle. Quoi qu'il en soit, ce qui précède est juste une mauvaise idée.

Si ce n'est pas rapide, vous devez examiner le plan d'explication de la requête. Vous n'avez probablement pas d'indices appropriés. Essayez avec:

CREATE INDEX ON relationship (parent_id, other_id) 

Avant d'aller dans la voie d'agrégation (SELECT COUNT (*) FROM ...) vous devriez lire SQL Statement - “Join” Vs “Group By and Having”.

Note: Les horaires ci-dessus sont basées sur:

CREATE TABLE parent (
    id INT PRIMARY KEY, 
    name VARCHAR(50) 
); 

CREATE TABLE other (
    id INT PRIMARY KEY, 
    name VARCHAR(50) 
); 

CREATE TABLE relationship (
    id INT PRIMARY KEY, 
    parent_id INT, 
    other_id INT 
); 

CREATE INDEX idx1 ON relationship (parent_id, other_id); 
CREATE INDEX idx2 ON relationship (other_id, parent_id); 

et près de 800 000 documents créés avec:

<?php 
ini_set('max_execution_time', 600); 

$start = microtime(true); 

echo "<pre>\n"; 
mysql_connect('localhost', 'scratch', 'scratch'); 
if (mysql_error()) { 
    echo "Connect error: " . mysql_error() . "\n"; 
} 
mysql_select_db('scratch'); 
if (mysql_error()) { 
    echo "Selct DB error: " . mysql_error() . "\n"; 
} 

define('PARENTS', 100000); 
define('CHILDREN', 100000); 
define('MAX_CHILDREN', 10); 
define('SCATTER', 10); 
$rel = 0; 
for ($i=1; $i<=PARENTS; $i++) { 
    query("INSERT INTO parent VALUES ($i, 'Parent $i')"); 
    $potential = range(max(1, $i - SCATTER), min(CHILDREN, $i + SCATTER)); 
    $elements = sizeof($potential); 
    $other = rand(1, min(MAX_CHILDREN, $elements - 4)); 
    $j = 0; 
    while ($j < $other) { 
     $index = rand(0, $elements - 1); 
     if (isset($potential[$index])) { 
      $c = $potential[$index]; 
      $rel++; 
      query("INSERT INTO relationship VALUES ($rel, $i, $c)"); 
      unset($potential[$index]); 
      $j++; 
     } 
    } 
} 
for ($i=1; $i<=CHILDREN; $i++) { 
    query("INSERT INTO other VALUES ($i, 'Other $i')"); 
} 

$count = PARENTS + CHILDREN + $rel; 
$stop = microtime(true); 
$duration = $stop - $start; 
$insert = $duration/$count; 

echo "$count records added.\n"; 
echo "Program ran for $duration seconds.\n"; 
echo "Insert time $insert seconds.\n"; 
echo "</pre>\n"; 

function query($str) { 
    mysql_query($str); 
    if (mysql_error()) { 
     echo "$str: " . mysql_error() . "\n"; 
    } 
} 
?> 

rejoint donc une nouvelle fois porter le jour.

+0

C'est exactement ce que j'ai, juste écrit différemment. –

+0

Vous avez complètement changé votre réponse ... –

+0

Oui. Parce que j'ai mal compris la question. – cletus

0

Je ne l'ai pas fait testé, mais quelque chose le long des lignes de:

SELECT id, name FROM (
    SELECT p1.id, p1.name 
    FROM parent AS p1 LEFT JOIN relationship as r1 ON(r1.parent_id=p1.id) 
    WHERE r1.other_id = 1 
    UNION ALL 
    SELECT p2.id, p2.name 
    FROM parent AS p2 LEFT JOIN relationship as r2 ON(r2.parent_id=p2.id) 
    WHERE r2.other_id = 2 
    -- etc 
) GROUP BY id, name 
HAVING count(*) = 2 

L'idée est que vous ne devez pas faire multivoies rejoint; concatéez simplement les résultats des jointures régulières, groupez-les selon vos identifiants et sélectionnez les lignes qui apparaissent dans chaque segment.

+0

Hmmm, ça pourrait marcher. Je pense que c'est encore plus compliqué que ce que j'ai, mais peut-être plus évident. –

+0

UNION dans la sous-requête = VRAIMENT VRAIMENT mauvais. Ne fais pas ça. – cletus

+0

ce n'est pas joli, mais le code pour le générer est simple, et je pense que vous verrez une grande amélioration des performances lorsque vous avez de nombreux parents. Si vous l'essayez, commentez avec vos résultats - je suis curieux. – SquareCog

2

Étant donné que la table parent contient la clé unique (id_parent, other_id) vous pouvez faire ceci:

select p.id, p.name 
    from parent as p 
where (select count(*) 
     from relationship as r 
     where r.parent_id = p.id 
     and r.other_id in (1,2) 
     ) >= 2 
+0

Très bonne idée ... maintenant pour tenter d'intégrer avec le reste de l'instruction SQL ... hmmm –

+0

Attention: avant de descendre cette route lire http://stackoverflow.com/questions/477006/sql-statement-join- vs-group-by-and-having/477013 # 477013 – cletus

+0

Cette solution (avec agrégation) est plus efficace que les sélections multiples si vous prévoyez avoir un nombre variable d'enfants. L'ajout d'identifiants à la liste IN est plus lisible et peut-être plus efficace que l'ajout de conditions avec SELECT par ID. Dans tous les cas, vérifiez le plan de requête pour comparer les requêtes ... – topchef

0

Ceci est un problème commun lors de la recherche de plusieurs associés par un grand nombre à plusieurs rejoindre. Ceci est souvent rencontré dans les services utilisant le concept 'tag', par ex. Stackoverflow

See my other post on a better architecture for tag (in your case 'other') storage

La recherche est un processus en deux étapes:

  1. Trouver tous les candiates possibles de TagCollections qui ont une/toutes les étiquettes dont vous avez besoin (peut être plus facile à l'aide d'un curseur de construction en boucle)
  2. Sélectionner des données sur la base qui correspond TagCollection

Performance est toujours plus rapide en raison de là être sig icantly moins TagCollections que des éléments de données à la recherche

0

Vous pouvez le faire avec un imbriquée select, je l'ai testé en MSSQL 2005, mais comme vous l'avez dit, il devrait être assez générique

SELECT * FROM parent p 
WHERE p.id in(
    SELECT r.parent_Id 
    FROM relationship r 
    WHERE r.parent_id in(1,2) 
    GROUP BY r.parent_id 
    HAVING COUNT(r.parent_Id)=2 
) 

et le numéro 2 dans COUNT(r.parent_Id)=2 est en fonction du nombre de jointures dont vous avez besoin)

+0

Pourquoi ai-je reçu un downvote pour une requête de travail testée? (Au moins, vous pouvez expliquer ce qui ne va pas avec) –

+0

N'était pas moi, mais peut-être parce que c'est presque le même que: http://stackoverflow.com/questions/599461/how-do-you-perform-an- et-with-a-join/599485 # 599485 –

+0

bien, alors que je le construisais dans MSSQL Studio je ne savais pas qu'une réponse similaire avait été postée, mais ce n'est pas une raison pour un downvote. –

1

Simplifier un peu, cela devrait fonctionner, et efficacement.

SELECT DISTINCT p.id, p.name
parent à partir de p
INNER JOIN relation r1 = ON p.id r1.parent_id ET r1.other_id = 1
INNER JOIN relation r2 ON p.id = r2.parent_id ET r2.other_id = 2

nécessitera au moins un enregistrement joint pour chaque "autre" valeur. Et l'optimiseur devrait savoir qu'il lui suffit de trouver une correspondance chacun, et il lui suffit de lire l'index, pas l'une ou l'autre des tables subsidiaires, dont aucune n'est même référencée.

0

Si vous pouvez mettre votre liste d'autres valeurs d'id dans une table qui serait idéale. Le code ci-dessous recherche les parents avec au moins les identifiants donnés. Si vous voulez qu'il ait exactement les mêmes identifiants (c'est-à-dire sans extras), vous devrez modifier légèrement la requête.

SELECT 
    p.id, 
    p.name 
FROM 
    My_Other_IDs MOI 
INNER JOIN Relationships R ON 
    R.other_id = MOI.other_id 
INNER JOIN Parents P ON 
    P.parent_id = R.parent_id 
GROUP BY 
    p.parent_id, 
    p.name 
HAVING 
    COUNT(*) = (SELECT COUNT(*) FROM My_Other_IDs)