Question

J'ai la structure de données et les données suivantes:

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 me demande s'il y a une meilleure façon:

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+ rejoint, il obtient en désordre et que la table de relation se développe, il devient lent.

Y at-il une meilleure façon?

J'utilise MySQL et PHP, mais cela est probablement assez générique.

Était-ce utile?

La solution

Ok, je l'ai testé cela. Les requêtes du meilleur au pire étaient les suivants:

Requête 1: Rejoint (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: EXISTS (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 À partir de parent p OU (SELECT COUNT (*) de la relation OÙ parent_id = p.id ET other_id IN (100.101.102.103))

Requête 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. Quel que soit le cas, ce qui précède est juste une mauvaise idée.

Si ce n'est pas rapide alors vous devez regarder l'expliquer plan pour la requête. Vous êtes probablement manque juste des 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 Déclaration SQL - « Join » Vs « group by et having »

.

Remarque: 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";
    }
}
?>

se joint à nouveau porter le jour.

Autres conseils

É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

Simplifier un peu, cela devrait fonctionner, et efficacement.

  

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

il faudra au moins un joint record pour chaque valeur « autre ». Et l'optimiseur doit savoir qu'il a seulement pour trouver un match chacun, et il n'a besoin que de lire l'index, pas non plus des tables auxiliaires, dont une est même pas fait référence à tous.

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; juste concaténer les résultats rejoint régulièrement, groupe par vos ids, et ramasser les lignes qui se sont présentés dans tous les segments.

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 exemple Stackoverflow

Voir mon autre post sur une meilleure architecture tag (dans votre cas 'autre') le stockage

La recherche est un procédé 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 les données sur la base qui correspond TagCollection

La performance est toujours plus rapide en raison d'être là TagCollections beaucoup moins que les éléments de données pour la recherche

Vous pouvez le faire avec une sélection imbriquée, 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 COUNT(r.parent_Id)=2 est selon le nombre d'entre vous avez besoin joint)

Si vous pouvez mettre votre liste de valeurs other_id dans une table qui serait idéal. Le code ci-dessous semble que les parents ayant au moins les ids données. Si vous voulez qu'il ait EXACTEMENT les mêmes ids (à savoir pas d'extras), vous devez changer la requête légèrement.

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)
Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top