<아랑고DB> 9. 데이터 모으기 COLLECT / AGGREGATE / MIN_BY, MAX_BY

|
지난 시간에는 간단한 그래프 횡단에 대해 알아보았다. 다만 횡단의 본래 목적은 횡단 그 자체가 아닌 횡단을 하며 경로에 있는 데이터를 가져오는 데 있다. 이번에는 데이터를 모아주는 역할을 하는 COLLECT AGGREGATE 문법에 대해 배워보자!

1. 개념잡기

아랑고DB에서 COLLECT는 데이터를 그룹지어주는 연산자이다. SQL에서 GROUP BY와 유사한 역할을 한다고 이해하면 된다.

둘의 느낌적인 차이를 사용자 입장에서 설명해보면 이렇다.

  • SQL은 데이터를 한 번에 조회해서, 기준에 따라 데이터를 그룹 연산할게요~의 느낌이라면
  • AQL은 FOR 루프를 돌면서 기준에 따라 데이터를 모을게요~의 느낌이다

예시를 들면, id별 최근 방문 시간을 조회한다고 가정해보자.

//SQL
//테이블 전체 조회해서 최대 시간 뽑아주세요~
SELECT
  id,
  MAX(visit_time) AS last_visit_time 
FROM mysql.test_db.test_table
GROUP BY id
//AQL
//컬렉션 돌면서 id 최대 시간 모아주세요~
FOR data in test_collection
  COLLECT
    id = doc.id
  AGGREGATE
    last_visit_time = MAX(doc.visit_time)
  RETURN {id, visit_time}

AQL에서는 FOR 루프와 함께 COLLECT가 쓰이기 때문에, 데이터를 모은다라는 표현이 어울리는 것 같다.

2. COLLECT 문법 익히기

이제 실제 문법을 배워보자. 모든 자세한 설명은 아랑고 DB 공식 문서에 아주 잘 설명되어 있다.

COLLECT
  variableName = expression
AGGREGATE
  variableName = aggregateExpression
INTO groupsVariable	

COLLECT는 다양한 형태로 쓰이는데, 일단은 전체적으로 살펴보자.

COLLECT 바로 다음에 나오는 변수는 그룹의 기준이 되는 변수들을 의미한다. 좌변인 variableName은 사용자가 설정해주는 변수 이름이 되고, 우변에는 해당 값이 들어간다.

AGGREGATE는 필수 연산은 아니며, 그룹의 기준이 되는 변수들을 대상으로, 데이터에 어떠한 그룹 연산을 취할 것인지를 결정해주는 부분이다. 그룹별로 MIN, MAX, UNIQUE다양한 그룹 연산을 사용할 수 있다.

INTO도 필수 연산은 아니며, 그룹별로 데이터를 묶어주는 역할을 한다. 다시 말하면, FOR 루프를 돌면서 그룹에 해당하는 모든 도큐먼트를 모아주는 역할이다.

SQL에 대응해보면 아래와 같다.

  • COLLECT id = doc.id : GROUP BY id
  • AGGREGATE last_visit_time = MAX(doc.visit_time) : MAX(visit_time) AS last_visit_time
  • INTO groups : INTO는 SQL에 대응할만한 연산자가 생각나지 않는데, 굳이 하나를 만들자면 “그룹에 해당하는 모든 레코드(행)을 묶어서 하나의 어레이로 만들어주는” 연산이라고 보면 되겠다.

3. AQL 예제를 통한 사용법 익히기

여러가지 사례를 통해 문법에 익숙해져보자. 지난 글의 공항 데이터를 사용할 예정이며, 기억이 나지 않으면 요기를 다시 보자.

횡단에서 했던 것처럼, COLLECT도 뽑을 데이터가 정해졌을 때 미리 머리속에 그려보면 좋다. 아래 세 가지를 미리 생각해보고, 코드를 짜보도록 하자.

1) 그룹의 기준을 정하고 2) 뽑을 데이터를 정하고, 3) 데이터 각각이 어떤 기준으로 뽑혀야 하는지를 생각한다.

airports 데이터에서 UNIQUE한 국가만 리턴하기

두 가지 방법이 있다. DISTINCT 연산을 통해 고유값만 리턴해도 되고, COLLECT로 묶어서 그냥 리턴해줘도 된다.

둘의 차이는 여기잘 설명되어 있다.

1) 우리는 UNIQUE한 국가만을 리턴할 것이기 때문에, 그룹의 기준은 국가가 된다. 2)3) 뽑을 데이터는 따로 없고, 그룹 기준만 리턴한다.

// COLLECT 사용하고, AGGREGATE이나 INTO 사용하지 않는 예제
FOR airport IN airports
  COLLECT
    country = airport.country
  RETURN country	

// 동일한 연산
FOR airport IN airports
  RETURN DISTINCT airport.country

airports 데이터에서 국가별로 공항 이름을 리턴하기

1) 국가별 공항 이름이기 때문에 기준은 ‘국가’가 된다. 2) 뽑을 데이터는 공항 이름의 목록이다 3) 목록이기 때문에 배열 형태로 묶어줘야 한다

배열 형태로 데이터를 묶어주는 연산은 뭐가 있었을까? 일단 AGGREGATE에는 그런 기능을 수행하는 연산자가 없다.

그렇다면 남은 연산자인 INTO를 써야겠구나!

FOR airport IN airports  
  COLLECT
    country = airport.country
  INTO names = airport.name
  RETURN {
    country,
    names
    }

여기서는 공항의 이름만 사용하기 때문에 INTO에서 airport.name만을 모아왔다.

flights 데이터에서 공항별로 도착했던 비행편의 수와 비행편의 정보를 알고싶다

flights 데이터는 40만 개가 넘는 데이터가 존재하기 때문에 서버의 메모리가 작다면 LIMIT을 꼭 걸어주자!

1) 도착 공항별 데이터이기 때문에 기준은 공항이 된다 2) 뽑을 데이터는 비행이 있었던 횟수와 비행편에 대한 정보이다 3) 횟수는 개수를 세는 COUNT 연산자를 쓰면 되고, 비행편은 모아주면 되겠구나!

FOR flight IN flights
  LIMIT 10000 // 리밋!!
  COLLECT
    airport= flight._to
  AGGREGATE
    cnt = COUNT(1)
  INTO flight_infos = flight.FlightNum
  RETURN {
    airport,
    cnt,
    flight_infos
  }

위 내용에 대한 심화로, 리턴된 결과에 더하여 공항별 풀네임과 국가, 도시를 알고 싶다면 어떻게 해야할까? 답은 아래에 적어두겠음

flights 데이터에서 공항별로 최초 출발 비행편의 시간과, 최종 출발 비행편의 시간을 알고싶다.

위 문제들을 해결했다면, 이것도 상당히 쉽기 때문에 설명은 생략한다!

FOR flight IN flights
  LIMIT 10000 // 리밋!!
  COLLECT
    airport = flight._from
  AGGREGATE
    min_time = MIN(flight.DepTimeUTC),
    max_time = MAX(flight.DepTimeUTC)
  RETURN {
    airport,
    first_departure: min_time,
    last_departure: max_time
  }

4. 그 외 연산들

COLLECT에서 사용할 수 있는 유용한 연산들이 더 있다.

COUNT

그룹별로 숫자를 셀 때, 위에서처럼 COUNT()를 사용해도 되지만 AQL에서 제공해주는 다른 방법도 있다.

  • COLLECT WITH COUNT INTO countVariableName
FOR flight IN flights
  LIMIT 10000 // 리밋!!
  COLLECT WITH COUNT INTO length
  RETURN length

혹은, 배열의 길이는 LENGTH()라는 연산자로도 쉽게 계산 가능하다. 아래 코드에서 LET의 사용에 주목하자. 많이 쓰게 될 예정이다.

LET records = (
FOR flight IN flights
  LIMIT 10000 // 리밋!!
  RETURN flight
)

RETURN LENGTH(records)

MIN_BY, MAX_BY

SQL로 쿼리문을 열심히 짜다보면 시스템별로 문법이 약간씩 상이하다. 나는 주로 Presto 쿼리 엔진을 통해 작업하는데, Presto에는 MIN_BY, MAX_BY라는 유용한 함수가 있다.

얘가 어떤 역할을 하냐면, MAX_BY(a, b)로 쓰이며, “그룹별로 b값이 최대가 되는 레코드의 a값을 리턴해줘”의 역할을 해준다.

예를 들어, A라는 사람이 {‘음식’: ‘라면’, ‘시간’ : ‘아침}, {‘음식’: ‘돈가스’, ‘시간’ : ‘점심}, {‘음식’: ‘탕수육’, ‘시간’ : ‘저녁} 의 데이터를 가지고 있다고 가정해보자.

A가 가장 늦은 시간에 먹은 음식을 알려줘!라는 쿼리는 어떻게 구성해야할까? subquery를 통해 A의 가장 늦은 시간을 찾아내서, 해당 시간을 조회하는 방식도 있겠지만 MAX_BY를 쓰면 아주 간단해진다.

그냥 MAX_BY(음식, 시간)을 하게되면 시간이 최대인(가장 늦은 시간인) 데이터의 음식을 반환해주기 때문이다.

안타깝지만 AQL에도 해당 연산자는 없다. 하지만 아랑고DB 커뮤니티를 통해 아랑고DB 개발자들의 구현 방식을 배울 수 있었다. 그 값진 내용을 여기에 공유한다 :)

AQL에서 배열의 크기 비교는 element-wise하게 이루어진다. 즉, 0번째 인덱스의 값부터 비교를 해나가는 방식이다.

예를 들어, [1, 2, 3, 4]와 [-1]을 비교한다면, 0번째 인덱스의 값은 1이 크기 때문에 왼쪽의 배열이 더 크다.

[1, 2, 3, 4]와 [1, 2, 3, -1]을 비교한다면, 3번째 인덱스에 해당하는 4와 -1 중 4가 더 크기 때문에 왼쪽의 배열이 더 크다.

이 원리를 이용하면, MIN_BYMAX_BY를 구현 가능하다.

내가 알고싶은 값을 b, 기준이 되는 값을 a라고 하자. (위 예시에서는 음식이 b, 시간이 a가 되겠다)

각 레코드를 a_i, b_i로 표시한다면, 특정 그룹에 대해 N개의 데이터가 있었다면 아래처럼 될 것이다.

  • [[a_0, b_0], [a_1, b_1], …, [a_N-1, b_N-1]]

위 배열에서 최대값을 구한다면, 어떤 의미를 가질까? 배열의 크기 비교는 원소끼리의 대응이라고 했으니까, 기준값 a끼리의 비교가 된다.

즉, MAX(배열) 연산을 하게 되면, 기준값 a가 가장 큰 배열이 리턴되며, 그 배열의 b값이 우리가 찾는 값이 된다.

마찬가지로 MIN(배열) 연산을 하게 되면, 기준값 a가 가장 작은 배열이 리턴되며, b값을 쓰면 된다.

이제 아래 문제를 풀어보자. 위에서는 시간만 리턴하는 문제였지만, 이제는 시간에 대응되는 값을 찾아야 한다.

flights 데이터에서 공항별로 최초 출발 비행편의 비행번호(FlightNum)와 최종 출발 비행편의 비행번호를 알고싶다.

답을 보기 전에, 꼭 끝까지 머리를 싸매고 문제를 해결해보자.

FOR flight IN flights
  COLLECT
    airport = flight._from
  AGGREGATE
    min_by_deptime_to = MIN([flight.DepTimeUTC, flight.FlightNum]),
    max_by_deptime_to = MAX([flight.DepTimeUTC, flight.FlightNum])
    
  RETURN 
    {
      airport,
      earlies: min_by_deptime_to[1],
      latest: max_by_deptime_to[1]
     }

기준은 출발 시간이기 때문에 [출발 시간, 비행번호]의 배열에 대해 각각 MIN, MAX 연산을 취해주면 된다. 원리만 이해하면 정말 쉽다.

5. 심화 답

위에서 나온 심화 문제의 답은 아래에 있다.

핵심은, Document() 함수로 공항을 조회해 해당 결과를 리턴 값 안에 포함시켜주는 것이다.

여기서 COLLECT의 그룹 기준인 flight._to가 ArangoDB에서 _id에 해당하기 때문에, 이 값을 Document()에서 바로 호출해 접근이 가능한 원리이다.

FOR flight IN flights
  LIMIT 10000 // 리밋!!
  COLLECT
    airport= flight._to
  AGGREGATE
    cnt = COUNT(1)
  INTO flight_infos = flight.FlightNum
  RETURN {
    airport,
    cnt,
    flight_infos,
    fullname: Document(airport).name,
    country: Document(airport).country
  }

6. 어디까지 왔나

이번 시간까지 해서 아랑고DB를 사용하기 위한 기본적인 지식들은 모두 익혔다. 기초 AQL부터 그래프 횡단, COLLECT까지 모두 훑었기 때문에 이제 실전에서 잘 써먹기만 하면 된다.

이제 얼마 남지 않은 아랑고DB 관련 글들은 실제 데이터를 사용해 프로젝트를 진행하는 부분에 초점을 맞추려고 한다.

실제 프로젝트를 진행할 때, 기존 다른 형태로 저장되어 있던 데이터들은 어떻게 설계할 것이고, 손쉽게 아랑고DB에 넣을 것이며, 복잡한 데이터는 어떻게 뽑을 것인지를 다루려고 한다.

내가 현재 Python으로 구현해서 사용하고 있는 그래프 Mapper를 되도록 라이브러리화해서 사용할 예정이고, Kaggle에 있는 데이터를 하나 잡아서 프로젝트 용으로 쓸 것이다.

  1. 아랑고DB란? 왜 쓰는가?
  2. 아랑고DB 세팅하기 on Ubuntu
  3. 아랑고DB 쉘로 붙어서 명령어 체험해보기, 실체 파악해보기
  4. AQL(Arango Query Lang) 배워보기 1
  5. AQL(Arango Query Lang) 배워보기 2 - RETURN / UPDATE
  6. AQL(Arango Query Lang) 배워보기 3 - REPLACE / UPSERT / REMOVE
  7. 그래프 개념잡기
  8. 그래프 횡단하기 Graph Traversal
  9. (지금 보고있는 글) 데이터 모으기 COLLECT / AGGREGATE / MIN_BY, MAX_BY
  10. 프로젝트. 그래프를 통한 영화 추천시스템 만들어보기 1
  11. 프로젝트. 그래프를 통한 영화 추천시스템 만들어보기 2 (최종편)

<아랑고DB> 8. 그래프 횡단하기 Graph Traversal

|
이번 글에서는 드디어 그래프 횡단에 대해 배워보려고 한다. 사실상 그래프DB를 사용하는 가장 큰 이유가 바로 이 그래프 횡단의 유용함이라고 생각한다. 재밌게 배워서 알차게 써먹어보자!

1. 준비사항

마찬가지로 아랑고DB에서 제공해주는 공식 튜토리얼 문서를 기반으로 설명을 할 예정이다. 튜토리얼에서 사용되는 비행 관련 데이터셋은 여기를 눌러 다운받을 수 있다.

이 데이터는 각 공항들 사이에 존재하는 비행 경로를 그래프로 나타낸 데이터이다.

PDF파일의 24페이지를 보면, Airports 데이터셋을 import하는 방법이 나와있다. ArangoDB가 설치되어 있는 Ubuntu Shell에서 내가 진행한 방식은 아래와 같다.

curl 명령어는 이 사이트에 간결하게 잘 나와있어 참고했다.

# 임의의 경로에서 curl로 다운받는다
# -L 옵션은 리다이렉션을 끝까지 따라가는 옵션이고, 여기서의 결과를 --output에 저장한다
# 원래 일반적인 url은 curl -O {url} 하면 되는데, 얘는 리다이렉션이 걸려있어서 이렇게 다운받음
sudo curl -L https://www.arangodb.com/arangodb_graphcourse_demodata/ --output airport.zip

sudo unzip airport.zip

# airports 노드 컬렉션 임포트
arangoimport --file airports.csv --collection airports --create-collection true --type csv

# flights 엣지 컬렉션 임포트
# 아래 명령어는 컴퓨터 성능에 따라 꽤 오랜 시간이 걸릴 수 있습니다!
# ec2 t2.micro에서는 한참 걸려서 어쩔 수 없이 10000개의 행만 분리했다
# 아래 명령어는 메모리가 충분하다면 스킵해도 됩니다.
sudo split -l flights.csv flights_10000.csv

arangoimport --file flights_10000.csvaa --collection flights --create-collection true --type csv --create-collection-type edge

대량의 데이터가 있다면, 이렇게 csv 형태를 편하게 import 할 수 있다는 것도 기억해두자.

2. 데이터 살펴보기

데이터 임포트가 완료되고 나면, 아랑고 WebUI로 가서 데이터를 살펴보자. _system 데이터베이스에 디폴트로 생성되었고, (위 명령어에서 데이터베이스를 지정해주면 새로운 데이터베이스에 만들 수도 있다.) 각각 airportsflights 컬렉션에 생성되었다.

airports는 5개 국가의 공항에 대한 위치 정보를 가지고 있고, 3375개의 노드 도큐먼트로 구성되어 있다.

flights는 위 공항들을 잇는 엣지 도큐먼트이고, 출발 시간, 도착 시간, 항공편 번호 등의 비행에 관한 데이터를 가지고 있다. 약 44만 개의 데이터가 존재한다. (나는 10000개만 임포트함)

이제 이 데이터들을 기반으로 그래프 횡단에 대해 배워보자.

3. 그래프 횡단 문법

앞선 7장에서 말한 것처럼, 여기서는 ArangoDB의 anonymous graph를 사용한다.

그래프 횡단이란, 그래프의 엣지를 따라 움직이는 행위를 지칭한다. 이때 횡단에서 몇 개의 엣지를 이동하는지를 횡단의 깊이 Traversal Depth라고 부른다.

아랑고 튜토리얼에서 발췌한 아래의 그림을 보면 이해하기 쉽다.

Node and Edges

모든 그래프 관련 데이터베이스에서 사용하는 횡단 관련 문법은 상이하다. 아랑고DB의 AQL에서는 아래와 같은 문법을 사용한다.

FOR vertex[, edge[, path]]
  IN [min[..max]]
  OUTBOUND|INBOUND|ANY startVertex
  edgeCollectionName[, more...]

위 문법을 그대로 해석하면, 아래 정도가 되겠다.

startVertex를 출발점으로 잡고, 이 출발점과 연결되어 있는 edgeCollectionName에 연결된 엣지 중에서, 출발점에서 뻗어나가거나(OUTBOUND) OR 들어오거나(INBOUND) OR 둘 중 하나거나(ANY) 에 해당하는데, 깊이가 min~max 사이인 경로에 해당하는 값들을 리턴해라. 이때, 리턴하는 값들은 도착하는 노드, 엣지, 경로이다.

천천히 하나씩 뜯어보자. 일단 위 쿼리에서 대괄호[]안의 내용은 생략이 가능하다는 의미이며, 세로 라인은 또는(or)의 의미로써 상황에 맞는 것을 사용하면 된다는 뜻임

FOR vertex[, edge[, path]]

횡단에서 사용할 세 개의 변수 vertex, edge, path를 나타내는 값이다. FOR loop에서 썼던 것처럼 변수 이름은 사용자 마음임.

이때 vertex횡단 후에 도착해 있는 노드 오브젝트를 의미하며, edge는 횡단하게 될 엣지, path는 횡단하게 될 경로의 모든 노드, 엣지를 총칭한다.

여기서 edgepath는 사용해도 되고, 사용하지 않아도 된다.

// JFK 공항에서 출발했을 , 도착하는 공항들을 리턴함
LET jfk = Document('airports/JFK')
FOR v IN OUTBOUND jfk flights
  RETURN v
// JFK 공항에서 출발했을 , 도착하는 공항들과  비행 정보를 리턴함
LET jfk = Document('airports/JFK')
FOR v, e IN OUTBOUND jfk flights
  RETURN {v, e}

IN [min[..max]] OUTBOUND|INBOUND|ANY startVertex

Node and Edges

여기서 startVertex는 횡단의 출발점을 의미하며, 이 출발점에서 나가는 방향을 OUTBOUND, 들어오는 방향을 INBOUND, 둘 다 상관없이 연결되어 있기만 하면 ANY라고 지칭한다.

셋 중 하나를 골라서 쓰면 된다.

그리고 min, max는 생략 가능한데, 횡단의 깊이를 나타낸다. 생략하면 디폴트로 1..1로 설정되어 있음.

edgeCollectionName[, more…]

마지막으로 edgeCollectionName들은 횡단의 기준이 되는 엣지 컬렉션의 이름을 의미한다. 하나만 써도 되고, 컴마로 분리하여 여러 엣지를 쓸 수도 있다.

4. 그래프 횡단 Graph Traversal

그래프 문법을 보면 상당히 사용자 친화적임을 알 수 있다. 사람이 생각하는 횡단의 개념대로 쿼리를 구성할 수 있기 때문이다.

이제 실제 예제를 통해 그래프 횡단에 익숙해져보자. 아주 쉬운 예제이지만, 꼭 혼자서 시도해본 뒤 답을 보자.

생각하는 포인트는, 1)출발점 2)횡단 엣지 3)엣지의 방향 4)도착점 5)리턴값 을 미리 생각해보는 것이다.

LA국제공항(LAX)에서 한 번에 갈 수 있는 공항의 이름을 리턴해보기

FOR airport IN OUTBOUND 'airports/LAX' flights
  RETURN DISTINCT airport.name

1) 출발점은 LAX, 2) 횡단 엣지는 flights, 3) 엣지의 방향은 출발이기 때문에 OUTBOUND, 4) 도착하는 임의의 공항은 airport라고 지칭하며, 5)리턴값은 공항의 이름이기 때문에 도착하는 노드에서 속성을 가져와야겠구나.

위 예시에서 DISTINCT의 사용을 통해 고유한 공항의 이름만을 리턴하도록 해준다. DISTINCT airport라고 하게되면 오브젝트 전체를 지칭하는 것이므로 잘못된 쿼리임에 주의.

비스마르크 공항(BIS)에 도착하는 항공 번호를 10개만 리턴해보기

FOR airport, flight IN INBOUND 'airports/BIS' flights
  LIMIT 10
  RETURN flight.FlightNum

1) 출발점은 BIS (횡단의 출발점이라는 뜻, 비행의 출발점과 혼동하지 말 것), 2) 횡단 엣지는 flights, 3) 엣지의 방향은 출발 노드에 도착하는 것이니까 INBOUND, 4) 도착점은 airport라는 임의의 변수(마찬가지로 횡단의 도착점이라는 뜻), 5) 리턴값은 항공 번호이기 때문에 엣지에서 속성을 가져와야겠구나.

1월 5일 ~ 1월 7일 비스마르크 공항에서 출발하거나 도착하는 항공편의 번호, 대상 도시, 도착 시간 리턴하기

FOR airport, flight IN ANY 'airports/BIS' flights
    FILTER flight.Month == 1
    AND flight.Day >= 5
    AND flight.Day <= 7
    RETURN {
        'flight_num' : flight.FlightNum,
        'city' : airport.city,
        'arrive_time' : flight.ArrTimeUTC
    }

1)2)4)는 위와 동일. 3) 엣지의 방향은 어디든 상관없기 때문에 ANY, 5) 리턴값은 공항 이름과 항공 번호이기 때문에 노드와 엣지 모두에서 속성을 가져와야겠구나. 추가로 비행편에 대한 조건이 있으므로 엣지 속성에 필터를 걸어야겠구나.

JFK, PBI 공항으로 도착하거나 출발하는 항공편 중, 항공편 번호가 859, 860에 포함되는 것들만 리턴해라. 단, 처음에는 For LOOP을 사용하여 JFK, PBI를 찾기

FOR origin IN airports
  FILTER origin._key IN ["JFK", "PBI"]
  FOR dest, flight IN ANY origin flights
    FILTER flight.FlightNum IN [859, 860]
    RETURN {
      from: origin.name,
      to: dest.name,
      number: flight.FlightNum,
      day: flight.Day
    }

일단 FOR loop 을 통해 해당하는 공항만을 조건을 걸어준다. 나머지는 모두 동일한데, 횡단의 startVertex가 For loop의 origin 변수에 걸려서 변하게 되는 것이다!

FLL 공항에서 2번의 경로에 걸쳐 도달할 수 있는 비행 목록의 수를 세보기

그래프 횡단은 메모리를 많이 잡아먹는다. 여기서 40만개의 데이터가 있기 때문에 메모리가 적다면 서버가 과부하로 멈출 수 있다!
아직 우리는 숫자를 세는 방법을 배우지 않았다. 일단은 COLLECT WITH COUNT INTO length 라는 문법을 사용해보자.
FOR airport, flight IN 2..2 OUTBOUND 'airports/FLL' flights
COLLECT WITH COUNT INTO length
    RETURN length

2번에 걸쳐 도달하기 때문에, 깊이를 2로 맞춰주었다. 정말로 위 경로가 맞는지 보려면 아래 AQL의 결과와 비교해보면 됨.

// 위와 동일한 Traversal
FOR airport, flight IN 1..1 OUTBOUND 'airports/FLL' flights
    FOR airport_2, flight_2 IN 1..1 OUTBOUND airport flights
        COLLECT WITH COUNT INTO length
        RETURN length

5. 다른 기능들

문서를 보면 그래프 횡단에는 여러 기능들이 많다. 그래프에서 DFS(Depth-First-Search) 또는 BFS(Breadth-First-Search) 탐색을 할 수 있고, 가장 짧은 Shortest Path를 찾는 등의 설정도 가능하다.

예를 들어, BIS 공항에서 JFK 공항까지의 최단 경로를 찾는 쿼리는 아래와 같다. (엄밀히 말하면 경로의 수가 최소인 것이지, 실제 거리의 최소인지는 알 수 없다.)

LET airports = (
 FOR v IN OUTBOUND
 SHORTEST_PATH 'airports/BIS'
 TO 'airports/JFK' flights
 RETURN v
)
RETURN LENGTH(airports) - 1

6. 어디까지 왔나

이런 식으로 그래프 횡단은 아주 유용하게 사용할 수 있다. 횡단에서의 출발점, 도착점, 그리고 횡단할 엣지만 잘 지정해주면 횡단 자체는 어렵지 않다.

정말 어려운 부분은 이러한 횡단이 효율적일 수 있도록 설계하는 것과, 횡단에서 내가 필요한 데이터를 모으는 일이다.

일반적으로 위 예시들처럼 하나의 값만 리턴하고 끝나는 것이 아닌, 횡단의 과정에 있는 모든 값들을 내가 원하는 형태로 리턴하는 일이 빈번하기 때문이다.

따라서 다음 시간에는 잠깐 다시 AQL로 돌아가서, COLLECT AGGREGATE 문법에 대해 배운다. SQL로 따지면 GROUP BY 정도로 볼 수 있겠다.

  1. 아랑고DB란? 왜 쓰는가?
  2. 아랑고DB 세팅하기 on Ubuntu
  3. 아랑고DB 쉘로 붙어서 명령어 체험해보기, 실체 파악해보기
  4. AQL(Arango Query Lang) 배워보기 1
  5. AQL(Arango Query Lang) 배워보기 2 - RETURN / UPDATE
  6. AQL(Arango Query Lang) 배워보기 3 - REPLACE / UPSERT / REMOVE
  7. 그래프 개념잡기
  8. (지금 보고있는 글) 그래프 횡단하기 Graph Traversal
  9. 데이터 모으기 COLLECT / AGGREGATE / MIN_BY, MAX_BY
  10. 프로젝트. 그래프를 통한 영화 추천시스템 만들어보기 1
  11. 프로젝트. 그래프를 통한 영화 추천시스템 만들어보기 2 (최종편)

<아랑고DB> 7. 그래프 개념잡기

|
지난 3개의 글에서는 기초 AQL에 대해 배웠다. 이제는 그래프DB의 꽃인 그래프 횡단에 대해 배워보려고 한다. 정말로 횡단을 배우기 전에, 이 글에서는 그래프의 간단한 개념과 아랑고DB에서 어떻게 그래프를 다루고 있는지 먼저 살펴본다.

1. Node & Edge

그래프 이론에서 버텍스 Vertex(혹은 노드 Node)란 그래프 구성의 기본 단위이다. 보통 다이어그램에서 동그라미 안에 라벨로 표현된다.

엣지 edge는 하나의 노드와 다른 노드를 연결하는 직선이며, 방향성을 띠고있다.

아랑고DB 튜토리얼 페이지에 좋은 예시가 있어 가져왔다. 왕좌의 게임에 나온 주인공들의 부모, 자식 관계를 나타낸 그래프이다.

Node and Edges

아랑고DB에는 도큐먼트 컬렉션과 엣지 컬렉션으로 두 타입의 컬렉션이 있다. 하지만 엣지 컬렉션의 레코드도 도큐먼트라고 부르기 때문에 여기서는 편의성 노드 컬렉션엣지 컬렉션이라고 부르려고 한다.

우리가 이때까지 작업했던, 오브젝트 형태의 값만 있는 컬렉션은 노드 컬렉션이다.

아랑고DB에서는 노드 컬렉션의 개별 도큐먼트가 노드의 역할을 수행하며, 엣지 컬렉션의 엣지 도큐먼트가 두 노드 도큐먼트를 잇는 엣지의 역할을 수행한다.

엣지 컬렉션의 가장 큰 차이점은 _from, _to의 키를 도큐먼트에 필수로 포함해야 한다는 점이다. 이들은 엣지의 시작과 끝을 표현하며, 각 노드 도큐먼트의 _id 값에 맵핑되어있다.

왜 _from, _to에는 _key값이 아닌 _id 값이 들어있을까? 답을 스스로 생각해보자. 맨 아래에 적어놓겠음

2. 엣지 컬렉션 만드는 법

이미 우리는 AQL을 마스터하고 왔기 때문에 정말 간단하게 짚고 넘어가겠다.

앞서 말한 것처럼 엣지 컬렉션의 차이점은 _from, _to 필드가 있다는 점 뿐이다.

아랑고 WebUI에서 Edge 컬렉션을 아무거나 만들어주고, 거기서 아래 코드처럼 값을 넣으면 된다.

예를들어, “나는 점심으로 라면을 먹었다. 그리고 저녁으로 돈가스를 먹었다.”를 그래프로 표현한다고 해보자. 나는 아래와 같이 데이터를 구성하기로 했다.

  • 노드 컬렉션(사람)
  • 노드 컬렉션(음식)
  • 엣지 컬렉션(사람 -> 먹었다 -> 음식)

그럼 아래와 같은 도큐먼트가 생성된다.

  • 사람 컬렉션 = {나}
  • 음식 컬렉션 = {라면, 돈가스}
  • 엣지 컬렉션 = {나 -> 라면, 나 -> 돈가스}

이를 코드로 옮기기만 하면 매우 간단히 완성된다.

// 사람 컬렉션의 도큐먼트
INSERT {'name' : '나'} INTO 사람
// 음식 컬렉션의 도큐먼트
FOR food IN ['라면', '돈가스']
  INSERT {'food' : food} INTO 음식
// 엣지 컬렉션
// 각각 한줄씩 따로 실행해야 된다. 
INSERT {'_from' : Document('사람/나'), '_to' : Document('음식/라면'), 'time' : '점심'} INTO 먹었다
INSERT {'_from' : Document('사람/나'), '_to' : Document('음식/돈가스'), 'time' : '저녁'} INTO 먹었다

3. Named Graph vs. Anonymous Graph

아랑고DB에는 두 종류의 그래프가 있다. Named와 Anonymous가 그것인데, 이름에서 알 수 있듯 전자는 네이밍이 되어있고 관리가 되는 그래프를 뜻하고 후자는 그때그때 써먹는 녀석을 의미한다.

둘에 대해 간단히 비교해보자.

Named Graphs

네임드 그래프는 아랑고DB에서 1)여러 제약을 통해 2)갖춰진 형태로 관리해주는 그래프를 지칭한다.

구체적으로는 아래와 같은 기능을 제공해준다. 크게 트랜잭션 제공, 데이터 정합성 확인, 그래프 인터페이스 제공이라고 보면 된다.

  • 기본 AQL Traversal
  • 모든 수정 작업이 트랜잭션으로 취급된다
  • 노드를 지우면 연결된 모든 엣지가 지워진다
  • 엣지를 삽입할 때, 엣지 정의에 부합하는지 확인한다
  • Web UI에서 시각적으로 편리하게 볼 수 있다
  • javascript SmartGraph나 Graph Implementation이 용이하다 (정확히 어떤 걸 의미하는지는 모르겠다)
  • RESTful Graph 인터페이스로도 접근 가능

Anonymous Graphs

어나니머스 그래프는 네임드 그래프와는 반대로 그때그때 그냥 있는 컬렉션에서 꺼내어서 쓰는 그래프 구현 방법이다.

아래 기능만 제공해준다.

  • 기본 AQL Traversal

What to use

기능의 차이에서 알 수 있듯 네임드 그래프는 그래프 정합성을 보장해주는 역할이 크다. 이는 동시에 데이터베이스에서 수행해야 할 작업량도 많아짐을 의미한다.

어나니머스 그래프는 이런 제약이 없는 대신에 속도가 더 빠르지만, 데이터 정합이 중요하다면 사용자가 관리해야 함을 뜻한다.

자세한 내용은 여기를 읽어보자.

우리가 앞으로 계속 사용할 건 어나니머스 그래프이다. 간편하게 횡단을 배우는 용도로만 사용할 것이기 때문임

위 질문에 대한 답 : _id값이 들어있는 이유는 엣지 도큐먼트가 임의의 두 컬렉션을 연결하기 때문이다. _key값은 컬렉션 내에서만 고유하기 때문에 데이터베이스 단위에서 고유한 _id 값을 쓰는 것임

4. 어디까지 왔나

그래프 횡단에 들어가기 위한 모든 준비를 끝마쳤다. 다음 글에서는 진짜진짜 재미있는 그래프 횡단을 진짜진짜 들어가려고 한다.

기존 데이터베이스들에서는 감히 상상도 할 수 없었던 것들을 많이 실습해보자!

  1. 아랑고DB란? 왜 쓰는가?
  2. 아랑고DB 세팅하기 on Ubuntu
  3. 아랑고DB 쉘로 붙어서 명령어 체험해보기, 실체 파악해보기
  4. AQL(Arango Query Lang) 배워보기 1
  5. AQL(Arango Query Lang) 배워보기 2 - RETURN / UPDATE
  6. AQL(Arango Query Lang) 배워보기 3 - REPLACE / UPSERT / REMOVE
  7. (지금 보고있는 글) 그래프 개념잡기
  8. 그래프 횡단하기 Graph Traversal
  9. 데이터 모으기 COLLECT / AGGREGATE / MIN_BY, MAX_BY
  10. 프로젝트. 그래프를 통한 영화 추천시스템 만들어보기 1
  11. 프로젝트. 그래프를 통한 영화 추천시스템 만들어보기 2 (최종편)

<데이터베이스> 관계형 vs. NoSQL 언제 무엇을 써야할까?

|
매번 아랑고DB에 관한 글만 다루다가, 오늘은 회사에서 간단하게 RDB vs. NoSQL에 대해 세미나를 진행한 내용이 있어 공유하려고 한다. 데이터베이스에 대해 공부하게 되면 정말 많이 찾아보게 되는 글인데, 사실 이론적인 부분보다는 내가 직접 쓰면서 느낀 경험적 측면에서 써보려고 한다.
여기서 읽는 내용들은 "참고"만 하자. 이론적으로 정교하게 서술한 글이 아니기 때문에 아 이런 성질이 있나 보구나 하고 가벼운 마음으로 읽기를 강력 추천함

1. RDB (Relational Database)

관계형 데이터베이스로 풀이되는 RDB는 말 그대로 관계형 모델을 기반으로 하는 데이터베이스이다. 이를 유지하고 관리하기 위한 시스템을 RDBMS (- Management System) 이라고 부른다.

그리고 이러한 RDBMS는 주로 SQL(Structured Query Lang)을 이용해 데이터를 조회하고, 관리하게 된다. 이 글에 DML, DDL, DCL 등에 대한 설명이 간결하게 잘 되어있는 것 같다.

관계형 데이터베이스의 특징은 아래와 같다.

  1. 2차원 데이터로 표현된다 (행/열)
  2. 상호관련성을 가진 테이블의 집합으로 구성된다
  3. 테이블 사이의 관계를 외래키로 나타낸다
  4. 스키마 변경이 어렵다
  5. Vertical Scalable 하지만, Horizontal scale은 어렵다
  6. 메인테넌스 코스트 / 사용 요금이 비싸다
  7. SQL을 사용해 데이터를 질의한다
  8. ACID 성질을 갖는다

여기서 몇가지만 자세히 다루고 넘어가자.

4 스키마 변경이 어렵다

RDB는 매우 정교한 초기 설계로 만들어진다. 테이블 사이에 서로 의존성이 있고, 쳇바퀴처럼 맞물려 돌기 때문에 데이터 타입을 바꾼다든지 새로운 열을 추가한다든지 하는 작업은 실제 프로덕션 환경에서 사실상 불가능하다.

보통 기존 테이블을 복제해서 거기에 수정을 가한 뒤, 기존 테이블과 교체하는 방식을 택한다.

5 스케일링

일반적으로 Vertical Scaling은 하드웨어 스펙의 확장을 의미하고, Horizontal Scaling은 양적 확장을 의미한다.

RDB가 수직 확장이 더 용이하고 수평 확장이 어려운 이유는, 데이터가 여러 테이블에 의존해있기 때문이다.

데이터가 서버에 분산될 경우 이를 중간에서 잘 중재해주는 역할이 중요해지는데, 그럴 바에는 그냥 좋은 서버를 하나 사서 분산 처리에 대한 신경을 쓰지 않아도 되기 때문이다.

8 ACID

ACID(원자성 Atomicity, 일관성 Consistency, 고립성 Isolation, 지속성 Durability)는 데이터베이스 트랜잭션의 성질을 나타낸다.

자세한 내용은 여기 읽어보자. 나중에 기회되면 공부해서 제대로 다루겠음

2. NoSQL Database

이러한 RDB에 대응되는 녀석이 NoSQL 데이터베이스다. Non-SQL 이라고도 하고, Not only SQL 의 약자라고도 한다.

아무튼 이들은 관계형이 아닌 데이터 모델을 총칭하고, 도큐먼트 모델 / 키-값 모델 / 그래프 모델 / 와이드 컬럼 모델 등 다양한 데이터 모델이 있다.

NoSQL 데이터베이스의 특징은 아래와 같다.

  1. 다양한 방식으로 데이터를 표현한다
  2. 테이블(혹은 컬렉션 혹은 또 다른 명칭) 사이에 딱히 명시된 제약이나 규칙이 없다
  3. 스키마가 고정적이지 않고, 매우 유연하다
  4. Horizontal Scale이 쉽다
  5. 코스트 저렴 / 오픈소스도 많다
  6. 연산이 빠르고 빅데이터 & 실시간 연산에 적합하다

1 다양한 방식으로 데이터를 표현한다

NoSQL 방식의 데이터 모델의 예시를 살펴보면, 아래 그림과 같이 표현할 수 있다. 출처 요기

NoSQL Data Models

  • 도큐먼트 모델은 레코드 하나를 오브젝트(도큐먼트) 형식으로 표현한다. 자유로운 스키마 구조를 가지며 구조가 확정되지 않은 데이터를 밀어넣고, 자유롭게 작업하기 좋다. 몽고DB나 아랑고DB가 대표적인 예시이다.
  • 그래프 모델은 데이터를 버텍스와 엣지로 그래프에 표현하는 방식이다. 데이터 사이의 유기적인 관계를 표현하기에 적합하다. 대표적인 예로 Neo4j와 ArangoDB, Gremlin 등이 있다. 아랑고DB에 대해서는 현재 블로그에서 다루고 있으니 궁금하면 여기
  • 키/값 모델은 하나의 키에 값을 맵핑하는 해시 구조의 데이터 모델이다. 빠르게 데이터에 접근할 수 있는 장점이 있다. 대표적인 예로 redis가 있음
  • 와이드 컬럼 모델은 테이블 형태를 취하지만 행마다 갖는 컬럼의 형태가 고정되어 있지 않은 데이터 모델이다. 언뜻 와닿지 않는 내용인데, 이 글을 참고하길 바란다.

3. RDB vs. NoSQL

그럼 이제 둘을 비교해보자.

  • RDB는 관계형으로 데이터를 저장하지만, NoSQL은 그렇지 않다.
  • RDB는 스키마가 정적이지만, NoSQL은 유연한 스키마 구조를 갖는다.
  • RDB는 수직 확장이 용이하고, NoSQL은 수평 확장이 용이하다. (즉, RDB는 서버 용량을 늘리는 게 쉽고, NoSQL은 서버를 여러 대 늘리는 게 쉽다)
  • 위와 관련해서, RDB는 확장 시 다운타임이 있을 수 있지만, NoSQL은 거의 없다.
  • RDB는 복잡한 쿼리와 Join 연산이 가능하다. NoSQL은 구조화된 쿼리 언어가 없는 경우도 많고, 일반적으로 Join이 없다.
  • RDB는 OLTP에 적합하고, NoSQL은 OLAP에 적합하다. (즉, RDB는 트랜잭션 처리에 용이하고, NoSQL은 분석 처리에 용이하다)

4. 언제 어떤 데이터베이스를 사용할까?

3번과 비슷한 이야기지만, 실제 내가 사용하고 있는 환경에 빗대어 내 경험을 토대로 이야기해보겠다.

RDB - MySQL

RDB 중 하나인 MySQL은 보통 관계형으로 잘 정리될 수 있는 고객 데이터나 비즈니스 데이터, 결제 데이터 등을 다룰 때 사용한다.

  • 테이블마다 연관되는 키가 잘 드러나기 때문에 관계형으로 표현하는게 매우 효율적이다 (JOIN 연산!)
  • 트랜잭션을 통해 OLTP의 역할을 해야할 때
  • 트랜잭션의 ACID가 보장되어야 할 때 (Dirty read..)

NoSQL - MongoDB

몽고DB는 아주 유용한 녀석이다. 도큐먼트 모델이기 때문에 넣는 데이터의 스키마에 구애받을 필요가 없다.

  • 중간 분석 데이터를 편하게 밀어넣어 두고, 꺼내쓰기가 편리함
  • 단, SQL에서는 간단한 쿼리문이 몽고DB의 언어로 바뀌면 수행하기가 어렵기 때문에 애초에 데이터를 잘 가공해서 넣어둠
  • 최종적으로 사용할 데이터를 넣어두는 용도로도 많이 씀
  • 한마디로 막쓰기 정말 좋음, 심지어 안정적이다

NoSQL - redis

redis는 인메모리 키-값 모델이다. 말그대로 인-메모리이기 때문에 데이터 접근 & 대기시간이 매우 매우 짧다. 따라서 주로 데이터 캐시, 메시지 브로커 & 대기열로 쓰임

  • 동시에 대량의 트래픽을 처리하는 웹서비스에서 데이터를 캐시하는 중간 DB 역할로 많이 씀 (1만명이 MySQL 칠 트래픽을 redis가 값을 가져와서 막아준다고 생각하면 됨)
  • celery, airflow 등의 분산 처리 프레임워크에서 메시지 브로커로 많이 쓴다. (‘분산’ 처리이기 때문에 워커나 태스크 사이의 데이터를 주고받을 때 중개자가 필요함. 그 역할을 수행한다)

NoSQL - ArangoDB

아랑고DB는 멀티 모델 데이터베이스이다. 즉, 도큐먼트 모델도 지원하지만 그래프 모델도 지원한다.

  • 그래프 모델은 데이터 분석의 깊이가 깊어질수록 그 진가를 발휘한다고 생각한다.
  • ‘내 친구의 친구가 좋아하는 영화의 감독이 만든 다른 영화들의 주연들의 목록’을 기존 데이터베이스로 뽑는다고 생각해보자. 아찔하다.
  • 그래프DB는 간단한 횡단(traversal) 쿼리로 이를 구현할 수 있다.
  • 물론, 초기 그래프 구조 설계가 효율에 많은 영향을 미치며, 쿼리 튜닝도 중요함

5. 마치며

이렇게 간단하게 RDB와 NoSQL 데이터베이스를 비교하는 시간을 가졌다. 각각을 심층적으로 모두 다루긴 어렵겠지만, 각 DB를 사용하며 겪었던 여러 이슈들이나 알면 좋은 개념들은 지속적으로 업데이트 할 생각이다!

<아랑고DB> 6. AQL(Arango Query Lang) 배워보기 3 - REPLACE / UPSERT / REMOVE

|
데이터를 조작하는 AQL에 대해 공부하는 마지막 시간이다. REPLACE, UPSERT, REMOVE 등의 연산을 소개하고, 이때까지 AQL1~3 시리즈에서 배운 연산들을 정리하는 시간을 가지려고 한다.

1. REPLACE, UPSERT, REMOVE

REPLACE

UPDATE_key값이 일치하는 도큐먼트의 일부분을 바꾸었다면 REPLACE는 도큐먼트 자체를 바꾸는 연산을 한다.

UPDATE와 마찬가지로 시스템 필드인 `_id`, `_key`, `_rev`는 업데이트가 불가능하고, 엣지 컬렉션의 `_from`과 `_to`는 업데이트가 가능하다.

문법도 UPDATE와 동일하다. _key값이 포함된 도큐먼트를 전달해 한 번에 바꾸거나, _key값을 먼저 지정해주고, 이후에 도큐먼트를 전달하는 방식이다.

  1. REPLACE document IN collection
  2. REPLACE keyExpression WITH document IN collection

나머지 방식이나 Options 같은 경우도 UPDATE와 동일하기 때문에 건너뛴다. UPDATE에 관한 이전 글은 여기를 보면 됨

REMOVE

UPSERT 전에 쉬운 REMOVE 먼저 잠깐 살펴본다. 이름 그대로 도큐먼트를 제거하는 연산이며, keyExpression 을 사용한다. 즉, _key값을 기준으로 제거할 도큐먼트를 찾는다.

아래 문법처럼 사용하며, 간단하니까 얘도 생략한다.

  • REMOVE keyExpression IN collection

UPSERT

마지막으로 볼 연산자는 UPSERT라는 재미있는 연산이다. UPSERTUPDATEINSERT의 합성어로써 아래 두 가지 상황에 맞는 동작을 한다.

  • 데이터가 없으면 INSERT를 한다.
  • 데이터가 이미 존재하면 UPDATEREPLACE를 한다.

문법은 아래 두 가지 형태를 취한다.

  1. UPSERT searchExpression INSERT insertExpression UPDATE updateExpression IN collection
  2. UPSERT searchExpression INSERT insertExpression REPLACE updateExpression IN collection

여기서 주목해야할 점은 keyExpression 을 통해 _key를 기준으로하는 UPDATE 연산과 달리, UPSERTsearchExpression 을 기준으로 삼는다는 점이다. 이는 아래 세 가지 포인트를 갖는다.

  • _key값에 국한되지 않고 여러 필드로 조건을 걸 수 있다!
  • 동시에, _key는 고유했지만 필드는 고유하지 않을 수 있기에 여러 도큐먼트가 UPSERT의 대상이 될 수 있다
  • 그리고 그들 중 하나가 임의로 대상 지정된다
따라서 UPSERT의 lookup 과정에서 성능을 높이기 위해서는 반드시 lookup 대상의 필드들에 인덱스를 걸어주어야 한다. 그렇지 않으면 UPSERT의 성능은 현저하게 떨어진다.

이제 실제 연산을 실행해보자. 컬렉션에 관한 설정은 여기에서 CREATE 부분의 노란 박스를 보면 됨.

titles 컬렉션의 인덱스 설정은 그대로 둔 채, 데이터만 truncate 해주자.

//  컬렉션에 해리포터 시리즈를 7 넣어준다
FOR i in 1..7
  INSERT {
      'title': CONCAT('Harry Potter ', i), 
      'series': 'Harry Potter'
  } INTO titles
// 이제 해리포터 시리즈에 UPSERT 연산을 해보자
// 임의의 해리포터 도큐먼트( 7)  하나에 `series_num`이라는 필드가 추가되었다
UPSERT {'series': 'Harry Potter'}
  INSERT {'meaning': 'less'}
  UPDATE {'series_num': 1}
INTO titles
// 이번에는 없는 도큐먼트에 대해 UPSERT 해보자
UPSERT {'series': 'Avengers'}
  INSERT {'meaning': 'less'}
  UPDATE {'series_num': 1}
INTO titles

위 연산은 대상 도큐먼트가 없기 때문에 INSERT 연산을 수행하게 되어 새로운 도큐먼트를 만든다.

그럼 위 연산을 한 번 더 실행하면 어떻게 될까? 지금 생각으로는 또다른 {‘meaning’: ‘less’} 도큐먼트를 삽입할 것 같다.

직접 해보자

// 원모어타임
UPSERT {'series': 'Avengers'}
  INSERT {'meaning': 'less'}
  UPDATE {'series_num': 1}
INTO titles

title 필드에 고유 인덱스 설정을 잘 주었다면, 위 AQL은 오류를 일으킨다. title은 반드시 unique해야 하는데, {‘meaning’: ‘less’} 도큐먼트는 ‘title’을 null값으로 가지고 있는 도큐먼트이기 때문이다.

따라서 해당 도큐먼트를 2개 넣으면 null이 두 개가 되어 고유값 에러를 일으킨다. 재미있는 발견이다.

UPSERT 연산에서 lookup 파트와 insert/update/replace 파트는 아토믹하지 않게 실행이 된다. 즉, 동시에 여러 UPSERT 쿼리를 쳤을 경우 lookup 과정에서 도큐먼트가 존재하지 않다고 인식하고 모든 쿼리가 INSERT 연산을 수행할 수 있다는 것이다. 따라서 찾으려는 lookup 키 (searchExpression)에는 unique constraint을 주는 것이 낫다. 이는 몽고DB의 UPSERT에서도 동일하게 발생하는 문제이다. 잘 알아두자.
UPSERT를 대량으로 하게되면 intermediate commit을 하게 되어 전체 연산이 끝나기 전에 중간 연산을 write하게 된다!

UPSERT는 매우 유용한 기능이지만, 위와 같이 사용에 주의해야할 점들이 존재한다. 또한, UPSERT는 아랑고DB에서 HTTP API를 제공해주지 않는 연산 중 하나이다. 나머지 연산들은 모두 API가 열려있지만, UPSERT만은 AQL을 통해서만 수행이 가능하다.

이는 많은 아랑고 유저들이 개선을 원하는 부분 중 하나인데 아직까지 해결되고 있지는 않다. 깃헙 이슈 참고.

2. CRUD Best Practice

AQL 기초 1~3편을 통해 기초 연산들을 배워보았다. 그럼 언제 어느 연산을 사용해야 할까?

데이터를 넣고, 업데이트하는 몇가지 상황을 생각해보자.

정말 많은 데이터를 관리해야 한다면, HTTP API를 통해 직접 ArangoDB에 데이터를 보내는 게 가장 빠른 방법이다. 다른 언어의 라이브러리를 통해서 하면 편리하긴 하지만, 효율이 최우선이라면 HTTP API를 살펴보자.

데이터를 넣을건데, (영화명, 영화시리즈번호)의 쌍이 고유했으면 좋겠다.

이 경우는 이때까지 예제로 살펴봤던 것과 약간 다르게, 여러 개의 필드쌍이 하나의 _key를 구성해야 하는 경우이다.

여러 방법이 있는데, 1) index에 컴마로 분리하여 두 개 필드를 넣어 unique 제한을 주거나 2) 애초에 _key값을 영화명_영화시리즈번호로 만들어 넣는 것이다.

나는 후자의 방법이 가장 빠른 시스템 인덱스인 _key를 사용하고, 별도의 추가 인덱스를 생성하지 않기 때문에 선호하는 편이다.

이렇게 인덱스를 설정한 후, INSERTignoreErrors:true, overWrite:ignore 를 줘서 빠르게 밀어넣는다.

시간별 통계 데이터를 넣을건데, 10분마다 업데이트되는 값을 반영하고싶다.

예를 들어, 시간별 블로그 이용자 통계를 만들고 싶다고 가정해보자. 이 통계의 업데이트 주기가 10분이라면 15시 데이터에 대해, 15시 10분에 측정한 값과 15시 20분에 측정한 값은 다를 것이다. (10분동안 사람들이 더 들어왔을거니까)

그럼 나는 {시간 : ‘2021-11-14-15’} 인 도큐먼트가 없으면 넣어주고, 있으면 업데이트를 하고 싶다.

없으면 넣어주고 있으면 업데이트하는 편리한 연산을 우리는 방금 배웠다. 이런 경우에 UPSERT - REPLACE를 쓰면 된다. 1번과 마찬가지로 시간값을 _key로 만들어주는게 쿼리 튜닝에 좋다.

UPSERT - UPDATE가 아니고 REPLACE를 썼을까? 단순히 통계값이 방문자수 하나이면 UPDATE가 간편하지만, 통계값이 무수히 많다면 REPLACE 하나로 전체 도큐먼트를 통째로 교체하는게 낫다.

아니면, 앞서 배운 INSERT의 OPTIONS가 기억나는가? 얘를 써도 좋다.

두 연산이 완전히 동일한 것인지에 대한 의문이 드는데, 이 부분은 Arango Community에 질문 후 답변이 오면 여기에 수정해두도록 하겠다.

===

2021년 11월 24일 추가) 까먹고 있다가 오늘 질문을 해서 관련 답변을 받았다. 결론은 여기에 매우 자세하게 나와있다.

핵심만 말하면, 아래의 경우일 때, INSERTUPSERT는 거의 동일한 연산이라고 볼 수 있다.

  • _key를 사용하여 탐색할 경우
  • 그리고 RETURN OLD와 같이 기존 데이터를 참조하는 표현을 쓰지 않는 경우

그럼에도, INSERT가 훨씬 효율이 좋다. 그리고 REST API를 사용하면 효율이 더더욱 좋다.

===

3. 어디까지 왔나

이제 기초적인 CRUD에 관한 AQL은 다 다룬 것 같다. 나중에 기회가되면 부록처럼 각 연산의 원리와 성능에 대해 추가적으로 적어보려고 한다.

다음 시간부터는 AQL의 꽃인 그래프 관련 연산을 살펴본다. 그래프 횡단부터 시작하여, 각 횡단 단계에서 실행할 수 있는 여러가지 연산들을 배워보자.

아~주 재미있는 시간이 될 것이다. 기대해도 좋다 :)

  1. 아랑고DB란? 왜 쓰는가?
  2. 아랑고DB 세팅하기 on Ubuntu
  3. 아랑고DB 쉘로 붙어서 명령어 체험해보기, 실체 파악해보기
  4. AQL(Arango Query Lang) 배워보기 1
  5. AQL(Arango Query Lang) 배워보기 2 - RETURN / UPDATE
  6. (지금 보고있는 글) AQL(Arango Query Lang) 배워보기 3 - REPLACE / UPSERT / REMOVE
  7. 그래프 개념잡기
  8. 그래프 횡단하기 Graph Traversal
  9. 데이터 모으기 COLLECT / AGGREGATE / MIN_BY, MAX_BY
  10. 프로젝트. 그래프를 통한 영화 추천시스템 만들어보기 1
  11. 프로젝트. 그래프를 통한 영화 추천시스템 만들어보기 2 (최종편)