Pergunta

Eu tenho a seguinte estrutura de dados e dados:

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

Eu quero encontrar os registros pai com as duas outras de 1 & 2.

Isto é o que eu descobri, mas eu estou querendo saber se existe uma maneira melhor:

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;

O resultado é 1, "pai 1" que é correto. O problema é que uma vez que você obter uma lista de 5+ junta-se, torna-se confuso e como a tabela de relacionamento cresce, torna-se lenta.

Existe uma maneira melhor?

Estou usando o MySQL e PHP, mas isso provavelmente é bastante genérico.

Foi útil?

Solução

Ok, eu testei isso. As consultas do melhor para o pior foram os seguintes:

Consulta 1: Junta (0.016s; basicamente instantânea )

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

Consulta 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

Consulta 3: Agregado (1.016s)

p.id SELECT, p.name DE-mãe p WHERE (SELECT COUNT (*) FROM relacionamento onde parent_id = p.id E EM other_id (100101102103))

Consulta 4: UNIÃO Agregado (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

Na verdade, o acima foi produzir os dados errados por isso é errado ou eu fiz algo de errado com ele. Seja qual for o caso, o acima é apenas uma má idéia.

Se isso não é rápido, então você precisa olhar para o plano de explicar para a consulta. Você provavelmente está apenas falta índices apropriados. Experimente-o com:

CREATE INDEX ON relationship (parent_id, other_id)

Antes de ir abaixo da rota de agregação (SELECT COUNT (*) FROM ...) você deve ler instrução SQL - ‘Junte-se a’ Vs ‘Group By e ter’

Nota: Os horários acima são baseados em:

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

e cerca de 800.000 registros criados com:

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

junta Então mais uma vez levar o dia.

Outras dicas

Uma vez que a tabela pai contém chave única na (parent_id, other_id) você pode fazer isso:

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

Simplificando um pouco, isso deve funcionar, e eficiente.

SELECT p.id DISTINCT, p.name
De pai p
INNER JOIN relação R1 no p.id = r1.parent_id E r1.other_id = 1 | INNER JOIN relação r2 ON p.id = r2.parent_id E r2.other_id = 2

exigirá pelo menos um registro juntou para cada valor "outro". E o otimizador deve saber que só tem de encontrar uma partida, e só precisa de ler o índice, não tanto das mesas auxiliares, um dos quais não é mesmo referenciados em tudo.

Eu realmente não tenho testado, mas algo ao longo das linhas 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

A idéia é que você não tem que fazer multi-caminho junta-se; apenas concatenar os resultados da junta regular, grupo por seus ids, e escolher as linhas que apareceram em todos os segmentos.

Este é um problema comum quando procurar vários associados através de muitos para muitos aderir. Este é freqüentemente encontrado em serviços que utilizam o conceito de 'tag', por exemplo, Stackoverflow

Veja meu outro post sobre uma melhor arquitetura para tag (no seu caso 'outro') de armazenamento

Searching é um processo de duas etapas:

  1. Encontre todas candiates possíveis de TagCollections que têm quaisquer / todas as tags que você precisa (pode ser mais fácil usar um cursor de construção de loop)
  2. Selecionar dados com base que os jogos TagCollection

O desempenho é sempre mais rápido devido à existência de um número significativamente menor TagCollections do que itens de dados de pesquisa

Você pode fazer isso com um SELECT aninhada, eu testei em MSSQL 2005, mas como você disse que deve ser bastante genérico

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
)

e o número 2 em COUNT(r.parent_Id)=2 está de acordo com o número de junções que você precisa)

Se você pode colocar sua lista de valores other_id em uma tabela que seria o ideal. O código abaixo olhares para os pais com pelo menos os ids dado. Se você deseja que ele tem exatamente o mesmo IDS (ou seja, sem extras), você teria que alterar a consulta ligeiramente.

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)
Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top