Как вы выполняете функцию AND с помощью join?
Вопрос
У меня есть следующая структура данных и data:
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, но это, вероятно, довольно общий язык.
Решение
Хорошо, я проверил это.Запросы от лучшего к худшему были следующими:
Запрос 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 с)
ВЫБЕРИТЕ p.id, p.name ИЗ родительского p ГДЕ (ВЫБЕРИТЕ COUNT(*) ИЗ отношения, ГДЕ parent_id = p.id И other_id В (100,101,102,103))
Запрос 4:Объединительный агрегат (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)
Прежде чем вы перейдете по пути агрегирования (ВЫБЕРИТЕ COUNT(*) ИЗ ...), вы должны прочитать Оператор 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";
}
}
?>
Итак, еще раз присоединяемся к carry the day.
Другие советы
Учитывая, что родительская таблица содержит уникальный ключ (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
Немного упрощая, это должно работать и эффективно.
ВЫБЕРИТЕ ОТЛИЧНЫЕ p.id, p.name
ОТ родительского п
Отношение ВНУТРЕННЕЕ СОЕДИНЕНИЕ r1 ON p.id = r1.parent_id И r1.other_id = 1
Отношение ВНУТРЕННЕЕ СОЕДИНЕНИЕ r2 ON p.id = r2.parent_id И r2.other_id = 2
потребуется хотя бы одна объединенная запись для каждого «другого» значения.И оптимизатор должен знать, что ему нужно найти только одно совпадение, и ему нужно прочитать только индекс, а не какую-либо из вспомогательных таблиц, на одну из которых вообще нет ссылок.
На самом деле я это не проверял, но что-то вроде:
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
Идея в том, что вам не обязательно выполнять многосторонние соединения;просто объедините результаты обычных объединений, сгруппируйте их по идентификаторам и выберите строки, которые появились в каждом сегменте.
Это распространенная проблема при поиске нескольких партнеров через соединение «многие ко многим».Это часто встречается в сервисах, использующих концепцию «тега», например.Переполнение стека
См. другой мой пост о лучшей архитектуре для хранения тегов (в вашем случае «другое»).
Поиск состоит из двух этапов:
- Найдите все возможные кандидаты TagCollections, которые имеют любые/все необходимые вам теги (может быть проще использовать курсор или конструкцию цикла)
- Выберите данные на основе, соответствующие TagCollection
Производительность всегда выше, поскольку коллекций тегов значительно меньше, чем элементов данных для поиска.
Вы можете сделать это с помощью вложенного выбора, я тестировал его в 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
в зависимости от количества соединений, которые вам нужны)
Если бы вы могли поместить список значений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)