catsridingCATSRIDING|OCEANWAVES
Data

페이징 쿼리 최적화하기

jynn@catsriding.com
Nov 03, 2023
Published byJynn
999
페이징 쿼리 최적화하기

Optimize Pagination

페이징은 사용자의 데이터 조회 경험을 향상시키는 중요한 기법입니다. 각 페이지에서 제한된 양의 데이터를 보여주어 사용자가 한 번에 표시될 정보의 양을 처리하고, 원하는 데이터를 쉽게 찾을 수 있도록 합니다. 또한 이 방식은 서버의 부하를 최소화하여 전체 시스템의 성능을 향상시킵니다.

그러나, 단순히 페이지당 조회 데이터 개수를 제한하는 것만으로는 효율적인 성능 향상에 한계가 있습니다. 페이징 쿼리는 데이터베이스 서버에서 충분히 비용이 큰 작업이며, 많은 양의 데이터를 처리해야 하는 경우 이 부하가 더욱 커질 수 있습니다.

이러한 제한을 극복하기 위해, 페이징 쿼리 자체를 최적화하여 데이터베이스 서버의 부하를 줄이고 성능을 향상시키는 다양한 전략이 제안되었습니다.

Prerequisite

아래에 제시된 간단한 POSTS 테이블을 활용하여 페이징 쿼리를 어떻게 작성하면 좋을지 더 알아보도록 하겠습니다:

MySQL
+-----------+---------------+---+
|Field      |Type           |Key|
+-----------+---------------+---+
|id         |bigint unsigned|PRI|
|pathname   |varchar(128)   |   |
|title      |varchar(255)   |   |
|preface    |varchar(255)   |   |
|created_at |datetime       |   |
+-----------+---------------+---+

Offset Pagination

SQL의 limit & offset 키워드를 활용한 방식은 페이징 기능을 구현하는 가장 기본적이고 보편적인 전략입니다. 쿼리를 통해 특정 수량의 데이터를 확보하고, 필요한 페이지로 쉽게 이동할 수 있습니다.

MySQL
select
    posts.id,
    posts.pathname,
    posts.title,
    posts.preface,
    posts.created_at
from
    POSTS posts
order by
    posts.id desc
limit 1000 offset 50000

Spring Framework에서도 limit & offset의 조합을 통한 페이징 구현에 다양한 인터페이스를 제공하고 있을 정도로 이 전략은 보편화되어 있습니다.

하지만, 이 방식은 데이터의 크기가 클수록 속도가 저하되는 치명적인 문제가 있습니다. offset 키워드의 경우, 특정 레코드까지 단계적으로 전체 레코드를 가져온 후 필요하지 않은 레코드를 버리는 역할을 합니다. 그래서 만약 50,000번째 레코드부터 1,000건의 데이터를 조회하는 경우, 실제로는 더 많은 51,000개의 레코드를 조회해야 합니다.

  • Pros:
    • 직관적 구현: SQL 쿼리의 limit & offset 절을 사용하는 것은 구현하기 매우 단순하며, 대부분의 SQL 기반 데이터베이스 시스템에서 지원합니다.
    • 페이지 이동의 자유: 사용자는 원하는 페이지로 즉시 이동할 수 있으며 이전 페이지나 다음 페이지로 이동하는 것 뿐 아니라, 특정 페이지로 직접 이동하는 것도 가능합니다.
  • Cons:
    • 비효율성: 큰 데이터셋에서는 offset 방식이 비효율적일 수 있습니다. offset은 단순히 지정된 수의 행을 건너뛰므로, 대량의 데이터를 다루는 경우에는 이 작업이 많은 처리 시간을 요구할 수 있습니다.
    • 데이터의 변동성: 데이터가 추가 혹은 삭제되는 동안 offset을 사용하면 일관성 없거나, 예상치 못한 결과를 보일 수 있습니다. 예를 들어 새 항목이 추가되면 이전 페이지에 있던 항목이 다음 페이지로 밀릴 수 있습니다.
    • 비실시간성: 실시간 업데이트를 반영하기가 어렵습니다. 페이지간 이동 중에 새 데이터가 들어오면, 사용자는 서로 다른 페이지에서 동일한 항목을 볼 수 있습니다.

Covering Indexes Pagination

인프런 CTO 이동욱님의 블로그에서 커버링 인덱스(Covering Indexes)를 활용한 페이징 쿼리 개선에 대해 자세한 설명을 제공하고 있습니다.

Covering Indexes는 한 쿼리를 처리하는 데 필요한 모든 데이터를 인덱스에 포함하는 방식을 말합니다. 이를 통해 데이터베이스는 실제 테이블에 대한 조회 없이도 인덱스에서 필요한 모든 정보를 추출할 수 있습니다.

페이징 쿼리를 작성하면서 만약 쿼리에 사용된 모든 컬럼이 인덱스가 지정되어 있다면, 앞서 살펴본 limit & offset 방식이 유용할 수 있습니다. 하지만, 모든 컬럼을 인덱싱하는 것은 여러 가지 이슈를 발생시킬 수 있습니다. 예를 들면, 과도한 디스크 공간을 소모하거나 인덱스 업데이트 시 성능 저하가 발생할 수 있습니다. 그러므로 인덱스는 신중하게 선택하여 사용해야 합니다.

따라서, 아래의 쿼리와 같이 필요한 데이터의 키 목록을 가져오는데 커버링 인덱싱으로 가능한 경우라면 이 전략을 고려해 보면 좋을 것 같습니다.

MySQL
-- 1. retrieving post_ids
select
    posts.id
from
    POSTS posts
order by
    posts.id desc
limit 1000 offset 50000

-- 2. retrieving data with post_ids
select
    posts.id,
    posts.pathname,
    posts.title,
    posts.preface,
    posts.created_at
from
    POSTS posts
where posts.id.in(:post_ids)
order by
    posts.id desc
  1. 키 리스트 조회: 우선, 조회하려는 페이지의 데이터에 해당되는 키 리스트를 커버링 인덱스가 적용된 컬럼을 기반으로 조회합니다.
  2. 데이터 조회: 그 다음, 이 키 리스트를 가지고 필요한 데이터를 추출합니다.

이 과정은 페이징 데이터를 조회하는 시점을 최대한 뒤로 미루는 방법입니다. 우선 키 리스트만을 빠르게 가져온 후, 이 키 리스트를 사용해서 실제로 필요한 데이터를 조회합니다. 불필요한 데이터 액세스를 최소화하며, 효율적인 페이징 쿼리를 가능하게 합니다.

  • pros:
    • 데이터 접근 최소화: 커버링 인덱스를 활용하여 처음에 필요한 키 리스트만을 빠르게 가져오고, 이를 통해 실제 필요한 데이터만 조회합니다. 이는 불필요한 데이터 액세스를 최소화하며, 데이터베이스 성능을 향상시킵니다.
    • 페이징 성능 향상: 대량의 데이터에서도 offset 없이 페이징하는 것이 가능하여 limit & offset 방식에서 발생하는 성능상의 문제를 해결합니다.
  • cons:
    • 구현 복잡성: 테이블 조인이나 조건 등이 많아질수록 매우 복잡한 쿼리를 작성해야 할 수 있습니다. 개발자가 해당 분야에 대해 적절한 이해를 가지고 있어야 합니다.
    • 인덱싱의 크기 증가: 커버링 인덱스를 사용함에 따라, 인덱스가 커지는 문제가 있을 수 있습니다. 이로 인해 디스크 공간의 사용이 증가하며, 이는 인덱스 관리의 복잡성을 높일 수 있습니다.

Keyset Pagination

Keyset Pagination은 레코드 트래킹 방식의 페이지네이션 전략입니다. Keyset은 페이징이 시작되는 특정 지점, 즉 고유한 ID 또는 키(Key)를 나타냅니다. 이는 커서(Cursor) 기반 페이지네이션 배치와 유사합니다. 이 전략은 limit & offset 접근법보다 더욱 빠르고 예측 가능한 성능을 제공하며, 중간에 데이터가 변경되더라도 안정적인 페이지네이션을 보장합니다.

특정 페이지로의 명시적인 이동보다는 끊임없이 계속되는 스크롤, 즉 무한 스크롤 (infinite scroll) 방식에 특화되어 있습니다. 당근마켓, 트위터, 또는 인스타그램과 같이 사용자가 생성한 데이터가 대규모이고 실시간 피드 업데이트를 지원해야 하는 애플리케이션에서 효율적인 페이지네이션 방식이라 할 수 있습니다.

offset 없이 구현하는 페이지네이션 방식을 의미하여 이를 No-Offset 또는 마지막 위치를 기반으로 한다고 해서 Left-Off 방식이라고도 합니다.

Don't use offset; instead remember where you "left off".

MySQL
select
    posts.id,
    posts.pathname,
    posts.title,
    posts.preface,
    posts.created_at
from  
    POSTS posts  
where  
    posts.id < :last_id
order by  
    posts.id desc  
limit 1000;

위 쿼리에서 last_id는 이전 요청에서 마지막으로 반환된 레코드의 ID입니다. 여기서 기준이 되는 where 절의 조건은 반드시 인덱싱되어 있어야 합니다.

limit & offset 방식과의 가장 큰 차이는 특정 레코드에 다다르는 과정에서 전체 데이터셋을 스캔할 필요 없이 필요한 데이터셋만 효율적으로 가져오기 때문에 조회 성능이 비약적으로 향상됩니다.

하지만, 현재 위치를 기준으로 다음 레코드의 시작 지점만 알기 때문에 특정 페이지로 이동하는 것이 복잡하거나 실질적으로 불가능할 수 있습니다.

  • Pros:
    • 높은 성능: No-Offset Pagination은 불필요한 데이터를 거쳐 가는 행위 없이 필요한 데이터만 즉시 로딩하는 강력한 특성을 가지고 있습니다. 특히, 데이터량이 많은 경우 이 방식의 성능 최적화 이점이 더욱 명확하게 드러납니다.
    • 대량 데이터 처리에 이상적: 대규모 데이터에 대한 처리에서는 offset 방식보다 훨씬 빠른 처리 속도와 효율성을 제공합니다. 블록을 건너뛰는 것 없이 다음 데이터 블록만 로드함으로써, 데이터 전달이 단순화되고 처리 속도가 높아집니다.
    • 실시간성: 실시간으로 데이터가 업데이트되는 환경에서는 현재 위치를 기준으로 새로운 데이터를 불러올 수 있으므로 실시간성을 유지하기에 용이합니다.
  • Cons:
    • 페이지 이동 제약: 이 방식은 특정 페이지로 바로 점프하는 기능을 제공하지 않습니다. 사용자는 단순히 다음 페이지나 이전 페이지의 이동만 가능하며, 원하는 페이지로 즉시 이동하는 것은 다른 페이징 방식에 비해 복잡하거나 실질적으로 불가능할 수 있습니다. 이는 사용자가 특정 순서에 따라 페이지를 조회해야 하는 작업에 제약을 초래할 수 있습니다.

  • Database
  • MySQL