Статьи

Neo4j: Cypher — удаление дублирующих узлов

У меня была проблема с графиком, над которым я недавно работал, когда мне удавалось создавать дублирующиеся узлы, потому что я не применял никаких уникальных ограничений .

Я хотел удалить дубликаты и наткнулся на отличный пост Джимми Рутса, в котором показаны некоторые способы сделать это.

Давайте сначала создадим граф с несколькими дублирующимися узлами для игры:

01
02
03
04
05
06
07
08
09
10
UNWIND range(0, 100) AS id
CREATE (p1:Person {id: toInteger(rand() * id)})
MERGE (p2:Person {id: toInteger(rand() * id)})
MERGE (p3:Person {id: toInteger(rand() * id)})
MERGE (p4:Person {id: toInteger(rand() * id)})
CREATE (p1)-[:KNOWS]->(p2)
CREATE (p1)-[:KNOWS]->(p3)
CREATE (p1)-[:KNOWS]->(p4)
  
Added 173 labels, created 173 nodes, set 173 properties, created 5829 relationships, completed after 408 ms.

Как нам найти дубликаты узлов?

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
MATCH (p:Person)
WITH p.id as id, collect(p) AS nodes
WHERE size(nodes) >  1
RETURN [ n in nodes | n.id] AS ids, size(nodes)
ORDER BY size(nodes) DESC
LIMIT 10
  
╒══════════════════════╤═════════════╕
"ids"                 "size(nodes)"
╞══════════════════════╪═════════════╡
│[1,1,1,1,1,1,1,1]     │8            │
├──────────────────────┼─────────────┤
│[0,0,0,0,0,0,0,0]     │8            │
├──────────────────────┼─────────────┤
│[17,17,17,17,17,17,17]│7            │
├──────────────────────┼─────────────┤
│[4,4,4,4,4,4,4]       │7            │
├──────────────────────┼─────────────┤
│[2,2,2,2,2,2]         │6            │
├──────────────────────┼─────────────┤
│[5,5,5,5,5,5]         │6            │
├──────────────────────┼─────────────┤
│[19,19,19,19,19,19]   │6            │
├──────────────────────┼─────────────┤
│[11,11,11,11,11]      │5            │
├──────────────────────┼─────────────┤
│[25,25,25,25,25]      │5            │
├──────────────────────┼─────────────┤
│[43,43,43,43,43]      │5            │
└──────────────────────┴─────────────┘

Давайте рассмотрим всех людей с «id: 1» и выясним, сколько у них отношений. Наш план — сохранить узел, который имеет наибольшее количество соединений, и избавиться от других.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
MATCH (p:Person)
WITH p.id as id, collect(p) AS nodes
WHERE size(nodes) >  1
WITH nodes ORDER BY size(nodes) DESC
LIMIT 1
UNWIND nodes AS n
RETURN n.id, id(n) AS internalId, size((n)--()) AS rels
ORDER BY rels DESC
  
╒══════╤════════════╤══════╕
"n.id""internalId""rels"
╞══════╪════════════╪══════╡
│1     │175         │1284  │
├──────┼────────────┼──────┤
│1     │184         │721   │
├──────┼────────────┼──────┤
│1     │180         │580   │
├──────┼────────────┼──────┤
│1     │2           │391   │
├──────┼────────────┼──────┤
│1     │195         │361   │
├──────┼────────────┼──────┤
│1     │199         │352   │
├──────┼────────────┼──────┤
│1     │302         │5     │
├──────┼────────────┼──────┤
│1     │306         │1     │
└──────┴────────────┴──────┘

Таким образом, в этом примере мы хотим сохранить узел с 210 связями и удалить остальные.

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
MATCH (p:Person)
WITH p
ORDER BY p.id, size((p)--()) DESC
WITH p.id as id, collect(p) AS nodes
WHERE size(nodes) >  1
RETURN [ n in nodes | {id: n.id,rels: size((n)--()) } ] AS ids, size(nodes)
ORDER BY size(nodes) DESC
LIMIT 10
  
╒══════════════════════════════════════════════════════════════════════╤═════════════╕
"ids"                                                                 "size(nodes)"
╞══════════════════════════════════════════════════════════════════════╪═════════════╡
│[{"id":1,"rels":1284},{"id":1,"rels":721},{"id":1,"rels":580},{"id":1,│8            │
"rels":391},{"id":1,"rels":361},{"id":1,"rels":352},{"id":1,"rels":5},│             │
│{"id":1,"rels":1}]                                                    │             │
├──────────────────────────────────────────────────────────────────────┼─────────────┤
│[{"id":0,"rels":2064},{"id":0,"rels":2059},{"id":0,"rels":1297},{"id":│8            │
│0,"rels":1124},{"id":0,"rels":995},{"id":0,"rels":928},{"id":0,"rels":│             │
│730},{"id":0,"rels":702}]                                             │             │
├──────────────────────────────────────────────────────────────────────┼─────────────┤
│[{"id":17,"rels":153},{"id":17,"rels":105},{"id":17,"rels":81},{"id":1│7            │
│7,"rels":31},{"id":17,"rels":15},{"id":17,"rels":14},{"id":17,"rels":1│             │
│}]                                                                    │             │
├──────────────────────────────────────────────────────────────────────┼─────────────┤
│[{"id":4,"rels":394},{"id":4,"rels":320},{"id":4,"rels":250},{"id":4,"│7            │
│rels":201},{"id":4,"rels":162},{"id":4,"rels":162},{"id":4,"rels":14}]│             │
├──────────────────────────────────────────────────────────────────────┼─────────────┤
│[{"id":2,"rels":514},{"id":2,"rels":329},{"id":2,"rels":318},{"id":2,"│6            │
│rels":241},{"id":2,"rels":240},{"id":2,"rels":2}]                     │             │
├──────────────────────────────────────────────────────────────────────┼─────────────┤
│[{"id":5,"rels":487},{"id":5,"rels":378},{"id":5,"rels":242},{"id":5,"│6            │
│rels":181},{"id":5,"rels":158},{"id":5,"rels":8}]                     │             │
├──────────────────────────────────────────────────────────────────────┼─────────────┤
│[{"id":19,"rels":153},{"id":19,"rels":120},{"id":19,"rels":84},{"id":1│6            │
│9,"rels":53},{"id":19,"rels":45},{"id":19,"rels":1}]                  │             │
├──────────────────────────────────────────────────────────────────────┼─────────────┤
│[{"id":11,"rels":222},{"id":11,"rels":192},{"id":11,"rels":172},{"id":│5            │
│11,"rels":152},{"id":11,"rels":89}]                                   │             │
├──────────────────────────────────────────────────────────────────────┼─────────────┤
│[{"id":25,"rels":133},{"id":25,"rels":107},{"id":25,"rels":98},{"id":2│5            │
│5,"rels":15},{"id":25,"rels":2}]                                      │             │
├──────────────────────────────────────────────────────────────────────┼─────────────┤
│[{"id":43,"rels":92},{"id":43,"rels":85},{"id":43,"rels":9},{"id":43,"│5            │
│rels":5},{"id":43,"rels":1}]                                          │             │
└──────────────────────────────────────────────────────────────────────┴─────────────┘

Теперь пришло время удалить дубликаты:

1
2
3
4
5
6
7
8
9
MATCH (p:Person)
WITH p
ORDER BY p.id, size((p)--()) DESC
WITH p.id as id, collect(p) AS nodes
WHERE size(nodes) >  1
UNWIND nodes[1..] AS n
DETACH DELETE n
  
Deleted 143 nodes, deleted 13806 relationships, completed after 29 ms.

Теперь, если мы запустим наш дубликат запроса:

1
2
3
4
5
6
7
8
MATCH (p:Person)
WITH p.id as id, collect(p) AS nodes
WHERE size(nodes) >  1
RETURN [ n in nodes | n.id] AS ids, size(nodes)
ORDER BY size(nodes) DESC
LIMIT 10
  
(no changes, no records)

Что делать, если мы удалим предложение WHERE?

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
MATCH (p:Person)
WITH p.id as id, collect(p) AS nodes
RETURN [ n in nodes | n.id] AS ids, size(nodes)
ORDER BY size(nodes) DESC
LIMIT 10
  
╒═════╤═════════════╕
"ids""size(nodes)"
╞═════╪═════════════╡
│[23] │1            │
├─────┼─────────────┤
│[86] │1            │
├─────┼─────────────┤
│[77] │1            │
├─────┼─────────────┤
│[59] │1            │
├─────┼─────────────┤
│[50] │1            │
├─────┼─────────────┤
│[32] │1            │
├─────┼─────────────┤
│[41] │1            │
├─────┼─────────────┤
│[53] │1            │
├─────┼─────────────┤
│[44] │1            │
├─────┼─────────────┤
│[8]  │1            │
└─────┴─────────────┘

Ура, больше нет дубликатов! Наконец, давайте проверим, сохранили ли мы ожидаемый узел. Мы ожидаем, что его внутренний идентификатор равен 175:

1
2
3
4
5
6
7
8
MATCH (p:Person {id: 1})
RETURN size((p)--()), id(p) AS internalId
  
╒═══════════════╤════════════╕
"size((p)--())""internalId"
╞═══════════════╪════════════╡
│242            │175         │
└───────────────┴────────────┘

Что это делает! Отношений намного меньше, чем раньше, потому что многие из этих отношений должны были дублировать узлы, которые мы сейчас удалили.

Если мы хотим пойти еще дальше, мы можем «объединить» отношения дубликата узла с узлами, которые мы сохранили, но это для другого поста!

Опубликовано на Java Code Geeks с разрешения Марка Нидхэма, партнера нашей программы JCG . Смотрите оригинальную статью здесь: Neo4j: Cypher — Удаление дублирующихся узлов

Мнения, высказанные участниками Java Code Geeks, являются их собственными.