neo4j - part5 - advanced cypher

9 분 소요

intro

Getting More Out of Queries(번역)

Filtering queries using WHERE

  • WHERE를 사용하면, 좀 더 원하는 조건에 맞춰서 쿼리를 작성할 수 있음. 아래가 기본적인 질의문이라면, 이를 같은 방식으로 WHERE문에 작성해서 같은 결과를 도출할 수 있음. 하지만, 이는 마치 SQL에서의 full-join과 같이, 계산량이 좀 더 늘어날 수 있음.
MATCH (p:Person)-[:ACTED_IN]->(m:Movie {released: 2008})
RETURN p, m
MATCH (p:Person)-[:ACTED_IN]->(m:Movie)
WHERE m.released = 2008
RETURN p, m
  • 여러 조건을 한번에 붙이고 싶으면 다음과 같이 수행하는 것이 좋습니다.
MATCH (p:Person)-[:ACTED_IN]->(m:Movie)
WHERE m.released = 2008 OR m.released = 2009
RETURN p, m
  • 그리고, 여기에, 당연하지만, AND, OR, XOR, NOT를 사용해서 복잡한 조건을 걸 수도 있으며, true, false를 사용해서 조건을 좀 더 정확하게 표현할 수도 있습니다.
MATCH (p:Person)-[:ACTED_IN]->(m:Movie)
WHERE (m.released = 2008) OR (m.released = 2009)
RETURN p, m
  • 그리고, whereexist를 사용하여, 노드의 프로퍼티가 존재하는지를 확인하여, 이 프로퍼티를 가진 것들만 매치하여 추출해낼 수도 있습니다. 즉 아래의 경우는 m.tagline를 가진 Moive들만 필터링 하게 되는 것이죠. 또한, 일반적인 full-join 들이 그러한 것처럼 WHERE 절에 조건을 많이 걸게 되면, 약간의 계산시간이 더 걸릴 수 있습니다.
MATCH (p:Person)-[:ACTED_IN]->(m:Movie)
WHERE p.name='Jack Nicholson' AND exists(m.tagline)
RETURN m.title, m.tagline
  • 또한, 문자열에 대해서 비교하는 경우는 다음처럼 처리할 수 있습니다. STARTS WITH, ENDS WITH, CONTAINS 이렇게 세 가지가 있는데요, 중간에 공백 있습니다. 굳이 공백을 만들어 둘 필요가 있었는지, 저는 약간 이해가 되지 않네요.
MATCH (p:Person)-[:ACTED_IN]->()
WHERE p.name STARTS WITH 'Michael'
RETURN p.name
  • 그 외로는 다음과 같은 Regular expression 또한 가능합니다. 물론, 저는 별로 익숙하지 않습니다만.
MATCH (p:Person)
WHERE p.name =~'Tom.*'
RETURN p.name
  • 다음처럼, 일종의 서브 쿼리처럼 사용할 수도 있습니다.
    • MATCH구문에서는 우선 Person-Wrote-> Movie의 관계를 모두 뽑아내었고,
    • 이제 WHERE 구문에서 적합하지 않은 것들을 제외하게 되는데요.
      • 여기서, (p)-[:DIRECTED]->(m)은 p가 감독한 m을 말하죠. 그리고 이는 그 앞에 NOT exists로 묶여 있습니다. 즉, DIRECTED관계가 아닌 것들이 조건인 것이죠.
    • 따라서, 영화의 각본을 썼지만, 감독을 하지는 않은 것들을 리턴하게 됩니다.
MATCH (p:Person)-[:WROTE]->(m:Movie)
WHERE NOT exists( (p)-[:DIRECTED]->(m) )
RETURN p.name, m.title
  • 물론, exists를 쓰지 않고, 그냥 아래처럼 NOT만 작성해도 별 차이는 없습니다.
MATCH (p:Person)-[:WROTE]->(m:Movie)
WHERE NOT ( (p)-[:DIRECTED]->(m) )
RETURN p.name, m.title
  • 아래와 같이 list에 포함되냐 안되냐는 식으로 처리하는 것도 물론 가능하구요.
MATCH (p:Person)
WHERE p.born IN [1965, 1970]
RETURN p.name as name, p.born as yearBorn
  • 그리고 기본적으로는 다음의 첫번째 쿼리와 같이, MATCH에서 노드의 유형 들을 정의해주는 것이 좋지만, 두번째 쿼리처럼 해도 같은 의미를 가지기는 합니다. Node의 경우는 type을 쓰지 않고 처리한다는 것이 조금 특이하긴 하죠.
  • 하지만, 이는 결국, 반드시 필요하지 않은 연산을 포함하게 됩니다. 따라서, 가능하면 MATCH에서 관련된 노드의 유형 등을 언급하면 연산의 속도가 현저하게 줄어들 것 같습니다.
MATCH (p:Person)-[:WROTE]->(m:Movie)
RETURN p.name, m.title
MATCH (p)-[rel]->(m)
WHERE p:Person AND type(rel)='WROTE' AND m:Movie
RETURN p.name, m.title

Using two MATCH patterns

  • 또한, 아래처럼 두 가지 이상의 MATCH를 동시에 사용할 수도 있습니다.
  • 다만, 아래 구문에서 보면, p1과 p2에 대한 아무런 조건이 붙어 있지 않아도, p1, p2가 같은 경우는 알아서 제외되어서 진행됩니다. 어떻게 보면 이는 상식적인 결과일 수 있죠.
MATCH (p1:Person)-[:ACTED_IN]->(m:Movie)<-[:ACTED_IN]-(p2:Person)
RETURN p1, p2
  • 아래 구문 또한 MATCH를 두 개 사용했습니다만, 이전의 MATCH문이 하나의 그래프를 가져왔다면, 아래 구문은 두 개의 그래프를 가져오는 형태가 되죠. 콤마로 구분되어 있습니다. 첫번째 그래프에서는 배우-영화-감독의 형태로 그래프를 가져오고, 두번째 그래프에서는 첫번째 그래프의 영화에 참여한 다른 배우 그래프를 가져오죠.
  • 어찌 보면, 이는 Cypher 구문의 한계로도 말할 수 있는데요, Cypher 구문 상에서 텍스트로 명령어는 앞 뒤로 하나밖에 넣을 수 업습니다. 즉, 하나의 MATCH 문으로는 2개의 관계(relationship)밖에 매칭할 수 없다는 것이죠. 따라서, 그 뒤에 다시 앞서 만든 그래프읩 변수와 연결하여 새로운 매치문을 넣어주는 것이 필요합니다.
  • 이를 통해 우리는 Movie에 연결된 ‘meg’, ‘other’, ‘d’라는 세 가지 종류의 노드를 매칭을 통해 가져올 수 있죠. 여기서, meg의 성질에 조건을 걸어주고, 나머지를 출력해줍니다.
MATCH (meg:Person)-[:ACTED_IN]->(m:Movie)<-[:DIRECTED]-(d:Person),
      (other:Person)-[:ACTED_IN]->(m)
WHERE meg.name = 'Meg Ryan'
RETURN m.title as movie, d.name AS director , other.name AS `co-actors`
  • 저는 가능하면 MATCH구문을 아래와 같이 분리해서 씁니다. 의미적으로 위의 쿼리와 아래 쿼리는 동일하죠. 이것이, 의미적으로 내가 어떤 관계등을 매칭하는지에 대해서 보다 선명하게 드러내주는 것 같아요.
MATCH (meg:Person)-[:ACTED_IN]->(m:Movie), 
      (d:Person)-[:DIRECTED]->(m:Movie), 
      (other:Person)-[:ACTED_IN]->(m)
WHERE meg.name = 'Meg Ryan'
RETURN m.title as movie, d.name AS director , other.name AS `co-actors`

Specifying varying length paths

  • 그래프에 존재하는 노드간의 거리는 다음의 방식으로 표현하고 매칭할 수 있습니다. 그냥 관계에 *n을 넣어주면 끝나는 군요 하하하 참 쉽다 하하하하하
MATCH (follower:Person)-[:FOLLOWS*2]->(p:Person)
RETURN follower, p
╒══════════════════════╤═══════════════════════════╕
│"follower"            │"p"                        │
╞══════════════════════╪═══════════════════════════╡
│{"name":"Paul Blythe"}│{"name":"Jessica Thompson"}│
└──────────────────────┴───────────────────────────┘
  • 그리고, * 뒤에 아무 값도 넣지 않을 경우, 일종의 wildcard로 인식하고, 접근가능한 모든 루트를 찾아서 리턴해줍니다.
MATCH (follower:Person)-[:FOLLOWS*2]->(p:Person)
RETURN follower, p
╒═════════════════════════╤═══════════════════════════╕
│"follower"               │"p"                        │
╞═════════════════════════╪═══════════════════════════╡
│{"name":"Paul Blythe"}   │{"name":"Angela Scope"}    │
├─────────────────────────┼───────────────────────────┤
│{"name":"Angela Scope"}  │{"name":"Jessica Thompson"}│
├─────────────────────────┼───────────────────────────┤
│{"name":"Paul Blythe"}   │{"name":"Jessica Thompson"}│
├─────────────────────────┼───────────────────────────┤
│{"name":"James Thompson"}│{"name":"Jessica Thompson"}│
└─────────────────────────┴───────────────────────────┘
  • 아래와 같이, 1번 follow하는 관계를 찾아보면 Paul -> Angela, Angela -> Jessica의 관계가 있죠. 즉, 위의 쿼리에서는 여기에 2배를 해서 2의 길이를 가지는 follow 관계를 모두 집어넣은 것이죠.
MATCH (follower:Person)-[:FOLLOWS]->(p:Person)
RETURN follower, p
╒═════════════════════════╤═══════════════════════════╕
│"follower"               │"p"                        │
╞═════════════════════════╪═══════════════════════════╡
│{"name":"Paul Blythe"}   │{"name":"Angela Scope"}    │
├─────────────────────────┼───────────────────────────┤
│{"name":"James Thompson"}│{"name":"Jessica Thompson"}│
├─────────────────────────┼───────────────────────────┤
│{"name":"Angela Scope"}  │{"name":"Jessica Thompson"}│
└─────────────────────────┴───────────────────────────┘

Finding shortest paths

  • 앞에서는 단지 길이만으로, path를 제한하였다면, 여기서는 아예 최단거리를 바로 찾는 방법을 알아봅니다. 당연하지만, 해당 함수는 shortestPath라는 이름으로 존재하며, 아래와 같이 사용할 수 있습니다.
  • 또한 여기서도 relationship에 *을 넣어주면, 접근가능한 모든 관계를 의미하는, 와일드카드를 의미하는 것이 됩니다. 따라서, 이를 이용해서 두 영화간의 모든 관계를 찾아주고, 이를 p라는 변수에 할당하며, 길이와 접근 방법을 모두 출력할 수 있죠.
MATCH p = shortestPath((m1:Movie)-[*]-(m2:Movie))
WHERE m1.title = 'A Few Good Men' AND
      m2.title = 'The Matrix'
RETURN  m1, m2, p, Length(p)

Specifying optional pattern matching

  • optional match는 MATCH와 비슷하지만, 하나의 차이점은 MATCH에 맞는 값들이 없을 경우에, 그냥 보여주지 말고 끝내는 것이 아니라, NULL을 표시해준다는 것이죠. 본문에서는 이것이, SQL에서의 Full-outer-join과 유사하다고 말하고 있습니다.
  • 아래의 예를 보면 좀 더 명확할 것으로 생각됩니다. 우선, 그냥 MATCH 하나만 사용하면 다음과 같은 결과가 나오게 되죠.
MATCH (p:Person)-[r:REVIEWED]->(m:Movie)
WHERE p.name STARTS WITH 'James'
RETURN p.name, type(r), m.title
╒════════════════╤══════════╤═══════════════════╕
│"p.name"        │"type(r)" │"m.title"          │
╞════════════════╪══════════╪═══════════════════╡
│"James Thompson"│"REVIEWED"│"The Replacements" │
├────────────────┼──────────┼───────────────────┤
│"James Thompson"│"REVIEWED"│"The Da Vinci Code"│
└────────────────┴──────────┴───────────────────┘
  • 하지만, OPTIONAL MATCH를 사용하게 되면, 다음과 같은 결과가 나오게 됩니다. 즉, 이전에 찾은 p를 기준으로 없으면 null을 표시해서라도 모두 출력해준다는 것이죠.
MATCH (p:Person)
WHERE p.name STARTS WITH 'James'
OPTIONAL MATCH (p)-[r:REVIEWED]->(m:Movie)
RETURN p.name, type(r), m.title
╒═════════════════╤══════════╤═══════════════════╕
│"p.name"         │"type(r)" │"m.title"          │
╞═════════════════╪══════════╪═══════════════════╡
│"James Marshall" │null      │null               │
├─────────────────┼──────────┼───────────────────┤
│"James L. Brooks"│null      │null               │
├─────────────────┼──────────┼───────────────────┤
│"James Cromwell" │null      │null               │
├─────────────────┼──────────┼───────────────────┤
│"James Thompson" │"REVIEWED"│"The Replacements" │
├─────────────────┼──────────┼───────────────────┤
│"James Thompson" │"REVIEWED"│"The Da Vinci Code"│
└─────────────────┴──────────┴───────────────────┘

Aggregation in Cypher

  • DB에서 값을 가져와서 비교적 간단한 리포트를 만든다고 하면, COUNT와 같은 aggregate function이 사용되죠. Cypher도 동일합니다. 다만, SQL에서는 Group by를 통해서 aggregate이 가능했다면, 여기서는 그냥 RETURN구문에 작성된 것을 그대로 사용해서 aggregate function이 적용됩니다.
  • 아래의 경우 ‘영화에 얼마나 많이 참여하였는지’를 보여주기 위한 쿼리문이죠.
MATCH (p:Person)-[:ACTED_IN]->(m:Movie)
RETURN p.name, count(*)
  • 아래 쿼리는 ‘배우별 감독별 얼마나 많이 참여했는지’를 보여주는 쿼리문입니다.
MATCH (p1:Person)-[:ACTED_IN]->(m:Movie)<-[:DIRECTED]-(p2:Person)
RETURN p1.name, p2.name, count(*)
  • 물론, SQL에서는 count(*) AS SS와 같은 식으로 설정하고, 뒤의 WHERE 문에서 해당 변수에 대한 조건을 걸어서 출력할 수도 있는데, 여기서는 바로 그렇게하기는 어려운 것 같아요. 그렇게 처리하기 위한 부분은 이후에 설명하도록 하겠습니다.

Collecting results

  • collect()는 결과를 가져와서 list 형태로 변환해주는 함수입니다.
  • 아래와 같은 일반적인 쿼리는, 테이블/그래프의 형태로 결과를 반환하죠.
MATCH (p:Person)-[:ACTED_IN]->(m:Movie)
WHERE p.name ='Tom Cruise'
RETURN m.title
╒════════════════╕
│"m.title"       │
╞════════════════╡
│"Jerry Maguire" │
├────────────────┤
│"Top Gun"       │
├────────────────┤
│"A Few Good Men"│
└────────────────┘
  • 다음과 같이 해당 성질의 부분에 collect를 선언해줍니다. 그럼, 하나의 리스트의 형태로 반환이 되죠.
  • 이는, 아마도 이후에 일종의 sub-query의 형식으로 사용해야 할때, 이런 리스트의 형태로 가져온 다음, 리스트에 포함된 성질만 가져오기 위해서 쓰는 것으로 보입니다.
MATCH (p:Person)-[:ACTED_IN]->(m:Movie)
WHERE p.name ='Tom Cruise'
RETURN collect(m.title)
╒════════════════════════════════════════════╕
│"collect(m.title)"                          │
╞════════════════════════════════════════════╡
│["Jerry Maguire","Top Gun","A Few Good Men"]│
└────────────────────────────────────────────┘
  • 그 외에도, mix, max등의 값들도 당연히 가지고 있으며, 이는 이후에 정리하도록 하겠습니다.

Additional processing using WITH

  • 앞서 언급한 것처럼, aggregate 결과에 대해서 어떤 식으로든 조건을 걸어서 처리하면 좋겠는데, SQL과 달리, 여기서는 그러한 조건을 바로 WHERE에 쓰는 형태가 아닙니다.
  • Cypher에서는 중간에 생성된 값들을 저장하게 위해서 WITH라는 명령어를 사용해야 하고, 이를 통해 중간의 값을 저정하고, 이후의 WHERE등에서 조건을 걸어서, 사용할 수 있씁니다. 말이 긴데, 결국 중간 값을 어떤 변수에서 저정하려면 WITH에서 정의하라는 말이죠.
  • 아래와 같이 WITH절에는 aggregate_func AS var_name의 형식이 사용됩니다. 즉, 각 값을 이러한 변수명으로 저장하고, WHERE에서 사용하겠다는 선언인 셈이죠. 저는, 이러한 방식으로 WITH를 사용해서 중간에 필요한 값들을 다른 변수 명으로 저장하고, WHERE절에서 조건을 처리하고, 필요한 값들을 RETURN을 통해서 처리하는 것이 훨씬 깔끔한 구문으로 보입니다.
MATCH (a:Person)-[:ACTED_IN]->(m:Movie)
WITH  a, count(a) AS numMovies
WHERE numMovies > 1 
RETURN a.name, numMovies
ORDER BY numMovies DESC
╒════════════════════════╤═══════════╕
│"a.name"                │"numMovies"│
╞════════════════════════╪═══════════╡
│"Tom Hanks"             │12         │
├────────────────────────┼───────────┤
│"Keanu Reeves"          │7          │
├────────────────────────┼───────────┤
│"Hugo Weaving"          │5          │
├────────────────────────┼───────────┤
│"Jack Nicholson"        │5          │
├────────────────────────┼───────────┤
│"Meg Ryan"              │5          │
├────────────────────────┼───────────┤
│"Cuba Gooding Jr."      │4          │
├────────────────────────┼───────────┤

Eliminating duplication

  • 중복을 삭제하려면 DISTINCT라는 구문을 사용하는 것이 필요합니다.
  • 아래 구문을 실행해 보면, 결과에서 collect로 묶어진 결과에 2개가 들어가 있음을 알 수 있습니다. 이는 톰 행크스가 영화 “That Thing You Do”에서 감독도 하고, 출연도 했기 때문에, 두 관계가 동시에 존재하며, 이 두 관계로부터 값을 COLLECT를 통해서 합쳐서 중복이 발생한 것이죠.
MATCH (p:Person)-[:DIRECTED | :ACTED_IN]->(m:Movie)
WHERE p.name = 'Tom Hanks' AND 1993<m.released<1997
RETURN m.released, collect(m.title) AS movies
╒════════════╤═════════════════════════════════════════╕
│"m.released"│"movies"                                 │
╞════════════╪═════════════════════════════════════════╡
│1996        │["That Thing You Do","That Thing You Do"]│
├────────────┼─────────────────────────────────────────┤
│1995        │["Apollo 13"]                            │
└────────────┴─────────────────────────────────────────┘
  • 따라서, 이 때는 해당 부분에 DISTINCT를 집어넣어서, 유일한 값만 가져오도록 처리할 수 있습니다.
MATCH (p:Person)-[:DIRECTED | :ACTED_IN]->(m:Movie)
WHERE p.name = 'Tom Hanks' AND 1993<m.released<1997
RETURN m.released, collect(DISTINCT m.title) AS movies
  • 물론, 위에서는 collect 안에 썼지만, SQL과 동일하게 RETURN DISTINCT의 형식으로 써도 문제없습니다.
MATCH (p:Person)-[:DIRECTED | :ACTED_IN]->(m:Movie)
WHERE p.name = 'Tom Hanks' AND 1993<m.released<1997
RETURN DISTINCT m.released, m.title AS movies
╒════════════╤═══════════════════╕
│"m.released"│"movies"           │
╞════════════╪═══════════════════╡
│1996        │"That Thing You Do"│
├────────────┼───────────────────┤
│1995        │"Apollo 13"        │
└────────────┴───────────────────┘

CollectIN을 사용해서 필요한 세트에 속하는 것만 필터링하기.

  • collect구문은 해당 값들을 리스트로 묶어 냅니다. 그리고 IN은 그 값이 주어진 리스트 안에 있는지 확인합니다.
  • 따라서 아래 구문과 같이, 필요할 때 collect로 묶어 내고 이 값에 내가 원하는 어떤 값이 존재하는지를 확인함으로써, 다음과 같이 상대적으로 조금 복잡한 구문을 만들어낼 수도 있죠.
MATCH (a:Person)-[:ACTED_IN]->(m:Movie)
WITH  m, count(m) AS numCast, collect(a.name) as cast
WHERE (numCast>3) AND ("Tom Hanks" in cast)
RETURN m.title, cast, numCast 
ORDER BY size(cast) DESC

Unwinding list

  • 묶어놓은 리스트를 다시 풀어낼 때 쓰는 것이 unwind라는 명령어입니다. 사실, 이렇게만 보면 ‘이 커맨드가 언제 필요하지?’싶을 수 있긴 한데요. 지금까지는 우리가 그래프가 만들어진 상황에서 그래프를 순회하는 쿼리만을 사용했습니다만, 반대로, 우리에게 상대적으로 보편적인 데이터들인 csv와 같은 데이터로부터 그래프를 만들어내야 할때 다음과 같은 데이터들이 주로 사용되게 됩니다.
  • 여기서는 간단한 사용법만을 정리하였으며, 다음과 같은 예를 들어보겠습니다. 다음 그래프에서,
    • 우선 WITH에서 리스트를 선언 및 정의한 다음
    • 그 값이 리스트로 되어 있으므로 row로 바꾸기 위해서 UNWIND로 돌립니다.
    • 그리고 그 결과는 row, list에 각각 저장되어 있으므로 이 값을 저장해주면, 다음의 테이블의 형태로 출력이 되죠.
WITH [1, 2, 3] AS list
UNWIND list AS row
RETURN row, list
╒═════╤═══════╕
│"row"│"list" │
╞═════╪═══════╡
│1    │[1,2,3]│
├─────┼───────┤
│2    │[1,2,3]│
├─────┼───────┤
│3    │[1,2,3]│
└─────┴───────┘

date

  • date()라는 함수를 사용해서 다음의 구문을 사용할 수 있습니다.
RETURN date() AS now, date().year AS year
╒════════════╤══════╕
│"now"       │"year"│
╞════════════╪══════╡
│"2019-12-28"│2019  │
└────────────┴──────┘

wrap-up

  • 사실, 호기심으로 Cypher를 공부하고 있지만, 큰 범주에서는 SQL과 크게 다르지 않습니다. 아마도 의도한 것이겠지만 SQL에서의 시멘틱 상에서의 유사점이 이미 내재되어 있어서, “아 이건 SQL에서의 이런 개념이네”, “아 이건 이런 게 조금 다르구나”라는 식으로 나아가니까 큰 어려움을 못 느끼는 것 같아요.
  • 다만, 그래프의 경우 테이블과 다르게 잘못 건드리면 계산의 복잡도가 급증할 수 있습니다. 이 부분을 어떻게 해결해야 하는지, 그 부분이 가장 큰 문제죠. 마치, 우리가 그냥 아무 생각없이 여러 테이블을 다 조인해버리고, 이를 모두 WHERE문에서만 처리한다면, 최적화없이 쓸데없는 과부하가 많이 걸리는 것처럼, 그래프도 마찬가지니까요.
  • 아무튼, 본문에서는 지난번보다는 조금 더 복잡한 쿼리문들을 정리하였습니다.

댓글남기기