편집자 주: 이 프레젠테이션은 Michael Hunger 님이 2021년 12월 Neo4j Connections: 클라우드 개발자를 위한 그래프 행사에서 발표한 내용이에요.
SQL은 정말 오랫동안 사용되어 왔죠. 강력한 쿼리 언어이긴 하지만, Cypher에서 훨씬 더 잘 되는 부분들이 있답니다. SQL을 비교 대상으로 잠깐 보여드리겠지만, 주로 Neo4j에서 실행되는 Cypher에 집중할 거예요.
Cypher가 더 잘하는 10가지 작업을 살펴보기 전에, Cypher와 SQL, 데이터 세트, 그래프 패턴과 JOIN을 간단하게 비교해볼게요.
Cypher와 SQL
쿼리 언어로서 SQL은 50년이 넘었고, 많은 회사와 ISO 위원회의 참여로 발전해왔어요. 처음에는 SQL이 단일 테이블에 대한 쿼리 언어로 시작했지만, 나중에 XML, JSON, 지리 공간 데이터에 대한 JOIN 기능이 추가되었죠. 아래는 소매 데이터베이스에서 가장 많이 팔린 제품을 선택하는 일반적인 SQL 쿼리 예시예요.
SELECT p.product_name, count(*) as cnt
FROM customers c
JOIN orders o ON (c.customer_id = o.customer_id)
JOIN order_details od on (od.order_id = o.order_id)
JOIN products p ON (od.product_id = p.product_id)
WHERE p.product_name LIKE '%G%'
GROUP BY p.product_name
ORDER BY cnt DESC LIMIT 5;
이 SQL 쿼리는 4개의 JOIN을 사용해서 다음과 같은 결과를 만들어냈어요.
product_name | cnt
———————————+—–
Guarana Fantastica | 51
Gorgonzola Telino | 51
Gnocchi di Nonna Alice | 50
Mozzarella di Giovanni | 38
Gumbär Gummibärchen | 32
보시다시피, 관계형 데이터베이스는 테이블을 기반으로 만들어지고, 연결은 JOIN을 통해 이루어지죠.
Cypher는 그래프를 위해 설계된 언어예요. 비교를 위해 Cypher 쿼리를 한번 볼까요?
MATCH (a:Person)-[:ACTED_IN]->(m:Movie)
WITH a, count(m) as movies WHERE movies > 2
MATCH (a)-[:DIRECTED]->(m)
RETURN a.name, collect(m.title) as movies, count(*) as count
ORDER BY count DESC LIMIT 5;
이 쿼리에는 그래프의 구성 요소, 즉 Node와 Relationship 같은 엔터티가 코드 안에 내장되어 있어요. Cypher는 개발자가 아닌 사람도 읽고 이해하기 쉬워요. 강력하고 현대적인 언어이며, Python 같은 다른 쿼리 및 프로그래밍 언어의 장점을 가져왔죠. 이 덕분에, 해당 분야 전문가나 비즈니스 분석가가 쿼리를 읽고 피드백을 주거나, 직접 쿼리를 작성할 수도 있답니다.
Graph Database의 인기 있는 특징 중 하나는 데이터베이스 검색 시 스키마가 없거나 선택 사항이라는 점이에요. 즉, 데이터를 나타내는 고정된 스키마가 없어도 괜찮고, 대신 스키마 데이터를 발전시켜 새로운 속성 유형과 Relationship을 추가할 수 있다는 거죠. 이는 애플리케이션 개발에 엄청난 유연성을 제공해줘요.
데이터 세트
오늘 블로그에서는 두 개의 Neo4j 내장 예제 데이터세트를 사용할 거예요. 하나는 Movies Industry 데이터이고, 다른 하나는 Northwind 데이터랍니다. SQL에서 사용할 수 있는 자료가 많기 때문에, IMDb 스타일 데이터베이스와 Northwind Retail 데이터베이스를 예시로 사용할게요. 여기서 두 데이터 모델을 모두 확인할 수 있어요.
이건 영화 데이터베이스이고, 사람들은 영화와 다양한 Relationship을 맺고 있어요. 영화를 감독하거나, 쓰거나, 제작하거나, 출연하거나, 리뷰를 남길 수도 있죠. 우리가 사용할 또 다른 데이터베이스인 Northwind에는 제품, 카테고리, 고객 주문 정보가 들어있답니다.

Graph 패턴과 JOINs
Cypher에서는 시각적인 패턴을 살펴봐요. 화이트보드나 다이어그램에 원과 화살표로 무엇을 그리든, 우리는 쿼리 언어로 표현할 수 있어요.
Node
모든 Node 또는 엔터티는 괄호 안에 있어요. 예를 들어 Node 유형이나 추가 속성 등을 괄호 안에 넣을 수도 있죠.
()
(:Person)
(p:Person {name:'Emil'})
Relationship
마찬가지로 Relationship을 나타내는 가장 간단한 방법은 두 개의 대시이지만, 화살표에 두 개의 대시와 보다 큼 기호를 추가하거나 대괄호를 사용하여 Relationship에 더 많은 정보를 추가할 수도 있어요.
--
-->
-[:RATED]->
-[:RATED {stars:5}]->
Node와 Relationship을 결합하면 Cypher 패턴이 만들어져요.
(:Person {name:'Emil'})-[:RATED {stars:5}]->(:Movie {title:'The Matrix'})
저는 이것이 화이트보드에 그려지는 것을 상상할 수 있어요. Emil이라는 사람이 영화 제목을 가리키는 화살표와 함께 있는 거죠. 매트릭스. 이 작은 반 대시는 실제로 관계형 데이터베이스의 JOIN과 같아요. 하지만 관계형 데이터베이스에서는 엔터티에서 JOIN 테이블로, 다시 JOIN 테이블에서 다른 엔터티로 JOIN해야 하므로 계산 단계가 추가돼요.
FROM customers c1
JOIN orders o1 ON (c1.customer_id = o1.customer_id)
JOIN order_details od1 ON (od1.order_id = o1.order_id)
JOIN products p1 ON (od1.product_id = p1.product_id)
JOIN orders o2 ON (o2.customer_id = o1.customer_id)
JOIN order_details od2 ON (od2.order_id = o2.order_id)
JOIN products p2 ON (od2.product_id = p2.product_id)
WHERE c1.customer_name = 'Emil'
AND p1.product_name = 'Philz Coffee'
여기에 나와 있는 이 고객-제품 Relationship SQL 예제는 추천 엔진을 구축하는 데 사용될 수 있어요. 이를 위해 관계형 데이터베이스가 작동하도록 하려면 많은 JOIN이 필요하며 각 JOIN은 애플리케이션 속도를 저하시키죠. (아시겠지만, Emil은 Philz Coffee를 정말 좋아해요.)
이제 몇 가지 핵심 사항을 다루었으니, SQL로는 어려운 일 중 Cypher로 쉽게 할 수 있는 10가지 작업을 살펴볼까요!
1. 경로 패턴
경로 패턴은 처음에는 고급 기능이지만 그냥 사용해도 된다고 생각했어요.
:param product=>"Pavlova";
여기서는 나중에 쿼리에 사용할 product라는 매개변수를 설정했어요. 이제 경로 패턴을 살펴볼게요.
MATCH path = (p:Product {productName:$product})-[:PART_OF*]->(root:Category)
RETURN path
카테고리 목록을 가리키는 제품이 있어요. 범주 계층 구조가 얼마나 깊은지 모르는 경우 별표를 표시하면 루트 범주까지 임의의 연결된 Relationship을 찾을 수 있어요.
MATCH path = (p:Product {productName:$product})-[:PART_OF*1..10]->(root:Category)
RETURN path
최대 10개 Relationship의 경로를 찾도록 말하여 이를 제한할 수도 있어요.
아래에는 과자라는 하나의 카테고리만 있지만 카테고리를 더 추가할 수 있어요.
MATCH path = (c:Category {categoryName: "Confections"})
CREATE (c)-[:PART_OF]->(:Category {categoryName: "Sweets"})-[:PART_OF]->(:Category {categoryName: "Unhealthy Food"})
RETURN path
PART_OF Relationship 유형을 사용하여 "Sweets"라는 카테고리 이름을 가진 다른 카테고리를 추가해요. "건강에 해로운 음식"이라는 카테고리 이름을 사용하여 추가 카테고리를 추가할 수도 있죠. 우리의 경로는 다음과 같아요.
이 결과가 내 제품에서 루트 카테고리까지 계속되는 것을 볼 수 있어요. SQL에서 동일한 작업을 수행하려면 필요한 만큼 모든 수준과 JOIN을 사용하여 쿼리를 작성해야 해요. 또는 공통 테이블 표현식을 사용할 수도 있지만 이는 테이블을 재귀적으로 결합하는 더 복잡한 방법일 뿐이죠.
경로 패턴이라는 주제에서 그래프에서 할 수 있는 또 다른 작업은 루프를 찾는 것이에요. 예를 들어 동일한 사람에게 다시 연결되는 루프를 보고 싶은 경우죠.
MATCH path = (p:Person)-[*1..5]-(p)
RETURN [n in nodes(path) | coalesce(n.name, n.title)] as path, length(path)
LIMIT 10
여기서는 1~5홉의 경로를 찾고 있는데, 처음 시작했던 사람과 끝나고 싶어해요. 이 데이터는 표 형식으로 표시할 수 있을 뿐만 아니라 그래프의 핵심인 시각적으로도 표시할 수 있어요. 여기에 시각적인 내용이 포함된 더 긴 루프가 있어요.
MATCH path = (p:Person)-[*3..10]-(p)
RETURN path
LIMIT 1
2. 최단 경로
두 번째 Cypher 기능은 최단 경로에요. 아마도 LinkedIn에서 어떤 사람이 다른 사람과 너무 많은 거리를 두고 있다는 것을 본 적이 있을 거예요. 이는 Graph Database에서 쉽게 수행할 수 있는 작업이죠.
:param name=>"Meg Ryan";
시작하는 사람부터 시작해볼게요. 여기서는 "Meg Ryan"이라는 이름을 사용하겠어요. Meg Ryan에서 Kevin Bacon까지의 최단 경로를 찾고 싶어요.
MATCH path = shortestPath(
(:Person {name:$name})-[:ACTED_IN*..10]-(:Person {name:'Kevin Bacon'}) )
RETURN path, length(path);
ACTED_IN 연결을 최대 10홉까지 설정한 다음 경로와 경로 길이를 반환해요. 이 경로를 임의의 길이(1홉, 2, 3, 4, 5)로 만들 수 있어요.
여기 시각적 결과에는 Meg Ryan에서 Kevin Bacon까지 4개의 홉 경로가 있어요. 배우 대 배우 관계를 통해 우리는 그 사이에 영화를 가지고 있죠.
3. GROUP BY를 사용하지 않은 집계
다음 기능은 제가 개인적으로 매우 만족하는 기능이에요. 저는 필드 선택과 GROUP BY 문 모두에서 그룹화 키를 철자하는 것이 불필요하기 때문에 항상 SQL에서 GROUP BY를 싫어했어요. 이는 GROUP BY 문이나 집계되지 않은 필드 그룹화 키가 있으면 분명하기 때문에 Cypher에서는 피하고 싶었던 것이죠.
MATCH (m:Movie)<-[:ACTED_IN]-(a:Person)
// in-between aggregation, only passing on declared fields
WITH a, count(m) as movies
// with filter on the aggregated value
WHERE movies > 2
// next query part
MATCH (a)-[:DIRECTED]->(m)
// aggregation at the end by a.name
RETURN a.name, collect(m.title) as movies, count(*) as count
ORDER BY count DESC LIMIT 5;
이 경우에는 영화에 출연한 인물을 찾은 다음 중간 집계를 사용해요. WITH 절을 사용하면 사람별 집계를 볼 수 있어요. Person은 반환 결과에 집계되지 않은 유일한 필드이기 때문에 자동으로 그룹화 키에요.
4. 내장 데이터 구조 지원: 목록 및 맵
프로그래밍 언어 배경이 있다면 목록과 맵을 일류 함수로 사용하는 것이 매우 유용하고 일반적이에요. Cypher는 목록을 완벽하게 지원해요. 범위, 맵 및 조각을 가질 수 있어요. 목록, 목록 이해 및 목록의 수량자에 대한 Index 액세스 권한이 있어요. 목록의 조건과 함께 맵의 경우 Dot 액세스, Key 액세스 및 맵 투영이 있어요.
// list comprehension
WITH [id IN range(1,100)
WHERE id%13 = 0 |
// literal map construction
{id:id, name:'Joe '+id, age:id%100}] as people
// list quantor predicate on list elements
WHERE ALL(p in people WHERE p.age % 13 = 0)
RETURN people
이 목록 및 맵 사용 예는 약간 인위적인 것이에요. 우리는 100개의 숫자 범위로 시작했으므로 1부터 100까지 목록을 반복해요. 해당 ID는 이 목록에 있어요.
그런 다음 13이 행운의 숫자이기 때문에 필터를 추가하고 나머지 없이 13으로 나눌 수 있는 모든 13개의 숫자를 선택해요. 각 ID에 대해 ID 필드, ID와 연결된 문자열인 이름 필드, 모듈로 100인 ID인 연령 필드가 있는 맵을 만들어요. 따라서 1부터 99까지 사람들을 반복해요. 다음과 같아요.
5. 긴장을 풀고 Collect 해보세요
다음 기능은 제가 구축에 참여했던 기능인 UNWIND 및 Collect이에요. Collect 함수는 여러 값을 실제 목록으로 변환해 주죠. UNWIND는 그 반대고요. Collect는 여러 `row`를 가져와 목록으로 변환하고, UNWIND는 목록을 가져와 여러 `row`로 변환해요. UNWIND는 목록을 기반으로 한 데이터 생성 및 업데이트에 특히 유용하답니다. 맵 또는 배열 목록과 같은 데이터 목록을 전달하는 경우, UNWIND를 사용해서 이 요소 목록을 반복할 수 있고 모든 요소에 대해 엔터티를 생성하거나 업데이트할 수 있어요.
// turn list into 10k rows
UNWIND range(1,10000) as id
CREATE (p:Person {id:id, name:"Joe "+id, age:id%100})
// aggregate people into age groups, collect per age-group
RETURN p.age, count(*) as ageCount, collect(p.name)[0..5] as ageGroup
이 예에서는 10,000명의 사람 목록을 가져와 `id` 필드에 1부터 10,000까지 번호가 매겨진 `row`로 변환해요. 그런 다음 각각에 대해 `id`를 가진 사람을 만들죠.
역 Collect 함수의 경우, 연령을 그룹화 키로 반환한 다음 각 연령 그룹에 있는 사람 수를 반환합니다. 우리는 이 경우에 수행하는 사람의 이름을 포함한 모든 표현을 수집할 수 있어요. 그런 다음 Collect에서 목록 조각을 수행하여 처음 5개 값만 표시하는 거죠.
실행한 후에는 100명까지의 연령 그룹이 표시될 거예요. 100에 100을 곱하면 10,000명이 되기 때문에 각 연령 그룹에는 100명이 포함되죠. 목록은 컬렉션에서 각 연령 그룹의 처음 5명이고요.
6. 패턴 이해 및 맵 투영
Neo4j는 그래프 모양 도메인 데이터에 대한 API `query` 언어인 GraphQL을 살펴봤어요. 약 6년 전, 우리는 Cypher에 추가할 수 있는 GraphQL의 몇 가지 중요한 기능을 생각했었죠. 하나는 맵 투영(Map Projections)이라고 하고 다른 하나는 패턴 이해(Pattern Comprehension)라고 합니다.
MATCH (m:Movie)
// map projection, with property access
RETURN m { .*,
// pattern comprehension
actors:
[(m)<-[r:ACTED_IN]-(a) |
// property access, nested expressions
a { .name, roles: r.roles,
movies: size([()<-[:ACTED_IN]-(a)|a]) }
][0..5], // list slice
// pattern comprehension with filter & expression
directors: [(m)<-[:DIRECTED]-(d) WHERE d.born < 1975 | d.name]
} as movieDocument
LIMIT 10
맵 투영의 경우 영화 데이터베이스를 사용한 다음 찾아야 하는 영화의 경우 해당 `property`를 모두 반환해요. 위 예에서는 .*이죠. 그런 다음 이 맵에 다른 필드도 추가할 수 있어요. 예를 들어 맵에 중첩된 구조인 실제 필드가 있을 수 있죠. 이는 프런트 엔드 애플리케이션이나 JavaScript 애플리케이션과 같이 문서와 유사한 요소를 반환하고 중첩된 문서의 모든 데이터가 포함된 JSON 개체를 다시 가져오려는 경우에 유용하답니다. 마지막으로 WHERE 절의 패턴 이해는 목록 이해와 유사하지만 요소 목록 대신 관련 그래프 패턴에 대한 것이에요.
7. 읽기와 쓰기 결합
SQL을 사용해 본 적이 있다면 읽기 및 쓰기를 지원해야 한다는 것을 알았을 거예요. insert 문을 수행하거나 select에서 삽입을 수행할 수 있지만 그게 전부죠. 이것이 동일한 `query`에서 읽기와 쓰기를 결합하는 것이 좋은 이유에요. GraphQL을 사용하면 데이터베이스를 업데이트하는 동안 데이터를 가져올 수도 있답니다. 이 예에서는 영화 제목, 사람 이름, 별점 몇 개로 매개변수를 설정하면 돼요. 이 매개변수를 `query`하려면 먼저 Read를 통해 영화를 찾은 다음 Write 문을 사용합니다.
param rating=>({title:'The Matrix',name:'Emil Eifrem', stars:5})
MATCH (m:Movie {title:$rating.title})
// possibly write
MERGE (u:User {name:$rating.name})
// possibly write
MERGE (u)-[r:RATED]->(m)
// write
SET r.stars = $rating.stars
WITH *
MATCH (m)<-[:ACTED_IN]-(a:Person)
// read & return
RETURN u, m, r, collect(a)
사용자가 존재하지 않으면 'Emil Eifrem'이라는 이름으로 사용자를 만들어요. User와 Movie 사이에 RATED relationship이 존재하지 않으면 생성하고, 이미 존재하면 접근하죠. 그런 다음 새로운 별 등급을 설정하는데, 여기서는 5를 설정했어요. 업데이트 후에는 또 다른 read operation을 수행해서 이 영화의 모든 배우를 찾은 다음 사용자, 영화, 등급, 그리고 이 영화의 모든 배우를 반환해요. 이걸 실행하면 아래와 같은 결과가 나온답니다.
8. 트랜잭션 일괄 처리
데이터베이스에 대해 대규모 업데이트를 수행하는 경우 transaction에서 업데이트 정보가 transaction을 커밋할 때까지 메모리에 유지된다는 점을 고려해야 해요. transaction 일괄 처리를 사용하면 한 번에 사용하는 메모리 양을 제한할 수 있죠.
:auto MATCH (o:Order) // imagine 100M
CALL { WITH o
MATCH (o)-[r:ORDERS]->()
WITH o, sum(r.count) as products, sum(toFloat(r.unitPrice)*r.quantity) as total
SET o.count = products, o.total = total
} IN TRANSACTIONS OF 100000 ROWS
총 주문에서 두 개의 새로운 필드를 계산하고 싶어요. 각 주문에 대해 제품 수와 총 가격을 얻는 거죠. 단가에 수량을 곱한 금액이 총 가격이 될 거구요. ORDER node에 이 두 가지 property를 설정할 거예요. 여기서는 각 하위 transaction에 대해 100,000개의 업데이트를 수행해요. 이걸 실행하면 한 transaction에서 다른 transaction으로 데이터가 어떻게 업데이트되는지 확인할 수 있답니다.
9. 내장된 데이터 로딩: CSV
Neo4j에서는 URL을 통해 CSV 파일에 액세스한 다음 CSV 파일을 사용하여 그래프를 업데이트할 수 있어요.
WITH "https://data.neo4j.com/importing/ratings.csv" AS url
LOAD CSV WITH HEADERS FROM url AS row
MATCH (m:Movie {movieId:row.movieId})
MERGE (u:User {userId:row.userId})
ON CREATE SET u.name = row.name
MERGE (u)-[r:RATED]->(m)
SET r.rating = toFloat(row.rating)
SET r.timestamp = toInteger(row.timestamp)
CSV 파일을 가져와서 URL을 사용하여 CSV를 map 구조로 변환해요. 각 row는 실제로 쿼리 도구 내에서 사용할 수 있는 map 구조인 거죠. 사용자가 존재하지 않으면 사용자를 병합하여 생성해요. 이 쿼리가 영화, 사용자 및 relationship에 대해 생성하는 node는 다음과 같아요.
10. 내장 데이터 로딩: API
여기에는 API에서 바로 JSON 데이터를 로드할 수 있는 사용자 정의 프로시저가 있어요.
WITH "https://api.stackexchange.com/2.2/questions?pagesize=2&order=desc&sort=creation&tagged=neo4j&site=stackoverflow&filter=!5-i6Zw8Y)4W7vpy91PMYsKM-k9yzEsSC1_Uxlf" AS url
// load the json data from the URL
CALL apoc.load.json(url) YIELD value
// turn element list into rows
UNWIND value.items AS item
// deconstruct row data
RETURN item.title, item.owner, item.creation_date, keys(item)
LIMIT 5;
YIELD를 사용하여 데이터를 가져와서 UNWIND하여 이 JSON 목록을 row로 바꿔요.
실제로 이 중첩된 개체에 접근해서 데이터를 가져오고, 중첩된 필드에서 사용자를 생성할 수도 있어요.
결론
자, 이렇게 해서 관계형 데이터베이스와 비교했을 때 Graph Database를 사용할 때의 많은 이점을 보여주는 Cypher의 기능 목록이 끝났어요. 한번 시도해 보시고 기존 SQL에는 없는 이러한 기능을 사용해 보세요!
다음의 다른 리소스도 확인해 보세요.
The 사이퍼리카드는 Cypher의 모든 기능에 대한 정말 편리한 참조 자료이고, 자세한 내용에 대한 직접 링크도 포함되어 있어요. 그리고 사이퍼 매뉴얼도 참고해보세요. 저희는 또한 그래프아카데미 교육에서 Cypher에 대한 대화형 초급 및 고급 과정을 제공하고 있을 뿐만 아니라 Cypher 개발자 가이드도 제공하고 있답니다.
지금 무료 Neo4j AuraDB 클라우드 Graph Database 인스턴스에서 Cypher Query Language를 사용해 보세요. 신용카드는 필요하지 않아요.지금 Neo4j AuraDB를 무료로 받으세요
에이치시스템즈의 LogTree는 Neo4j 기반 GraphRAG 플랫폼으로, 데이터를 자동으로 지식그래프화하고 자연어 질의로 즉시 답을 제공합니다.
👉 에이치시스템즈 홈페이지