Как вы выполняете И с соединением?

У меня есть следующая структура данных и данные:

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

Я хочу найти родительские записи с другими 1 и 2.

Это то, что я понял, но мне интересно, есть ли лучший способ:

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;

Результат 1, «родитель 1», что правильно. Проблема в том, что как только вы получаете список из 5+ объединений, он становится беспорядочным, а по мере роста таблицы отношений он становится медленнее.

Есть ли способ лучше?

Я использую MySQL и PHP, но это, вероятно, довольно общее.


person Darryl Hein    schedule 01.03.2009    source источник


Ответы (7)


Хорошо, я проверил это. Запросы от лучшего к худшему:

Запрос 1: соединения (0,016 с; в основном мгновенно)

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

Запрос 2: СУЩЕСТВУЕТ (0,625 с)

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

Запрос 3: Сводка (1,016 с)

SELECT p.id, p.name FROM parent p WHERE (SELECT COUNT(*) FROM отношения WHERE parent_id = p.id AND other_id IN (100,101,102,103))

Запрос 4: UNION Aggregate (2,39 с)

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

На самом деле вышеизложенное выдавало неверные данные, так что это либо неправильно, либо я сделал что-то не так. В любом случае, это просто плохая идея.

Если это не быстро, вам нужно посмотреть план объяснения для запроса. Вероятно, вам просто не хватает соответствующих индексов. Попробуйте с:

CREATE INDEX ON relationship (parent_id, other_id)

Прежде чем идти по маршруту агрегации (SELECT COUNT(*) FROM...), вы должны прочитать оператор SQL — «присоединиться» или «группировать по и по наличию».

Примечание. Вышеуказанные сроки основаны на:

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

и почти 800 000 записей, созданных с помощью:

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

Так что еще раз присоединяется нести день.

person cletus    schedule 01.03.2009
comment
У меня именно так, только написано по другому. - person Darryl Hein; 01.03.2009
comment
да. Потому что я неправильно понял вопрос. - person cletus; 01.03.2009
comment
Хммм... похоже, моя первая догадка была верной... и вам даже не понадобился DISTINCT. Приятно, когда лучший ответ можно определить тестированием, а не голосованием. Спасибо, Клетус. - person dkretz; 01.03.2009
comment
это доказывает, что иногда не общий запрос лучше, чем общий, конечно, не всегда вы можете динамически создать запрос. - person Pablo Retyk; 02.03.2009

Учитывая, что родительская таблица содержит уникальный ключ (parent_id, other_id), вы можете сделать это:

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
person topchef    schedule 01.03.2009
comment
Очень хорошая идея... теперь попробуем интегрироваться с остальной частью оператора SQL... хммм - person Darryl Hein; 01.03.2009
comment
Предупреждение: прежде чем идти по этому маршруту, прочитайте stackoverflow.com/questions/477006/ - person cletus; 01.03.2009
comment
Это решение (с агрегацией) более эффективно, чем множественный выбор, если вы ожидаете иметь различное количество дочерних элементов. Добавление идентификаторов в список IN более удобочитаемо и, возможно, более эффективно, чем добавление условий с помощью SELECT для каждого идентификатора. В любом случае проверьте план запроса для сравнения запросов... - person topchef; 01.03.2009

Немного упрощая, это должно работать, и эффективно.

SELECT DISTINCT p.id, p.name
FROM родительского p
INNER JOIN отношение r1 ON p.id = r1.parent_id AND r1.other_id = 1
INNER JOIN отношение r2 ON p.id = r2. parent_id И r2.other_id = 2

потребуется по крайней мере одна объединенная запись для каждого "другого" значения. И оптимизатор должен знать, что ему нужно найти только одно совпадение для каждой, и ему нужно только прочитать индекс, а не любую из вспомогательных таблиц, на одну из которых вообще нет ссылок.

person dkretz    schedule 01.03.2009

Я на самом деле не проверял это, но что-то вроде строк:

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

Идея в том, что вам не нужно выполнять многосторонние соединения; просто объедините результаты обычных соединений, сгруппируйте по своим идентификаторам и выберите строки, которые появились в каждом сегменте.

person SquareCog    schedule 01.03.2009
comment
Хм, это может сработать. Я думаю, что это еще более грязно, чем то, что у меня есть, но, может быть, более очевидно. - person Darryl Hein; 01.03.2009
comment
UNION в подзапросе = ОЧЕНЬ ОЧЕНЬ плохо. Не делай этого. - person cletus; 01.03.2009
comment
это некрасиво, но код для его создания прост, и я подозреваю, что вы увидите значительное улучшение производительности, когда у вас будет много родителей. Если вы попробуете это, прокомментируйте свои результаты - мне любопытно. - person SquareCog; 01.03.2009
comment
Клетус - правда? Я знаю, что Oracle все равно, но у меня есть некоторый травмирующий опыт работы с подзапросами в MySQL. Что он делает, что делает его плохим? - person SquareCog; 01.03.2009
comment
Оракл нет? Э-э, я работал над системами, абсолютно поврежденными использованием UNION и UNION ALL... - person cletus; 01.03.2009
comment
Похоже, что Oracle все равно, является ли запрос подзапросом или нет. Я думал, вы говорите, что не используйте союзы в подзапросах в частности - вы говорили, что не используете союзы вообще? - person SquareCog; 01.03.2009

Это распространенная проблема при поиске нескольких партнеров через соединение «многие ко многим». Это часто встречается в службах, использующих концепцию «тегов», например. Переполнение стека

См. мой другой пост о лучшей архитектуре для тега (в ваш случай "другое") хранилище

Поиск состоит из двух шагов:

  1. Найдите все возможные кандидаты TagCollections, которые имеют любые/все необходимые вам теги (может быть проще, используя курсор конструкции цикла)
  2. Выберите данные, соответствующие TagCollection

Производительность всегда выше из-за того, что коллекций тегов значительно меньше, чем элементов данных для поиска.

person TFD    schedule 01.03.2009

Вы можете сделать это с помощью вложенного выбора, я тестировал его в MSSQL 2005, но, как вы сказали, он должен быть довольно общим.

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
)

и число 2 в COUNT(r.parent_Id)=2 соответствует количеству соединений, которые вам нужны)

person Pablo Retyk    schedule 01.03.2009
comment
почему я получил минус за протестированный рабочий запрос? (по крайней мере, вы можете объяснить, что с ним не так) - person Pablo Retyk; 01.03.2009
comment
Это был не я, но, возможно, потому, что это почти то же самое, что и: stackoverflow.com/questions/599461/ - person Darryl Hein; 01.03.2009
comment
ну, пока я создавал его в MSSQL Studio, я не знал, что был опубликован аналогичный ответ, но это не причина для отрицательного ответа. - person Pablo Retyk; 01.03.2009
comment
Я дам вам голос, чтобы сделать его 0 ... хотя это все равно дает вам больше репутации. - person Darryl Hein; 01.03.2009
comment
Спасибо, просто чтобы прояснить, что проблема не в представителе, я здесь, чтобы узнать, был ли у меня неправильный ответ, я просто хотел бы знать, что не так. - person Pablo Retyk; 01.03.2009
comment
Разве r.parent_id в (1, 2) не должен быть r.other_id в (1, 2)? - person Tom H; 01.03.2009

Если бы вы могли поместить свой список значений other_id в таблицу, это было бы идеально. Приведенный ниже код ищет родителей, у которых есть, ПО КРАЙНЕЙ МЕРЕ, указанные идентификаторы. Если вы хотите, чтобы у него были ТОЧНО те же идентификаторы (т.е. без дополнений), вам придется немного изменить запрос.

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)
person Tom H    schedule 01.03.2009