N억개 이상의 Documents에 신규 필드값 (key-value) 값을 추가해야 하는 요구사항이 들어왔다. 실제 운영중인 서비스에 영향도를 최소화하기 위해, 어떠한 전략으로 작업해야할까?
그 고민의 흔적속에서 얻었던 사소하지만 중요한 MongoDB의 특징에 대해 정리하려고 한다.
사용자의 데이터를 분간하기 위한 신규 Key값이 추가되었다.
이에 따라, 기존에 사용했던 고유ID를 통해 외부 서비스를 호출하여 신규 Key값 (이하 K)으로 전환및 저장하기 위한 작업이 필요했다.
운영중인 서비스의 영향도를 최소화하기 위해 (최대한 많은 사용자 Data들을 실시간으로 업데이트 하기 위해) 서버로 인입된 요청에 대해 K필드값이 없을 경우 이를 추가하는 로직을 미리 반영해놓은 상태이다. 또한, K필드값에 대해 단일 인덱스도 추가해놓았다.
하지만 해당 API 반영 이후부터 인입이 없었던 사용자들은 전환이 되지 않았을 것이므로, 이러한 Data들 또한 데드라인 전까지 전환하기 위해 배치 작업을 계획했다.
가장 처음으로 대충(?) 생각해봤을때 생각했던 쿼리이다.
그 당시에 이 쿼리가 실제로 어떻게 동작할까 생각했던 내용은 다음과 같다.
(참고: mongo cursor 기반으로 대량 데이터를 처리하는 방식은 옳지 않다고 생각했다. 배치 실행시간이 외부 API의 처리속도와 반비례하기 때문에 장기간 mongo cursor를 유지하기 위한 DB 서버 부하를 생각하지 않을 수 없었다)
(1) "K"필드값에 대한 인덱스는 있지만, 엄밀히 말하면 "K" 필드값이 없는 Document를 대상으로 하므로 Collscan일 것이다.
(2) (1)을 보완하기 위해 limit을 걸어보자. 그러면 Collscan중간에 limit 개수만큼 Document가 fetching된다면 Collscan이 멈춰서 DB부하를 줄일 수 있지 않을까?
(1)은 확실하다고 생각해서, (2)번만 검증이 필요했다. 공식 문서나 geeksforgeeks.. DBA통해서 들은 결과로는 나의 생각이 맞았다.
근데 한가지 찝찝했던 부분이 있었다.
초기 쿼리 실행시, 아직 업데이트 되지 않은 다큐먼트들이 많아 limit개수만큼 금방 fetching될 것이지만, 쿼리를 실행하면 실행할수록, 조건에 부합하는 Document들의 개수가 줄 것이고, 그에 따라 Collscan의 영향을 받을 것이라고 생각했다.
확실치는 않으니 일단 선택지중 하나로 남겨놓고 다른 확실한 방법을 생각해봤다.
아까 전의 방식은 Collscan + limit 방식이라 문제가 생겼다면, Collscan을 하지 않으면 되는 것이 아닌가?
현재 Document들에 저장되고 있는 key값중의 createDate 필드가 있었고, 이 필드에 인덱스를 추가 후, 날짜별로 range scan하는 방식을 생각해보았다.
그러면 날짜의 range만큼만 fetching되어 사실상 IdxScan + limit와 비슷한 효과를 낼 수 있다.
근데 N억개의 다큐먼트에 인덱스를 거는 것이라 솔직히 조금 부담이 되었다. 그리고 또 작업이 끝나면 불필요한 인덱스라 삭제도 해야했고.. 귀찮은 일이었다.
그래서 생각한것이 ObjectId. 즉 기본 몽고 고유 ID 필드값이다.
이전에 MongoDB 서적을 읽으며 ObjectId의 앞 몇자리가 시간과 관련된 정보라고 얼핏 봤던 기억이 났다.
그러면, 굳이 createDate 필드에 인덱스를 추가하지 않고도, 기존에 존재하는 Id만으로도 같은 효과를 낼 수 있는것이 아닌가?!
이 또한 검증을 해보기로 했었다.
먼저 find( "K": {$exist: false} ).limit()
쿼리가 나의 생각대로 K필드값이 없는 다큐먼트가 극소수라면 Collscan의 영향을 받게될지 검증해봤다.
데이터 셋을 두가지로 가져갔다. (환경: mongo6.0 standalone container 환경)
두가지 경우 모두 K필드에 인덱스를 걸었다.
그후 두가지 데이터셋에서 쿼리를 날려봤다. 실행시간 속도도 측정해봤다.
?? 엥 ?? 차이가 없다
다시 해봤다. ?? 엥 차이가없다.
실행계획을 분석해봤다.
Fetch를 했고, inputStage에 IXSCAN이 있다.
인덱스 스캔 이후에 Fetching을 했다는 소리인데... 다큐먼트에는 인덱스 필드가 없는데 어떻게 된 일이지?
https://www.mongodb.com/docs/manual/reference/operator/query/exists/#use-a-sparse-index-to-improve--exists-performance
위 공식문서를 참고하자
이럴때는 항상 공식문서를 보아야한다. GPT를 믿어서도 안된다.

뭔가 non-sparse index가 걸린 것 같았다. non-sparse index일 경우에 Fetch를 사용한다는데.. 이게 뭔지 찾아봤다.
공식문서 내용
non-sparse indexes contain all documents in a collection, storing null values for those documents that do not contain the indexed field.
이 뜻이 index대상의 필드가 없어도 null처리 되어서 indexing이 된다는 뜻이었다.
그리고 index를 걸때 어떠한 옵션도 주지 않으면 default로 non-sparse index라고 한다.
그래서 인덱스를 거는데 오래걸렸나? 하고 K필드 index 사이즈 확인해보니 452메가쯤 되어있었다.
음.. 뭔가 있긴 했다.
그래서 index를 삭제하고 다시 쿼리를 수행해봤더니, 겁~~나게 느린걸 확인할 수 있다.
실제 실행계획도 Collscan이었다.
그래서 sparse-index로 옵션을 주고 다시 인덱스를 생성해보니 인덱스 사이즈가 0에 가까웠다.
(쿼리 실행은 안해봤는데 쿼리실행도 해볼걸 그랬다.)
K필드값이 non-sparse index라면 find( "K": {$exist: false} ).limit() 요렇게 작업해도 무방하다는 결과를 얻게 되었다.
간단한 방법이 있으니, 굳이 ObjectId로 fetching하는 방식은 사용하지 않아도 될것 같다.
어쨌거나, 두가지를 얻어갈 수 있었다.
물론 옵티마이저가 최적화를 해주긴 하겠지만,
초기에는 Collscan으로 limit개수만큼 fetching을 하고, 그 이후에 index가 효용가치가 있어지면 그때부터 sparse-index를 통한 IxScan + fetching이 이뤄지지 않을까? 추측중이다..