페이지를 끝까지 읽었는데 빠진다: offset·정렬·조회 중 변경

이 카테고리 글 목록

전체글 보기

다음 페이지를 끝까지 따라갔는데 같은 항목이 두 번 보이거나 일부가 빠진다면, 반복문의 종료 조건만 볼 일이 아니다. 페이지 사이에 목록이 바뀌었는지, 정렬이 모든 행의 순서를 확정하는지 확인해야 한다. offset은 앞에서 몇 행을 건너뛸지 정하므로 앞부분의 삽입·삭제가 다음 페이지의 대상을 밀어낼 수 있다.

아래 ID와 페이지는 로컬 Python 목록으로 만든 합성 예다. 실제 PostgreSQL 서버·REST API나 여러 사용자의 동시 요청을 실행하지 않았다. 공식 DB 설명은 원리를 이해하는 근거이며 임의 API의 일관성 보장으로 옮기지 않는다.

삭제 후 같은 offset을 쓰면 ID 3이 빠진다

오름차순 ID가[1,2,3,4,5,6]이고 두 건씩 읽는다고 하자. 첫 페이지에서[1,2]를 받은 직후 다른 주체가1을 삭제했다. 이제 목록은[2,3,4,5,6]인데 다음 요청이 offset2로 앞의 두 행을 건너뛰면[4,5]가 나온다. 끝까지 읽어도 조합한 결과는[1,2,4,5,6]이고3은 한 번도 수신하지 못한다.

initial = [1, 2, 3, 4, 5, 6]
first = initial[:2]
current = initial[1:]  # 첫 페이지 이후 ID 1이 삭제된 가정
seen = first + current[2:4] + current[4:6]
dedup = list(dict.fromkeys(seen))
missing = sorted(set(current) - set(dedup))
# seen == [1, 2, 4, 5, 6]
# missing == [3]

고유 ID로 중복을 지워도3을 새로 만들어 주지는 않는다. 또 결과의1은 처음 읽을 때는 존재했지만 마지막 목록에서는 삭제됐다. 이 조합은 어느 한 시점의 완전한 현재 목록이라고 보기 어렵다.

반대로 첫 페이지 뒤 맨 앞에0을 삽입하면 이후 offset2는[2,3]으로 시작한다. 최종 수신은[1,2,2,3,4,5,6]이 돼2가 반복되고, 마지막 목록에 있는0은 빠진다. 두 사례 모두 다음 페이지를 끝까지 읽었지만 입력 집합이 이동했다는 점이 핵심이다.

같은 평가 카드가 청록색과 노란색 두 목록으로 나뉘고 돋보기로 실패 카드를 살펴보는 삽화
같은 항목과 두 결과 목록을 비교하는 기존 AI 생성 개념도. 처음 기준 목록과 페이지를 이어 붙인 관측 결과의 대응을 살피는 맥락이다. 실제 동시 요청·페이지 이동 화면이나 누락 건수 도표는 아니다.

정렬이 있어도 동률의 순서가 남을 수 있다

PostgreSQL LIMIT/OFFSET 문서는 LIMIT을 쓸 때 결과가 유일한 순서가 되도록 ORDER BY를 지정해야 한다고 설명한다. 정렬 문서도 정렬을 지정하지 않은 반환 순서에 의존하지 말라고 한다. 하지만 수정 시각 하나로만 정렬했다면 같은 시각의 여러 행 사이 순서는 여전히 확정되지 않을 수 있다.

합성 예에서 A·B·C가 같은 시각이고 D·E·F는 그 뒤라고 두었다. 첫 조회의 유효한 순서는[A,B,C,D,E,F], 다음 조회의 동률 순서는[C,A,B,D,E,F]일 수 있다. 각 순서에서 offset0·2·4의 두 행을 가져오면[A,B,B,D,E,F]가 돼C가 빠진다. 이는 실제 DB가 그 순서를 반환했다는 실험이 아니라, 시각만으로는 배제되지 않는 동률 순서를 보여 주는 결정론적 반례다.

완전 정렬·keyset·snapshot은 다른 조건이다

방법 도움이 되는 지점 별도로 남는 조건
시각+유일 ID 정렬 동률 행의 순서 확정 조회 사이 입력 변경
keyset 진행 앞쪽 삭제로 밀리는 위치 회피 정렬 키 변경·뒤늦은 삽입
문서화된 snapshot 한 버전의 자료로 비교 유효 기간·권한·제품 계약

예를 들어 불변의 고유 ID 오름차순으로2까지 읽었다면, 다음에 ‘ID>2’를 읽는 keyset 방식은 앞의1이 삭제돼도3부터 이어갈 수 있다. 이 특정 합성 결과는1~6을 모았지만, 이미 읽은1의 삭제까지 결과에서 반영한 것은 아니다. 마지막 상태의 snapshot을 얻었다는 결론은 여전히 성립하지 않는다. 정렬 키가 수정되거나 새 항목이 진행 위치 앞에 들어오면 별도 누락·중복 조건도 생긴다.

cursor라는 이름만으로 snapshot이 보장되는 것도 아니다. 서버가 무엇을 인코딩하고 어떤 버전·기간·필터를 고정하는지 문서가 있어야 한다. 불투명 토큰을 임의로 분해하거나 숫자 순번처럼 비교하지 않는다.

DB의 트랜잭션 보장을 REST 페이지에 추정하지 않는다

PostgreSQL 격리 수준 문서에서 Read Committed의 연속 SELECT는 같은 트랜잭션 안에서도 다른 커밋을 볼 수 있다. 반면 같은 Repeatable Read 트랜잭션의 조회는 다른 트랜잭션이 나중에 커밋한 변경을 보지 않는 같은 snapshot을 사용한다. 자기 트랜잭션의 앞선 변경 등 문서 조건은 별개다.

이 설명은 PostgreSQL의 계약이다. 서로 다른 REST 페이지 요청이 같은 DB 트랜잭션을 공유한다는 보장은 아니다. 특정 시점 전체가 필요하면 서버가 제공하는 일관된 내보내기·버전 고정 기능과 유효 범위를 확인한다. 현재 목록을 결국 맞추는 목적이라면 삭제·변경분과 재대조 방법도 정해야 한다. 이는 작성자의 업무 구분이며 이번에 서비스를 연결해 검증한 절차는 아니다.

첫 페이지를 받고 멈춘 문제는 페이지 끝과 수집 범위 확인, 다음 실행의 변경분은 증분 진행 위치와 삭제 계약으로 나눈다. 도착 순서가 다른 push 사건은 웹훅 중복·역순 처리의 다른 문제다. 이미 모은 표의 결합에서 행이 늘었다면 JOIN 키 관계 점검으로 구별한다.

자주 묻는 질문

ID 중복 제거를 했으면 누락도 해결되나요?

이미 받은 동일 ID의 반복을 줄일 뿐, 한 번도 받지 못한 ID는 복원하지 못한다. 재대조 기준이나 서버의 일관성 계약 없이 고유 건수만으로 전체성을 선언하지 않는다.

ORDER BY에 유일 ID를 붙이면 충분한가요?

고정 자료에서 동률 순서를 확정하는 데 필요하다. 조회 중 삽입·삭제나 정렬 키 변경까지 막지는 않으므로 일관성 범위를 따로 정한다.

cursor API로 바꾸면 모든 변경 경합이 사라지나요?

제품 문서를 확인해야 한다. cursor가 단순 다음 위치인지, 고정된 자료 버전까지 포함하는지에 따라 다르다. 처리량이나 모든 동시 변경의 안전성을 이번 합성 예로 보장하지 않는다.

이 카테고리의 다른 글

전체글 보기

댓글 남기기