Neo4j - part5 - advanced cypher
intro
- neo4j의 online-training course에서 cypher에서 query에 대한 내용을 가져와서, 번역했습니다.
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
- 그리고,
where에exist를 사용하여, 노드의 프로퍼티가 존재하는지를 확인하여, 이 프로퍼티를 가진 것들만 매치하여 추출해낼 수도 있습니다. 즉 아래의 경우는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" │
└────────────┴───────────────────┘
Collect와 IN을 사용해서 필요한 세트에 속하는 것만 필터링하기.
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문에서만 처리한다면, 최적화없이 쓸데없는 과부하가 많이 걸리는 것처럼, 그래프도 마찬가지니까요. - 아무튼, 본문에서는 지난번보다는 조금 더 복잡한 쿼리문들을 정리하였습니다.
GitHub에서 이 글의 마크다운 원본 보기 & 수정 제안src/content/posts/2019-12-27-neo4j_part5