Количество связных компонент графа в SQL

У меня есть график в моей базе данных PostgreSQL, для примера давайте определим его так:

CREATE TABLE nodes (node_id INTEGER);
CREATE TABLE roads (road_id INTEGER, nodes INTEGER[]);
INSERT INTO nodes VALUES (1), (2), (3), (4), (5);
INSERT INTO roads VALUES (1, {1, 2}), (2, {3, 4}));

Я хочу создать SQL-запрос, который возвращает количество подключенных компонентов графа, в в этом примере число равно 3, потому что узлы 1/2 соединены, 3/4 также, а 5 ни с чем не соединены.

Я попытался найти реализации find&union в SQL, но безрезультатно, затем я обратился к CTE, но я не могу сделать это самостоятельно, я думал что-то вроде этого:

WITH RECURSIVE cc(iterator_id, node_id, rank, iterator) AS
(
        SELECT row_number() OVER(), n.node_id, row_number() OVER (), 1 FROM nodes AS n
    UNION ALL
        # Something here that does the magic
)
SELECT
    COUNT(DISTINCT rank) AS no_of_cc
FROM
    cc,
    (SELECT COUNT(*) FROM nodes) AS last_iterator_id
WHERE iterator = last_iterator_id;

где на каждой итерации мы обновляем ранги строк, у которых iterator_id ‹= iterator. Мы повторяем до тех пор, пока iterator не сравняется с наибольшим iterator_id, но я не могу думать о рекурсивной части.

Можете ли вы помочь мне найти количество подключенных компонентов?


person Ritave    schedule 01.11.2015    source источник
comment
Есть ли у вас особая причина использовать простой SQL или решение может быть реализовано как процедура PL/pgSQL? Еще одна вещь, я вижу, вы пишете на питоне. В PostreSQL вы можете писать процедуры на PL/Python, который в основном является Python. плюс некоторые функции для доступа к отношениям с БД.   -  person Gabriel's Messanger    schedule 01.11.2015
comment
Наверное, просто потому, что я не знаю этих технологий и не знал, что они могут мне помочь. Тогда я попробую научиться и сделать это на PL/Python.   -  person Ritave    schedule 01.11.2015


Ответы (2)


Ты что теперь? Несмотря на то, что я рекомендовал вам написать процедуру хранения на PL/Python, позже я решил написать этот одиночный sql-запрос просто для развлечения. Вот что я сделал. Я использовал RECURSIVE CTE.

WITH RECURSIVE graph_search(node_id, connected_to, path, cycle) AS (
        SELECT node_id, connected_to, ARRAY[node_id], false FROM paths
    UNION 
        SELECT p.node_id, p.connected_to, gs.path || p.node_id, p.node_id=ANY(gs.path)
        FROM graph_search gs JOIN paths p ON gs.connected_to = p.node_id AND NOT gs.cycle
 ),
 paths AS (
    SELECT node_id, connected_to
    FROM (
        SELECT n.node_id, unnest(r.nodes) AS connected_to
        FROM nodes n JOIN roads r ON n.node_id = ANY(r.nodes)
    ) sub
    WHERE node_id <> connected_to
 ) 
SELECT count(DISTINCT component)
FROM (
        SELECT node_id,
               array_agg(DISTINCT reachable_node ORDER BY reachable_node) as component
        FROM (
            SELECT node_id, unnest(path) as reachable_node from graph_search 
        ) sub
        GROUP BY node_id
    UNION ALL /*need to append lonely nodes - they are components for themselves*/
        SELECT node_id, ARRAY[node_id]
        FROM nodes
        WHERE node_id NOT IN (SELECT node_id from paths)
) sub;
  • Во-первых, мне нужно другое представление самого графика. Обычный CTE с именем paths создает двухстолбцовую таблицу с парами связанных узлов.
  • Затем я просто немного изменил пример из руководства PostgreSQL, так что у меня есть список узлов и всех узлов, достижимых из него.
  • Агрегация дает мне компоненты графа.
  • В конце я подсчитываю отдельные компоненты.
person Gabriel's Messanger    schedule 01.11.2015
comment
Postgres съедает весь мой 1 ГБ оперативной памяти при попытке выполнить этот запрос, а затем разрывает соединение и останавливает запрос. Вы уверены, что это не бесконечный цикл? Или, может быть, я могу заставить Postgres работать на диске вместо того, чтобы взрывать оперативную память? Весь набор данных составляет 485 МБ, если это поможет - person Ritave; 02.11.2015
comment
Подождите, здесь у каждого узла есть множество друзей. В худшем случае (все узлы как один подключенный компонент) это 235 ГБ данных, которые Postgres пытается загрузить. Не будет :Р - person Ritave; 02.11.2015
comment
Я уверен, что нет бесконечного цикла (этот запрос может обрабатывать даже циклические графики), но размер набора данных является важным фактором, как и производительность машины. Алгоритм здесь довольно наивен, потому что такого рода задачи не являются фаворитами SQL :) Хотя я не тестировал его на таком большом наборе данных. У меня есть некоторое представление об оптимизации, если я придумаю что-то полезное, я напишу это здесь позже. Кстати, каждое соединение postgres имеет ограниченную песочницу RAM для себя. Если он слишком мал для работы, сервер начинает использовать диск, что действительно увеличивает время выполнения. - person Gabriel's Messanger; 02.11.2015

Решение выше не будет работать, если количество узлов слишком велико.

Наиболее эффективное решение (если у вас достаточно оперативной памяти для чтения всех данных) — это считывание данных в память с помощью таких языков, как C или C++, и выполнение вычислений там.

Но если размер данных слишком велик и у вас нет выбора, то, вероятно, вы можете сделать это следующим образом:

(реализация plpgssql, предполагая, что у нас есть табличные дороги (node1, node2))

CREATE TABLE node AS
  SELECT DISTINCT node1 AS id, node1 AS color
  FROM roads

  CREATE OR REPLACE FUNCTION merge_node()
  RETURNS VOID
AS
$$
DECLARE
left_to_do INT := 1;
counter INT :=1;
row record;
BEGIN
    DROP TABLE IF EXISTS t;
CREATE TEMP TABLE t  (
    node1 INT,
    prev INT,
    next INT
);

    WHILE left_to_do > 0
    LOOP
        WITH joined_table AS (
            SELECT roads.node1,
                   MAX (v1.color) AS prev,
                   MAX (v2.color) AS next
            FROM roads
            JOIN node v1 ON roads.node1 = v1.id
            JOIN node v2 ON roads.node2 = v2.id
            GROUP BY roads.node1
        )
        INSERT INTO t (node1, prev, next)
        SELECT node1,
               prev,
               next
        FROM joined_table
        WHERE prev < next;
        SELECT COUNT(*) INTO left_to_do FROM t;
        UPDATE node color
        SET color = t.next
        FROM t
        WHERE color.id = t.node1;
        DELETE FROM t;
        counter := counter + 1;
    END LOOP;
END;
$$
LANGUAGE plpgsql;

это должно работать лучше, если степень узла низка по сравнению с количеством узлов. протестировал его на графе с 2,4 миллионами узлов и 24 миллионами ребер, и это занимает около 30-60 минут с индексами. (Для сравнения, в С++ требуется 2,5 минуты, большую часть времени он считывает данные из csv/записывает данные в csv)

person Blomex    schedule 12.12.2019