Посещение ориентированного графа, как если бы оно было ненаправленным, с использованием рекурсивного запроса - PullRequest
11 голосов
/ 07 января 2012

Мне нужна ваша помощь о посещении ориентированного графа, хранящегося в базе данных.

Рассмотрим следующий ориентированный граф

1->2 
2->1,3 
3->1

В таблице хранятся эти отношения:

create database test;
\c test;

create table ownership (
    parent bigint,
    child  bigint,
    primary key (parent, child)
);

insert into ownership (parent, child) values (1, 2);
insert into ownership (parent, child) values (2, 1);
insert into ownership (parent, child) values (2, 3);
insert into ownership (parent, child) values (3, 1);

Я хотел бы извлечь все полусвязанные ребра (т.е. соединенные ребра, игнорируя направление) графа, достижимого из узла .Т.е., если я начну с parent = 1, я бы хотел получить следующий вывод

1,2
2,1
2,3
3,1

Я использую postgresql .

Я изменилпример в руководстве Postgres , который объясняет рекурсивные запросы, и я адаптировал условие соединения для перехода "вверх" и "вниз" (при этом я игнорирую указания).Мой запрос следующий:

\c test

WITH RECURSIVE graph(parent, child, path, depth, cycle) AS (
SELECT o.parent, o.child, ARRAY[ROW(o.parent, o.child)], 0, false
    from ownership o
    where o.parent = 1
UNION ALL
SELECT 
    o.parent, o.child,
    path||ROW(o.parent, o.child), 
    depth+1, 
    ROW(o.parent, o.child) = ANY(path)
    from 
        ownership o, graph g
    where 
        (g.parent = o.child or g.child = o.parent) 
        and not cycle

)
select  g.parent, g.child, g.path, g.cycle
from
    graph g

его вывод выглядит следующим образом:

 parent | child |               path                | cycle 
--------+-------+-----------------------------------+-------
      1 |     2 | {"(1,2)"}                         | f
      2 |     1 | {"(1,2)","(2,1)"}                 | f
      2 |     3 | {"(1,2)","(2,3)"}                 | f
      3 |     1 | {"(1,2)","(3,1)"}                 | f
      1 |     2 | {"(1,2)","(2,1)","(1,2)"}         | t
      1 |     2 | {"(1,2)","(2,3)","(1,2)"}         | t
      3 |     1 | {"(1,2)","(2,3)","(3,1)"}         | f
      1 |     2 | {"(1,2)","(3,1)","(1,2)"}         | t
      2 |     3 | {"(1,2)","(3,1)","(2,3)"}         | f
      1 |     2 | {"(1,2)","(2,3)","(3,1)","(1,2)"} | t
      2 |     3 | {"(1,2)","(2,3)","(3,1)","(2,3)"} | t
      1 |     2 | {"(1,2)","(3,1)","(2,3)","(1,2)"} | t
      3 |     1 | {"(1,2)","(3,1)","(2,3)","(3,1)"} | t
(13 rows)

У меня есть проблема : запрос извлекает много одинаковых ребер многораз , так как они достигаются по разным путям, и я хотел бы избежать этого.Если я изменю внешний запрос на

select  distinct g.parent, g.child from graph

, то получу желаемый результат, но неэффективность останется в запросе WITH, поскольку выполняются ненужные объединения. Итак, есть ли решение для извлечения достижимых ребер графа в БД, начиная с заданного, без использования отличных?

У меня также есть другая проблема (этоодин из них решен, смотрите внизу): как видно из выходных данных, циклы останавливаются только при достижении узла во второй раз .Т.е. у меня (1,2) (2,3) (1,2). Я бы хотел остановить цикл перед повторным циклом по последнему узлу, то есть, имея (1,2) (2,3). Я попытался изменить условие where следующим образом

where
    (g.parent = o.child or g.child = o.parent) 
    and (ROW(o.parent, o.child) <> any(path))
    and not cycle

, чтобы избежатьпосещение уже посещенных ребер, но это не работает, и я не могу понять, почему ((ROW(o.parent, o.child) <> any(path)) следует избегать зацикливания, прежде чем снова перейти на зацикленное ребро, но это не работает). Как я могу остановить цикл за один шаг до того, как узел закроет цикл?

Редактировать : как предложил Данихп, решитьВторая проблема, которую я использовал

where
    (g.parent = o.child or g.child = o.parent) 
    and not (ROW(o.parent, o.child) = any(path))
    and not cycle

, и теперь вывод не содержит циклов.Строки перешли с 13 на 6, но у меня все еще есть дубликаты, поэтому основная (первая) проблема извлечения всех ребер без дубликатов и без четких еще жива.Токовый выход с and not ROW

 parent | child |           path            | cycle 
--------+-------+---------------------------+-------
      1 |     2 | {"(1,2)"}                 | f
      2 |     1 | {"(1,2)","(2,1)"}         | f
      2 |     3 | {"(1,2)","(2,3)"}         | f
      3 |     1 | {"(1,2)","(3,1)"}         | f
      3 |     1 | {"(1,2)","(2,3)","(3,1)"} | f
      2 |     3 | {"(1,2)","(3,1)","(2,3)"} | f
(6 rows)

Edit # 2: : следуя советам Эрвина Брандштеттера, я изменил свой запрос, но если я не ошибаюсь, предложенный запрос дает БОЛЬШЕ строк, чем мое (сравнение ROW все еще там, поскольку мне кажется более ясным, даже я понял, что сравнение строк будет более эффективным).Используя новый запрос, я получаю 20 строк, а мой - 6 строк

WITH RECURSIVE graph(parent, child, path, depth) AS (
SELECT o.parent, o.child, ARRAY[ROW(o.parent, o.child)], 0
    from ownership o
    where 1 in (o.child, o.parent)
UNION ALL
SELECT 
    o.parent, o.child,
    path||ROW(o.parent, o.child), 
    depth+1
    from 
        ownership o, graph g
    where 
        g.child in (o.parent, o.child) 
        and ROW(o.parent, o.child) <> ALL(path)

)
select  g.parent, g.child from graph g

Редактировать 3 : так, как указал Эрвин Брандштеттер, последний запросвсе еще был неправ, а правильного можно найти в его ответе.

Когда я отправил свой первый запрос, я не понял, что пропустил некоторые объединения, как это происходит в следующем случае: если я начинаю с узла 3, БД выбирает строки (2,3) и(3,1).Затем на первом индуктивном шаге запроса выбираются из этих строк строки (1,2), (2,3) и (3,1), пропущенные строки (2,1), которые должны быть включены в результат, как концептуально алгоритмбудет означать ((2,1) "рядом" (3,1))

Когда я пытался адаптировать пример в руководстве к Postgresql, я был прав, пытаясь соединить родителя и потомка ownership, но я ошибалсяне сохраняйте значение graph, которое нужно было объединять на каждом шаге.

Похоже, что эти типы запросов генерируют различный набор строк в зависимости от начального узла (т.е. в зависимости от набора строк, выбранного вбазовый шаг).Поэтому я думаю, что было бы полезно выбрать только одну строку, содержащую начальный узел в базовом шаге, поскольку в любом случае вы получите любой другой «соседний» узел.

1 Ответ

6 голосов
/ 07 января 2012

Может работать так:

WITH RECURSIVE graph AS (
    SELECT parent
          ,child
          ,',' || parent::text || ',' || child::text || ',' AS path
          ,0 AS depth
    FROM   ownership
    WHERE  parent = 1

    UNION ALL
    SELECT o.parent
          ,o.child
          ,g.path || o.child || ','
          ,g.depth + 1
    FROM   graph g
    JOIN   ownership o ON o.parent = g.child
    WHERE  g.path !~~ ('%,' || o.parent::text || ',' || o.child::text || ',%')
    )
SELECT  *
FROM    graph

Вы упомянули производительность, поэтому я оптимизировал в этом направлении.

Основные моменты:

  • Траверсграфик только в определенном направлении .

  • Нет необходимости в столбце cycle, вместо этого сделайте его условием исключения.Еще один шаг впереди.Это также прямой ответ на:

Как я могу остановить цикл за один шаг до того, как узел закроет цикл?

  • Используйте строку для записи пути.Меньше и быстрее, чем массив строк.Все еще содержит всю необходимую информацию.Может измениться с очень большими числами bigint.

  • Проверка циклов с оператором LIKE (~~) должна быть намного быстрее.

  • Если вы не ожидаете более 2147483647 строк с течением времени, используйте простые integer столбцы вместо bigint.Меньше и быстрее.

  • Убедитесь, что index on parent.Индекс на child не имеет значения для моего запроса.(За исключением вашего оригинала, где вы пересекаете края в обоих направлениях.)

  • Для огромных графиков Я бы переключился на процедуру plpgsql ,где вы можете сохранить путь в качестве временной таблицы с одной строкой на шаг и соответствующим index .Хотя это немного накладные расходы, которые окупятся огромными графиками.


Проблемы в исходном запросе:

WHERE (g.parent = o.child or g.child = o.parent) 

В любой точке процесса есть только одна конечная точка вашего обхода.Когда вы запускаете ориентированный граф в обоих направлениях, конечная точка может быть родительской или дочерней, но не обе.Вы должны сохранить конечную точку каждого шага, а затем:

WHERE g.child IN (o.parent, o.child) 

Нарушение направления также ставит под сомнение ваше начальное условие:

WHERE parent = 1

Должно быть

WHERE 1 IN (parent, child)

И две строки (1,2) и (2,1) фактически дублируют этот путь ...


Дополнительное решение после комментария

  • Игнорировать направление
  • Все еще проходите любой край только один раз за путь.
  • Используйте ARRAY для пути
  • Сохранение исходного направления в пути, а не фактического направления.

Примечание, что таким образом (2,1) и (1,2) являются эффективными дубликатами, но оба могут использоваться по одному и тому же пути.

Я ввожу столбец leaf, в котором сохраняется фактическая конечная точка каждого шага.

WITH RECURSIVE graph AS (
    SELECT CASE WHEN parent = 1 THEN child ELSE parent END AS leaf
          ,ARRAY[ROW(parent, child)] AS path
          ,0 AS depth
    FROM   ownership
    WHERE  1 in (child, parent)

    UNION ALL
    SELECT CASE WHEN o.parent = g.leaf THEN o.child ELSE o.parent END -- AS leaf
          ,path || ROW(o.parent, o.child) -- AS path
          ,depth + 1 -- AS depth
    FROM   graph g
    JOIN   ownership o ON g.leaf in (o.parent, o.child) 
    AND    ROW(o.parent, o.child) <> ALL(path)
    )
SELECT *
FROM   graph
...