У меня есть следующая таблица в 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-й степени):
Я не уверен, как это сделать в 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, в которой нет рекурсивных функций.
Спасибо!
@ Ларс Дж. Олсен: Большое спасибо за добрые слова! Я пытался решить это сам здесь: stackoverflow.com/questions/78273876/… .... Я подошел близко, но проблемы все еще есть. Есть идеи?


Рассмотрим этот рекурсивный пример.
Есть условие 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
Результат для всех
Для person='person10' cte r обходит граф по всем путям.
AllFriends для person10 (повышение уровня 2) и количество путей для друга
Там 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
Пример для SQL-сервера. Практически без изменений (varchar(1000)->nchar(1000)) подходит для MySQL.
К сожалению, я не знаю, как реализовать расширенную рекурсию в postgresql, например (выбрать.. объединить все, выбрать... объединить все, выбрать...)
Мы можем сделать это в postgresql, усложнив соединение привязки и таблицы.
Обновление1. Если в графе есть циклы, мы должны проверить - мы уже прошли этот узел - есть ли узел в path? Такая проверка несколько зависит от используемой СУБД (instr,patindex...). В этом случае путь является обязательным.
#престиж^2 за этот ответ, я ошеломлен им, и мне придется проверить его самому, просто чтобы насладиться глубиной ясности, которую он обеспечивает!
@ValNik: большое спасибо за ответ! Я какое-то время уезжал из страны и только что вернулся..
Я пытался решить это сам здесь: stackoverflow.com/questions/78273876/… .... Я подошел близко, но проблемы все еще есть. Есть идеи?
Давайте сначала ответим на ваш второй вопрос, потому что вы можете получить результат по первому вопросу, используя результат второго вопроса.
Чтобы ответить на ваш второй вопрос, вы можете написать функцию, которая выполняет следующие действия:
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
#спасибо рычагу ясности в вопросе!!!