PostgreSQL · 2026-04-03

PostgreSQL 커서와 페이징 성능 최적화 전략

PostgreSQL에서 커서를 활용한 대용량 페이징과 OFFSET/LIMIT의 성능 문제를 이해하고, 키셋 페이징과 서버사이드 커서로 페이징 성능을 개선하는 방법에 대한 설명

작성일 : 2026-04-03 ㆍ 작성자 : 관리자
post
목차

개요

대량의 데이터에서 페이징을 구현할 때 OFFSET/LIMIT 방식은 간단하지만 성능 저하를 일으킨다. 이 글에서는 PostgreSQL 커서의 기본 개념과 키셋 페이징, 서버사이드 커서를 활용한 페이징 성능 개선 방법을 초보자 관점에서 차근차근 설명한다.

왜 OFFSET/LIMIT은 느린가

OFFSET은 원하는 페이지까지 스킵하는 비용이 발생한다. 데이터가 많아질수록 스킵 비용도 증가한다. 특히 큰 OFFSET 값은 전체 스캔 또는 많은 행 건너뛰기를 동반해 쿼리 시간이 급격히 늘어난다.

문제 상황 요약

  • 대량 페이지 이동 시 쿼리 비용 증가
  • 정렬된 인덱스가 있어도 스킵 비용이 남음
  • 사용자 경험 저하 및 DB 부하 증가

대안 1: 키셋 페이징(keyset pagination)

키셋 페이징은 마지막으로 본 항목의 키를 기준으로 다음 데이터를 조회한다. OFFSET을 사용하지 않아 스킵 비용이 거의 없다. 특히 최신 데이터 위주 탐색에 적합하다.

핵심 아이디어

  • 정렬 컬럼(예: id, created_at)에 인덱스를 둔다.
  • 마지막 키 이후의 행을 WHERE로 제한해 페이징한다.

예시 SQL

-- 첫 페이지
SELECT id, title, created_at
FROM posts
ORDER BY created_at DESC, id DESC
LIMIT 20;

-- 이후 페이지 (마지막 항목의 키가 last_created, last_id)
SELECT id, title, created_at
FROM posts
WHERE (created_at < last_created)
   OR (created_at = last_created AND id < last_id)
ORDER BY created_at DESC, id DESC
LIMIT 20;

대안 2: 서버사이드 커서(postgres cursor)

서버사이드 커서는 클라이언트가 서버에 결과 집합을 열어둔 채로 일부만 가져가게 해준다. 네트워크 왕복과 메모리 사용을 줄일 수 있어 일괄 처리(batch processing)나 스크롤 형태의 페이징에 유리하다.

커서의 특징

  • 트랜잭션 범위 내에서 사용 시 일관된 결과를 제공
  • WITH HOLD 옵션으로 트랜잭션 종료 후에도 유지 가능
  • FETCH로 필요한 개수만큼 가져오므로 메모리 절약

SQL로 커서 사용 예

BEGIN;
DECLARE my_cursor CURSOR FOR
SELECT id, title FROM posts ORDER BY created_at DESC;

FETCH 100 FROM my_cursor; -- 100행씩 가져오기
-- 필요 시 반복 FETCH
CLOSE my_cursor;
COMMIT;

-- 트랜잭션 종료 후에도 사용하려면
DECLARE my_hold_cursor CURSOR WITH HOLD FOR
SELECT id, title FROM posts ORDER BY created_at DESC;

언어별 사용 예: Python(psycopg2)

애플리케이션 레벨에서는 클라이언트 커서를 사용해 서버사이드 커서를 제어할 수 있다. psycopg2의 named cursor를 활용하면 자동으로 서버사이드 커서가 생성된다.

간단한 Python 예

import psycopg2
conn = psycopg2.connect(dsn)
cur = conn.cursor(name='my_cursor')
cur.execute("SELECT id, title FROM posts ORDER BY created_at DESC")
for batch in iter(lambda: cur.fetchmany(100), []):
    for row in batch:
        process(row)
cur.close()
conn.close()

성능 튜닝 체크리스트

  • 필요한 컬럼만 조회해 네트워크 전송량을 줄임
  • 정렬 기준에 적절한 인덱스 생성
  • 키셋 페이징에서 비교 조건은 복합 인덱스 순서와 일치
  • 커서는 트랜잭션 범위를 고려해 사용. WITH HOLD는 리소스 장기 점유 가능
  • fetch size는 네트워크와 메모리 균형을 고려해 조정

언제 어떤 방법을 선택할까

다음 기준으로 선택하면 된다.

  • 사용자가 페이지 번호로 임의 접근한다면 키셋 페이징으로는 구현이 어렵다. 이 경우에는 범위를 제한한 쿼리나 적절한 캐싱이 필요
  • 연속적인 스크롤(무한스크롤)이나 대량 데이터 일괄 처리에는 서버사이드 커서가 효율적
  • 정확한 페이지 번호 접근성과 성능을 모두 원하면 키셋 페이징과 서버사이드 커서를 조합하거나, 페이지네이션 인덱스를 따로 설계하는 방법 고려

주의사항과 함정

  • 키셋 페이징은 정렬 기준 변경 시 재설계 필요
  • 커서는 서버 리소스를 사용하므로 장기간 열린 커서는 피함
  • WITH HOLD는 편리하지만 세션/트랜잭션 관리가 복잡해질 수 있음

요약

OFFSET/LIMIT은 단순하지만 대규모 데이터에서 비효율적이다. 키셋 페이징은 스킵 비용을 줄여 응답성을 개선하며, 서버사이드 커서는 메모리와 네트워크를 절약하면서 대량 처리를 가능하게 한다. 상황에 따라 두 기법을 적절히 조합하고, 인덱스 및 fetch size를 튜닝하면 PostgreSQL에서 페이징 성능을 크게 개선할 수 있다.

postgres cursor 사용법 페이징 성능 postgres 개선 offset limit 대체 postgres keyset pagination 서버사이드 커서 postgres 성능 튜닝 cursor paging psycopg2 cursor