SQL группирует интересные/перекрывающиеся строки

У меня есть следующая таблица в Postgres, в которой есть перекрывающиеся данные в двух столбцах a_sno и b_sno.

create table data
( a_sno integer not null,  
  b_sno integer not null,
  PRIMARY KEY (a_sno,b_sno)
);

insert into data (a_sno,b_sno) values
  ( 4, 5 )
, ( 5, 4 )
, ( 5, 6 )
, ( 6, 5 )
, ( 6, 7 )
, ( 7, 6 )
, ( 9, 10)
, ( 9, 13)
, (10, 9 )
, (13, 9 )
, (10, 13)
, (13, 10)
, (10, 14)
, (14, 10)
, (13, 14)
, (14, 13)
, (11, 15)
, (15, 11);

Как видно из первых 6 строк значения данных 4,5,6 и 7 в двух столбцах пересекаются/перекрываются, что необходимо разделить на группу. То же самое касается рядов 7-16 и рядов 17-18, которые будут помечены как группа 2 и 3 соответственно.

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

group | value
------+------
1     | 4
1     | 5
1     | 6
1     | 7
2     | 9
2     | 10
2     | 13
2     | 14
3     | 11
3     | 15

person bogeyman    schedule 19.04.2015    source источник


Ответы (3)


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

Простой случай

Все соединения можно выстроить в одной восходящей последовательности и усложнить, как я добавил в fiddle невозможно, мы можем использовать это решение без дубликатов в rCTE:

Я начинаю с получения минимума a_sno на группу с минимальным связанным b_sno:

SELECT row_number() OVER (ORDER BY a_sno) AS grp
     , a_sno, min(b_sno) AS b_sno
FROM   data d
WHERE  a_sno < b_sno
AND    NOT EXISTS (
   SELECT 1 FROM data
   WHERE  b_sno = d.a_sno
   AND    a_sno < b_sno
   )
GROUP  BY a_sno;

Для этого нужен только один уровень запроса, поскольку оконная функция может быть построена на агрегате:

Результат:

grp  a_sno  b_sno
1    4      5
2    9      10
3    11     15

Я избегаю ответвлений и дублированных (умноженных) строк — потенциально намного дороже с длинными цепочками. Я использую ORDER BY b_sno LIMIT 1 в коррелированном подзапросе, чтобы заставить это летать в рекурсивном CTE.

Ключом к производительности является соответствующий индекс, который уже присутствует в ограничении PK PRIMARY KEY (a_sno,b_sno): а не наоборот (b_sno, a_sno):

WITH RECURSIVE t AS (
   SELECT row_number() OVER (ORDER BY d.a_sno) AS grp
        , a_sno, min(b_sno) AS b_sno  -- the smallest one
   FROM   data d
   WHERE  a_sno < b_sno
   AND    NOT EXISTS (
      SELECT 1 FROM data
      WHERE  b_sno = d.a_sno
      AND    a_sno < b_sno
      )
   GROUP  BY a_sno
   )

, cte AS (
   SELECT grp, b_sno AS sno FROM t

   UNION ALL
   SELECT c.grp
       , (SELECT b_sno  -- correlated subquery
          FROM   data
          WHERE  a_sno = c.sno
          AND    a_sno < b_sno
          ORDER  BY b_sno
          LIMIT  1)
   FROM   cte  c
   WHERE  c.sno IS NOT NULL
   )
SELECT * FROM cte
WHERE  sno IS NOT NULL   -- eliminate row with NULL
UNION  ALL               -- no duplicates
SELECT grp, a_sno FROM t
ORDER  BY grp, sno;

Менее простой случай

Все узлы могут быть достигнуты в порядке возрастания с одной или несколькими ветвями от корня (наименьший sno).

На этот раз получите все большие sno и удалите дубликаты узлов, которые могут посещаться несколько раз, с UNION в конце:

WITH RECURSIVE t AS (
   SELECT rank() OVER (ORDER BY d.a_sno) AS grp
        , a_sno, b_sno  -- get all rows for smallest a_sno
   FROM   data d
   WHERE  a_sno < b_sno
   AND    NOT EXISTS (
      SELECT 1 FROM data
      WHERE  b_sno = d.a_sno
      AND    a_sno < b_sno
      )
   )   
, cte AS (
   SELECT grp, b_sno AS sno FROM t

   UNION ALL
   SELECT c.grp, d.b_sno
   FROM   cte  c
   JOIN   data d ON d.a_sno = c.sno
                AND d.a_sno < d.b_sno  -- join to all connected rows
   )
SELECT grp, sno FROM cte
UNION                     -- eliminate duplicates
SELECT grp, a_sno FROM t  -- add first rows
ORDER  BY grp, sno;

В отличие от первого решения, здесь мы не получаем последнюю строку с NULL (из-за коррелированного подзапроса).

Оба должны работать очень хорошо, особенно с длинными цепочками/множеством ответвлений. Результат по желанию:

SQL Fiddle (с добавленными строками для демонстрации сложности).

Неориентированный граф

Если есть локальные минимумы, которые не могут быть достигнуты из корня при обходе по возрастанию, приведенные выше решения не будут работать. В этом случае рассмотрим решение Farhęg.

person Erwin Brandstetter    schedule 20.04.2015
comment
отличный ответ с отличными объяснениями, мне особенно нравятся такие объяснения, которые НАМНОГО лучше - person null; 20.04.2015
comment
а также умный момент с этим ответом заключается в том, что рекурсивный cte не участвует в цикле, поэтому нет необходимости использовать флаг цикла и массив путей - person null; 20.04.2015
comment
@Farhęg: Однако я только что понял, что, возможно, переоптимизировал с помощью LIMIT 1. Запрос может пропустить членов группы, в зависимости от фактических характеристик данных ... добавление корректирующего ... - person Erwin Brandstetter; 20.04.2015
comment
Эрвин, ваше решение работает лучше, чем два других решения с точки зрения производительности. Однако только для небольших наборов данных до 50 000 строк. Я применяю эту логику к таблице строк 2M, и производительность запроса очень плохая. Есть ли способ улучшить производительность? Ваша помощь очень ценится. - person bogeyman; 03.05.2015
comment
@bogeyman: Какой из моих ответов соответствует вашему сценарию? Я сделал предположения и дал несколько ответов, потому что вопрос не содержал всех деталей. Упомянутые вами кардинальности должны быть в вопросе. Для оптимизации производительности необходимо предоставить полную информацию. Рассмотрите рекомендации здесь. Обновите свой вопрос, указав недостающие детали, или, если это изменит характер вопроса, начните новый. Наконец, если мой ответ работает лучше всего, почему другой все еще принимается? - person Erwin Brandstetter; 03.05.2015
comment
Эрвин, Спасибо. Пометка вашего вопроса как ответа. Я выбрал предыдущий, так как он был первым, который изначально работал. Но после нескольких попыток я обнаружил, что первые два решения не могут обрабатывать более 3000 строк. Код, который я использую, - это ваш случай Less Simple. - person bogeyman; 03.05.2015
comment
Эрвин, Не обращай внимания. Я попробовал ваше первое решение. Это работает как шарм. Я не обратил особого внимания на вашу заметку. В отличие от первого решения, здесь мы не получаем последнюю строку с NULL. В этом была проблема. Спасибо за вашу помощь. - person bogeyman; 03.05.2015

Хочу сказать по другому, может пригодится, сделать можно в 2 шага:

1. возьмите max(sno) за каждую группу:

  select q.sno,
         row_number() over(order by q.sno) gn
  from(
    select distinct d.a_sno sno
    from data d
    where not exists (
      select b_sno
      from data
      where b_sno=d.a_sno
      and a_sno>d.a_sno
     )
    )q

результат:

sno gn
7   1
14  2
15  3

2. используйте recursive cte, чтобы найти всех связанных участников в группах:

with recursive cte(sno,gn,path,cycle)as(
  select q.sno,
         row_number() over(order by q.sno) gn,
         array[q.sno],false
  from(
    select distinct d.a_sno sno
    from data d
    where not exists (
      select b_sno
      from data
      where b_sno=d.a_sno
      and a_sno>d.a_sno
     )
    )q
  union all
  select d.a_sno,c.gn,
         d.a_sno || c.path,
         d.a_sno=any(c.path)
  from data d
  join cte c on d.b_sno=c.sno 
  where not cycle
 )
 select distinct gn,sno from cte
 order by gn,sno

Результат:

gn  sno
1   4
1   5
1   6
1   7
2   9
2   10
2   13
2   14
3   11
3   15

вот демонстрация того, что я сделал.

person null    schedule 20.04.2015
comment
Очень умный запрос. Мне это нравится. Я придумал несколько похожее решение, без лишних строк и DISTINCT шагов. - person Erwin Brandstetter; 20.04.2015
comment
Я вижу, вы сделали это умнее, ваш cte не нуждается в флаге цикла и массиве путей. - person null; 20.04.2015

Вот начало, которое может дать некоторые идеи о подходе. Рекурсивный запрос начинается с a_sno каждой записи, а затем пытается следовать по пути b_sno, пока не достигнет конца или не сформирует цикл. Путь представлен массивом из sno целых чисел.

Функция unnest разбивает массив на строки, поэтому значение sno сопоставляется с массивом путей, например:

4, {6, 5, 4}

будет преобразован в строку для каждого значения в массиве:

4, 6
4, 5
4, 4

Затем array_agg меняет операцию, объединяя значения обратно в путь, но избавляясь от дубликатов и упорядочения.

Теперь каждый a_sno связан с путем, и путь образует группу. dense_rank можно использовать для сопоставления группировки (кластера) с числовым значением.

SELECT array_agg(DISTINCT map ORDER BY map) AS cluster
      ,sno
  FROM ( WITH RECURSIVE x(sno, path, cycle) AS (
             SELECT a_sno, ARRAY[a_sno], false FROM  data
             UNION ALL
             SELECT b_sno, path || b_sno, b_sno = ANY(path)
               FROM data, x
               WHERE a_sno = x.sno
                 AND NOT cycle
         )
         SELECT sno, unnest(path) AS map FROM x ORDER BY 1
       ) y
  GROUP BY sno
  ORDER BY 1, 2

Выход:

   cluster    | sno 
--------------+-----
 {4,5,6,7}    |   4
 {4,5,6,7}    |   5
 {4,5,6,7}    |   6
 {4,5,6,7}    |   7
 {9,10,13,14} |   9
 {9,10,13,14} |  10
 {9,10,13,14} |  13
 {9,10,13,14} |  14
 {11,15}      |  11
 {11,15}      |  15
(10 rows)

Оберните это еще раз для рейтинга:

SELECT dense_rank() OVER(order by cluster) AS rank
      ,sno
  FROM (
    SELECT array_agg(DISTINCT map ORDER BY map) AS cluster
          ,sno
      FROM ( WITH RECURSIVE x(sno, path, cycle) AS (
                 SELECT a_sno, ARRAY[a_sno], false FROM  data
                 UNION ALL
                 SELECT b_sno, path || b_sno, b_sno = ANY(path)
                   FROM data, x
                   WHERE a_sno = x.sno
                     AND NOT cycle
             )
             SELECT sno, unnest(path) AS map FROM x ORDER BY 1
           ) y
      GROUP BY sno
      ORDER BY 1, 2
) z

Выход:

 rank | sno 
------+-----
    1 |   4
    1 |   5
    1 |   6
    1 |   7
    2 |   9
    2 |  10
    2 |  13
    2 |  14
    3 |  11
    3 |  15
(10 rows)
person Glenn    schedule 19.04.2015
comment
Большое спасибо, Гленн. Работает отлично. - person bogeyman; 20.04.2015
comment
Он выполняет свою работу, но если количество строк превышает несколько, это может дорого обойтись. - person Erwin Brandstetter; 20.04.2015