[Database] Paging query with index (1)

Noah·2022년 1월 20일
0

Database

목록 보기
1/1

Project Description

2021년 여름방학 기간 동안 크누마켓이라는 프로젝트를 진행했었다. 해당 프로젝트에 대해 간단히 언급하자면 내가 재학 중인 경북대학교 재학생을 위한 공동구매 서비스이다.

간단한 예로 배달 음식을 혼자 시켜 먹을 때 최소 주문금액 때문에 고민되었던 경험들이 있을 것이다. 또 택배로 생필품을 사고 싶을 때도 필요한 물품이긴 하지만 하나만 팔기보다는 여러 개를 세트로 파는 경우도 많이 봤을 것이다. 물론 개별로 여러 개를 사는 것보다 싸긴 하지만..

어쨌든 이런 이유로 우리 학교 주변에 자취생들이나 기숙사에 거주하는 학생들이 공동구매를 하기 위한 오픈 카톡 방이 존재했었고 이걸 서비스화하여 좀 더 편하게 공동구매를 할 수 있다면 어떨까라는 생각에 해당 앱을 만들게 되었다. (아이디어는 제가 내지 않았습니다..!)


Features

앱의 주요 기능 중엔 특정 물품에 대한 공동 구매를 위해 인원 모집 글이 필요했고 백엔드에서 해당 API를 내가 구현을 했었다. 간단히 UI를 보면 다음과 같다.

POST /api/v1/posts

GET /api/v1/posts?pageNo

GET /api/v1/posts/{post_uid}

Delete나 Update 기능도 만들었지만 해당 포스트에서 얘기하고자 하는 것이 아니기에 넘어가도록 하겠다.


Paging Query

위에서 공동구매 모집 글의 리스트에 대한 데이터를 응답하는 페이징 기능을 확인할 수 있다. 당시에 구현할 땐 급하게 투입되어 진행하던 프로젝트라 성능을 신경 쓰지 않고 구현했었다. 해당 방식을 코드로 살펴보자.

getPosts = async (skipValue: number) => {
        const posts = await getRepository(Post)
            .find({
                select: ['post_uid', 'title', 'created_at'],
                order: {
                    created_at: 'DESC'
                },
                skip: skipValue,
                take: 20                
            })
        return posts;
    }

위의 코드를 SQL 쿼리로 바꾼다면 다음과 같다.

SELECT post_uid, title, created_at FROM post
ORDER BY created_at DESC
LIMIT 20 OFFSET skipValue;

간단하게 설명하자면 skipValue는 스킵 할 글의 개수를 나타내고 Limit은 가져올 글의 개수이다. skipValue가 변수라 저렇게 나타냈을 뿐이지 실제 쿼리로 수행될 땐 20, 40, 60 등 클라이언트로부터 받은 pageNo 쿼리 값에 따라 상수로 정해지게 된다.

어쨌든 위 방식으로 페이징을 구현했는데 우리가 만든 앱에서 문제가 되진 않았다. Post 테이블의 row가 1000개도 안되었으니까..! 하지만 우리 앱의 요청 트래픽 중 가장 많은 부분을 차지하는 부분이고 분명히 데이터가 많이 쌓이면 쌓일수록 추후에 문제가 될 거라 생각하긴 했다. 아니나 다를까 리팩토링을 진행하면서 페이징 쿼리 방식에 대해 공부를 하고 있었는데 위 방식엔 문제가 있었다.


What is Problem?

그래서 문제가 정확히 뭔데? 백엔드 개발로 유명한 이동욱 님의 글을 참고했다!

https://jojoldu.tistory.com/528

Offset Problem

동욱 님의 글을 참고하면 알 수 있겠지만 Offset을 적용하면 Offset의 크기만큼 뛰어넘어 조회를 하는 방식이 아니라 Offset + Limit 만큼 읽은 다음 Offset 크기만큼 결과에서 버리는 방식이다. 즉 앞에서 읽었던 행을 요청할 때마다 읽는 방식이다. 데이터가 1억 건이라 가정하면 1억에 가까운 행을 풀 테이블 스캔하는 방식인 것이다.


Solution

이런 비효율적인 방식을 해결하기 위한 방법으로 인덱스를 이용한 No Offset 방법이 있다.

SELECT post_uid, title, created_at FROM post
ORDER BY created_at DESC
LIMIT 20 OFFSET skipValue;

기존에 위와 같은 쿼리로 페이징을 했다면 이제는 다음과 같이 직전 페이지의 마지막 글의 pk인 post_uid를 이용해 인덱스를 타도록 해 다음 페이지의 첫 조회할 글을 빠르게 찾아갈 수 있도록 하는 것이다.

SELECT post_uid, title, created_at FROM post
WHERE post_uid < '직전 페이지의 마지막 post_uid 값'
ORDER BY post_uid DESC
LIMIT 20;

해당 방식을 내가 진행했던 프로젝트에 적용하기 위해 post_uid를 순차적으로 증가하는 number 값으로 바꾸는 작업이 필요할 듯하다.


정리
페이징 쿼리를 효율적으로 처리하기 위한 No Offset 방법을 알아보았는데 정말 흥미로웠다. 데이터베이스 수업을 들으며 인덱스가 무엇이고 중요성 또한 깨닫고 있었지만 제대로 적용하는 법은 잘 몰랐는데 그 첫 번째 발걸음으로 동욱 님의 좋은 글을 보게 돼서 다행이다. 인덱스 또한 여러 종류의 인덱스가 있으며 학부 때 배웠던 내용을 바탕으로 정리하도록 해야겠다.

profile
개발 공부는 🌳 구조다…

0개의 댓글