Определение соседей узла с помощью SQL

У меня есть следующая таблица в SQL:

CREATE TABLE friendships 
(
    id INT PRIMARY KEY,
    person VARCHAR(50),
    friend VARCHAR(50)
);

INSERT INTO friendships (id, person, friend) VALUES
(1, 'person3', 'person9'),
(3, 'person10', 'person4'),
(4, 'person2', 'person1'),
(5, 'person6', 'person7'),
(7, 'person4', 'person10'),
(8, 'person6', 'person7'),
(10, 'person10', 'person9'),
(11, 'person5', 'person10'),
(12, 'person3', 'person7'),
(13, 'person9', 'person5'),
(14, 'person9', 'person7'),
(15, 'person9', 'person5'),
(16, 'person3', 'person6'),
(17, 'person8', 'person9'),
(18, 'person10', 'person2'),
(19, 'person7', 'person5'),
(20, 'person10', 'person8');

Используя R, я могу построить таблицу данных следующим образом:

library(igraph)

friendships = structure(list(person = c("person3", "person10", "person2", "person6", 
"person4", "person6", "person10", "person5", "person3", "person9", 
"person9", "person9", "person3", "person8", "person10", "person7", 
"person10"), friend = c("person9", "person4", "person1", "person7", 
"person10", "person7", "person9", "person10", "person7", "person5", 
"person7", "person5", "person6", "person9", "person2", "person5", 
"person8")), row.names = c(1L, 3L, 4L, 5L, 7L, 8L, 10L, 11L, 
12L, 13L, 14L, 15L, 16L, 17L, 18L, 19L, 20L), class = "data.frame")


g <- graph_from_data_frame(df, directed = FALSE)
plot(g, vertex.size=10, vertex.label.cex=0.8)

ТОЛЬКО с помощью SQL-команд хочу ответить на следующие вопросы (в дальнейшем обобщаю до n-й степени):

  • Сколько узлов подключено к Person10 по степени2 (т.е. друзья друзей, 8 человек)
  • Кто эти люди, связанные с человеком 10 по степени 2? (т. е. человек2, человек1, человек4, человек8, человек9, человек5, человек7, человек3)

Я не уверен, как это сделать в SQL. Обычно для решения этих задач я использую igraph.

Вот моя попытка ответить на первый вопрос:

SELECT COUNT(DISTINCT f2.friend)
FROM friendships f1
JOIN friendships f2 ON f1.friend = f2.person
WHERE f1.person = 'person10' AND f2.friend != 'person10' AND f2.friend NOT IN (
    SELECT friend FROM friendships WHERE person = 'person10'
);

#result
 3

Вот моя попытка ответить на второй вопрос:

SELECT DISTINCT f2.friend
FROM friendships f1
JOIN friendships f2 ON f1.friend = f2.person
WHERE f1.person = 'person10' AND f2.friend != 'person10' AND f2.friend NOT IN (
    SELECT friend FROM friendships WHERE person = 'person10'
);

#result
1 person5
2 person7
3 person1

Однако это не правильные результаты.

Может кто-нибудь, пожалуйста, покажите мне, как это сделать правильно? Возможно, на R можно написать цикл/функцию, которая будет выполнять SQL до завершения?

Я использую более старую версию SQL, в которой нет рекурсивных функций.

Спасибо!

#спасибо рычагу ясности в вопросе!!!

Lars G Olsen 24.03.2024 07:31

@ Ларс Дж. Олсен: Большое спасибо за добрые слова! Я пытался решить это сам здесь: stackoverflow.com/questions/78273876/… .... Я подошел близко, но проблемы все еще есть. Есть идеи?

stats_noob 04.04.2024 14:35
ReactJs | Supabase | Добавление данных в базу данных
ReactJs | Supabase | Добавление данных в базу данных
Это и есть ваш редактор таблиц в supabase.👇
Понимание Python и переход к SQL
Понимание Python и переход к SQL
Перед нами лабораторная работа по BloodOath:
3
2
165
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Ответ принят как подходящий

Рассмотрим этот рекурсивный пример.
Есть условие where person='person10' для выбора одного человека, иначе - найти друзей 2 уровня для всех людей.
Условие lvl<2 - засчитывайте друзей для 1 и 2 уровня. Вы можете установить любой возможный уровень.
И столбец path только для ясности, если в графике нет циклов.

with t0 as (
select person,friend from friendships
-- where person='person10'
  union
select friend,person from friendships
-- where friend='person10'
)
, r as(
select person,friend,1 lvl
  ,cast(concat(person,'-',friend) as varchar(1000))path
from t0
union all
select r.person,t.friend ,r.lvl+1 lvl
  ,cast(concat(path,'-',t.friend) as varchar(1000)) path
from r
inner join friendships t on (t.person=r.friend and t.friend<>r.person)
where lvl<2
union all
select r.person,t.person ,r.lvl+1 lvl
  ,cast(concat(path,'-',t.person)  as varchar(1000))path
from r
inner join friendships t on (t.friend=r.friend and t.person<>r.person)
where lvl<2
)
,allfriends as (
select person,friend ,count(*) qty
from r
group by person,friend
)
select person,count(*)friend_qty
from allfriends
group by person

Результат для всех

человек друг_кол-во человек1 2 человек10 8 человек2 6 человек3 6 человек4 5 человек5 8 человек6 4 человек7 6 человек8 7 человек9 8

Для person='person10' cte r обходит граф по всем путям.

человек друг уровень путь человек10 человек2 1 человек10-человек2 человек10 человек4 1 человек10-человек4 человек10 человек5 1 человек10-человек5 человек10 человек8 1 человек10-человек8 человек10 человек9 1 человек10-человек9 человек10 человек1 2 человек10-человек2-человек1 человек10 человек3 2 человек10-человек9-человек3 человек10 человек5 2 человек10-человек9-человек5 человек10 человек5 2 человек10-человек9-человек5 человек10 человек7 2 человек10-человек5-человек7 человек10 человек7 2 человек10-человек9-человек7 человек10 человек8 2 человек10-человек9-человек8 человек10 человек9 2 человек10-человек8-человек9 человек10 человек9 2 человек10-человек5-человек9 человек10 человек9 2 человек10-человек5-человек9

AllFriends для person10 (повышение уровня 2) и количество путей для друга

человек друг количество человек10 человек1 1 человек10 человек2 1 человек10 человек3 1 человек10 человек4 1 человек10 человек5 3 человек10 человек7 2 человек10 человек8 2 человек10 человек9 4

Там 2 пути (10-4), (4-10) считаются за 1 путь - в первом пункте мы используем union (не union all).
Для пары (10,9) есть 4 пути (10-9),(10-8-9),(10-5-9) и (10-5-9), на остальных уровнях пути не исключаем (union all) . Можно оставить только уникальные пути при группировке.

Вывод для лвл=1

человек друг количество человек10 человек2 1 человек10 человек4 1 человек10 человек5 1 человек10 человек8 1 человек10 человек9 1

Демо здесь

Пример для SQL-сервера. Практически без изменений (varchar(1000)->nchar(1000)) подходит для MySQL.

К сожалению, я не знаю, как реализовать расширенную рекурсию в postgresql, например (выбрать.. объединить все, выбрать... объединить все, выбрать...)
Мы можем сделать это в postgresql, усложнив соединение привязки и таблицы.

Обновление1. Если в графе есть циклы, мы должны проверить - мы уже прошли этот узел - есть ли узел в path? Такая проверка несколько зависит от используемой СУБД (instr,patindex...). В этом случае путь является обязательным.

#престиж^2 за этот ответ, я ошеломлен им, и мне придется проверить его самому, просто чтобы насладиться глубиной ясности, которую он обеспечивает!

Lars G Olsen 24.03.2024 07:34

@ValNik: большое спасибо за ответ! Я какое-то время уезжал из страны и только что вернулся..

stats_noob 04.04.2024 12:39

Я пытался решить это сам здесь: stackoverflow.com/questions/78273876/… .... Я подошел близко, но проблемы все еще есть. Есть идеи?

stats_noob 04.04.2024 14:35

Давайте сначала ответим на ваш второй вопрос, потому что вы можете получить результат по первому вопросу, используя результат второго вопроса.

Чтобы ответить на ваш второй вопрос, вы можете написать функцию, которая выполняет следующие действия:

DECLARE @Degree INT = 2
DECLARE @Root NVARCHAR(50) = 'Person1'

CREATE TABLE #ResultNieghbours (
    person    VARCHAR(50)
);

INSERT INTO #ResultNieghbours
VALUES(@Root);

WHILE @Degree > 0
BEGIN
    INSERT INTO #ResultNieghbours
    SELECT DISTINCT f.friend
    FROM #ResultNieghbours rn
    JOIN Friendships f ON rn.person = f.person
    WHERE NOT EXISTS (
        SELECT n.person 
        FROM #ResultNieghbours n 
        WHERE n.person = f.friend
    )

    SET @Degree = @Degree - 1
END

Идея здесь состоит в том, чтобы выполнить поиск в ширину (BFS).

В каждую волну нам нужно добавлять друзей из следующей волны. Мы делаем это, перебирая все взятые на данный момент Person и ищем их друзей. Мы делаем это n раз, где n — друг n степени.

Мы гарантируем, что возьмем предметы только из следующей волны (а не из друзей, которые уже включены в список с помощью пункта WHERE NOT EXISTS (...)).

Чтобы получить результат вашего первого вопроса, вы можете просто посчитать количество элементов в #ResultNieghbours.

Редактировать: В настоящее время вы рассматриваете свои отношения как однонаправленные («А дружит с Б» != «Б дружит с А»). Если вы хотите рассматривать отношения как двунаправленные («A дружит с B» == «B дружит с A»), вот изменение кода:

WHILE @Degree > 0
BEGIN
    -- A to B direction
    INSERT INTO #ResultNieghbours
    SELECT DISTINCT f.friend
    FROM #ResultNieghbours rn
    JOIN Friendships f ON rn.person = f.person
    WHERE NOT EXISTS (
        SELECT n.person 
        FROM #ResultNieghbours n 
        WHERE n.person = f.friend
    )

    -- B to A direction
    INSERT INTO #ResultNieghbours
    SELECT DISTINCT f.person
    FROM #ResultNieghbours rn
    JOIN Friendships f ON rn.person = f.friend
    WHERE NOT EXISTS (
        SELECT n.person 
        FROM #ResultNieghbours n 
        WHERE n.person = f.person
    )

    SET @Degree = @Degree - 1
END

Другие вопросы по теме