MySQL запрос для общих друзей
Возможный дубликат:
MYSQL выбирает общих друзей
У меня есть стол для дружбы, дружба хранится только в одной строке. Так что нет повторяющихся записей.
id Person1 Person2 status
1 1 2 friend
2 1 3 friend
3 2 3 friend
4 3 4 friend
Какой запрос MySQL (соединение, внутреннее соединение) поможет мне найти общих (общих) друзей между человеком № 1 и человеком № 3? Входные данные в этом примере - {1,3}, а выходные данные должны быть {2}, так как Person #2 дружит с ботами #1 и #3.
11 ответов
Ну, единственный запрос, который может работать до сих пор, - это запрос Саймона... но это настоящее излишество - такой сложный неприятный запрос (2 подзапроса с 2 объединениями!) Для настолько простой вещи, что вам нужно разместить награду?:-) И если у вас более 1000 пользователей, запрос будет медленным до чертиков - запомните, он квадратичный, и из-за объединений в подзапросах вряд ли будет использоваться какой-либо индекс!
Я бы посоветовал еще раз подумать над дизайном и добавить 2 повторяющихся строки для дружбы:
id Person1 Person2 status
1 1 2 friend
2 2 1 friend
3 1 3 friend
4 3 1 friend
Вы можете подумать, что это неэффективно, но следующее упрощение позволит переписать запрос в простые объединения:
select f1.Person2 as common_friend
from friends as f1 join friends as f2
using (Person2)
where f1.Person1 = '$id1' and f2.Person1 = '$id2'
and f1.status = 'friend' and f2.status = 'friend'
который будет быстрым как ад! (Не забудьте добавить индексы для Person1,2.) Я посоветовал аналогичное упрощение (переписать подзапросы в объединения) в другой очень неприятной структуре данных, и это ускорило запрос от вечности до мгновенных результатов!
То, что выглядело как большие накладные расходы (2 строки на одну дружбу), на самом деле является большой оптимизацией:-)
Кроме того, это значительно упростит запросы типа "найди всех друзей Х". И больше никаких наград не нужно будет тратить:-)
Еще один ответ.
select
(case when f1.person1 = 1 then f1.person2 else f1.person1 end) as fid
from friends f1
where f1.person1 = 1 or f1.person2 = 1
and f1.status = 'friend'
intersect
select
(case when f1.person1 = 3 then f1.person2 else f1.person1 end) as fid
from friends f1
where f1.person1 = 3 or f1.person2 = 3
and f1.status = 'friend'
Этот запрос работает с предположением, что в таблице дружбы нет самодеятельности и дубликатов, если эти условия не соответствуют небольшим изменениям, необходимым для его работы.
SELECT fid FROM
(
--FIRST PERSON (X) FRIENDLIST
SELECT
(CASE WHEN Person1 = X THEN Person2 ELSE Person1 END) AS fid
FROM Friendships WHERE (Person1 = X OR Person2 = X) AND status = "friend"
UNION ALL --DO NOT REMOVE DUPLICATES WITH ALL JOIN
--SECOND PERSON (Y) FRIENDLIST
SELECT
(CASE WHEN Person1 = Y THEN Person2 ELSE Person1 END) AS fid
FROM Friendships WHERE (Person1 = Y OR Person2 = Y) AND status = "friend"
) FLIST
GROUP BY fid
HAVING COUNT(*) = 2
set search_path='tmp';
DROP TABLE friendship CASCADE;
CREATE TABLE friendship
( id integer not null PRIMARY KEY
, person1 integer not null
, person2 integer not null
, status varchar
, CONSTRAINT pk1 UNIQUE (status,person1,person2)
, CONSTRAINT pk2 UNIQUE (status,person2,person1)
, CONSTRAINT neq CHECK (person1 <> person2)
);
INSERT INTO friendship(id,person1,person2,status) VALUES
(1,1,2,'friend' ) ,(2,1,3,'friend' ) ,(3,2,3,'friend' ) ,(4,3,4,'friend' )
;
-- -----------------------------------------
-- For implementations that don't have CTEs,
-- a view can be used to emulate a CTE.
-- -----------------------------------------
CREATE VIEW flip AS (
SELECT person1 AS one
, person2 AS two
FROM friendship WHERE status = 'friend'
UNION
SELECT person2 AS one
, person1 AS two
FROM friendship WHERE status = 'friend'
);
SELECT DISTINCT
f1.two AS common
FROM flip f1
JOIN flip f2 ON f1.two = f2.two
WHERE f1.one = 1
AND f2.one = 3
;
DROP VIEW flip;
-- ------------------------------
-- The same query with a real CTE
-- ------------------------------
with flip AS (
SELECT person1 AS one
, person2 AS two
FROM friendship WHERE status = 'friend'
UNION
SELECT person2 AS one
, person1 AS two
FROM friendship WHERE status = 'friend'
)
SELECT DISTINCT
f1.two AS common
FROM flip f1
JOIN flip f2 ON f1.two = f2.two
WHERE f1.one = 1
AND f2.one = 3
;
РЕЗУЛЬТАТ:
SET
DROP TABLE
NOTICE: CREATE TABLE / PRIMARY KEY will create implicit index "friendship_pkey" for table "friendship"
NOTICE: CREATE TABLE / UNIQUE will create implicit index "pk1" for table "friendship"
NOTICE: CREATE TABLE / UNIQUE will create implicit index "pk2" for table "friendship"
CREATE TABLE
INSERT 0 4
CREATE VIEW
common
--------
2
(1 row)
DROP VIEW
common
--------
2
(1 row)
Этот запрос возвращает "22" в качестве результата, поскольку он является общим для 1 и 3. Возможно, вам придется отфильтровать отдельное ЛИЦО1/ ЛИЦО2. Если я могу оптимизировать этот запрос, я обновлю его
SELECT DISTINCT (REPLACE(TRANSLATE((WM_CONCAT(DISTINCT F.PERSON1) || ',' ||
WM_CONCAT(DISTINCT F.PERSON2)),
'1,3',
' '),
' ',
'')) AS COMMON_FRIEND
FROM FRIENDSHIP F
WHERE UPPER(F.STATUS) = 'FRIEND'
AND ((SELECT DISTINCT WM_CONCAT(F1.PERSON1)
FROM FRIENDSHIP F1
WHERE F1.PERSON2 = '3') LIKE ('%' || F.PERSON1 || '%') OR
(SELECT DISTINCT WM_CONCAT(F1.PERSON2)
FROM FRIENDSHIP F1
WHERE F1.PERSON1 = '3') LIKE ('%' || F.PERSON2 || '%'))
AND ((SELECT DISTINCT WM_CONCAT(F1.PERSON1)
FROM FRIENDSHIP F1
WHERE F1.PERSON2 = '1') LIKE ('%' || F.PERSON1 || '%') OR
(SELECT DISTINCT WM_CONCAT(F1.PERSON2)
FROM FRIENDSHIP F1
WHERE F1.PERSON1 = '1') LIKE ('%' || F.PERSON2 || '%'))
AND NOT ((F.PERSON1 = '1' AND F.PERSON2 = '3') OR
(F.PERSON1 = '3' AND F.PERSON2 = '1'))
Приношу свои извинения, если один из различных ответов или комментариев уже предложил это, но как насчет:
select Person2 mutual_friend from
(select Person1, Person2 from friends
where Person1 in (1,3) union
select Person2, Person1 from friends
where Person2 in (1,3)
) t
group by Person2 having count(*) > 1;
Внутренний запрос получает только идентификаторы FRIEND для первого лица и стандартизирует его в один столбец "FriendID". Если найденная запись имеет идентификатор человека = 1 в первой позиции, она захватывает вторую... если идентификатор человека = 1 во второй позиции, то она захватывает первую.
После того, как это будет сделано, мы узнаем, кто является единственным списком друзей человека 1... Готово. Теперь снова присоединитесь к таблице дружбы, но только для тех, кто ПЕРВЫЙ квалифицирован как один из друзей человека 1... Как только это будет квалифицировано, убедитесь, что другой человек на втором столе - это человек 3, который вы ищете общность.
Убедитесь, что индекс "ОБА человек1", а другой - "Человек2", позволяет воспользоваться условиями ИЛИ.
select
JustPerson1Friends.FriendID
from
( select
if( f.Person1 = 1, f.Person2, f.Person1 ) as FriendID
from
Friendships f
where
( f.Person1 = 1
OR f.Person2 = 1 )
AND f.status = "friend" ) JustPerson1Friends
JOIN Friendships f2
on ( JustPerson1Friends.FriendID = f2.Person1
OR JustPerson1Friends.FriendID = f2.Person2 )
AND f2.status = "friend"
AND ( f2.Person1 = 3 OR f2.person2 = 3 )
Другой вариант - предварительно указать человека "3" в качестве общего в результирующем наборе, так что нам не нужно явно определять 3 позже. Кроме того, с помощью MySQL Variables легко создавать сценарии и реализовывать их в качестве параметров. После внутреннего запроса выполните ДВОЙНОЕ присоединение к друзьям, чтобы явно протестировать ОБА комбинации, где можно найти человека в комбинации X/Y или Y/X. Таким образом, последнее предложение where просто говорит, что пока запись находится в состоянии EITHER LEFT-JOIN, она является общим другом и включается в набор результатов.
select
JustPerson1Friends.FriendID
from
( select
@WantPerson2 as FindInCommonWith,
if( f.Person1 = @WantPerson1, f.Person2, f.Person1 ) as FriendID
from
( select @WantPerson1 := 1,
@WantPerson2 := 3 ) sqlvars
Friendships f,
(
where
( f.Person1 = @WantPerson1
OR f.Person2 = @WantPerson2 )
AND f.status = "friend" ) JustPerson1Friends
LEFT JOIN Friendships f2
on JustPerson1Friends.FindInCommonWith = f2.Person1
AND JustPerson1Friends.FriendID = f2.Person2
AND f2.status = "friend"
LEFT JOIN Friendships f3
on JustPerson1Friends.FindInCommonWith = f2.Person2
AND JustPerson1Friends.FriendID = f2.Person1
AND f2.status = "friend"
where
f2.Person1 > 0
OR f3.Person1 > 0
Я спросил, всегда ли пользователь с меньшим номером Person1
, но я закончил писать запрос, который не волнует, если это правда.
set @firstParty = 1, @secondParty = 3
select friends_of_first.friend
from (
select Person2 as friend from friends where Person1 = @firstParty
union
select Person1 as friend from friends where Person2 = @firstParty
) as friends_of_first
join (
select Person2 as friend from friends where Person1 = @secondParty
union
select Person1 as friend from friends where Person2 = @secondParty
) as friends_of_second
on friends_of_first.friend = friends_of_second.friend
Подзапросы для поиска друзей пользователя могут быть заменены на один из @Nirmal- thInk beYond:
select case when f1.person1 = @firstParty then f1.person2 else f1.person1 end
from friend f1 where f1.person1 = @firstParty or f1.person2 = @firstParty
Мне было бы интересно посмотреть, какая альтернатива работает лучше.
id Person1 Person2 status
1 1 2 friend
2 1 3 friend
3 2 3 friend
4 3 4 friend
SELECT
DISTINCT
F1.Person
FROM
--Friends of 1
(
SELECT F.Person1 Person FROM People F WHERE F.Person2 = 1 AND F.status = 'friend'
UNION
SELECT F.Person2 Person FROM People F WHERE F.Person1 = 1 AND F.status = 'friend'
) F1
INNER JOIN
(
--Friends of 3
SELECT F.Person1 Person FROM People F WHERE F.Person2 = 3 AND F.status = 'friend'
UNION
SELECT F.Person2 Person FROM People F WHERE F.Person1 = 3 AND F.status = 'friend'
) F2 ON
F2.Person = F1.Person
Выход:
Person
2
Я думаю, что это довольно просто достигается этим
SELECT * FROM friends
WHERE
(Person1 = '1' or Person2 = '1') &&
(Person1 = '2' or Person2 = '2') &&
status = 'friend'
Учитывая, что вы пытаетесь найти взаимность между человеком 1 и 2
Это должно ответить на ваш текущий вопрос, хотя я бы не советовал делать это так. В этой ситуации я всегда выбрал бы хранение двух копий отношений, по одной в каждом направлении.
SELECT IF(f1.person1 IN ($id1, $id3), f1.person2, f1.person1) AS mutual_friend
FROM friends f1
INNER JOIN friends f2
ON (f1.person1 = $id1 AND f2.person1 = $id3 AND f1.person2 = f2.person2)
OR (f1.person1 = $id1 AND f2.person2 = $id3 AND f1.person2 = f2.person1)
OR (f1.person2 = $id1 AND f2.person1 = $id3 AND f1.person1 = f2.person2)
OR (f1.person2 = $id1 AND f2.person2 = $id3 AND f1.person1 = f2.person1)
WHERE f1.status = 'friend' AND f2.status = 'friend'