νŽ˜μ΄μ§€λ„€μ΄μ…˜ 방법둠 비ꡐ(Offset vs Cursor)

2024. 2. 17. 17:17ㆍDatabase

νŽ˜μ΄μ§€λ„€μ΄μ…˜μ΄λž€?

OLTP ν™˜κ²½μ—μ„œ μ‚¬μš©μžμ—κ²Œ λ°˜ν™˜λ˜λŠ” 데이터듀은 λŒ€λΆ€λΆ„ νŠΉμ • κΈ°μ€€μœΌλ‘œ μ •λ ¬λœ 일뢀 데이터이닀. 

예λ₯Ό λ“€λ©΄ μ›Ήκ°œλ°œμ˜ κ°€μž₯ ν”ν•œ μ˜ˆμ‹œμΈ κ²Œμ‹œνŒμ΄ κ·Έλ ‡λ‹€. κ²Œμ‹œνŒμ€ 일반적으둜 ν•œ νŽ˜μ΄μ§€μ— μ•½ 20개의 κ²Œμ‹œκΈ€λ“€μ„ μ΅œμ‹ μˆœμœΌλ‘œ λ…ΈμΆœμ‹œν‚€κ³ , μ‚¬μš©μžκ°€ λ‹€μŒ νŽ˜μ΄μ§€λ‘œ λ„˜μ–΄κ°€κ²Œ 되면 21 ~ 40번째 데이터가 κ·Έμ œμ„œμ•Ό μ‘°νšŒλ˜λŠ” λ°©μ‹μœΌλ‘œ κ΅¬ν˜„λœλ‹€.

이 μƒν™©μ—μ„œ μ„œλ²„λŠ” μ‚¬μš©μžμ—κ²Œ 전체 κ²Œμ‹œκΈ€ 데이터λ₯Ό μ „λ‹¬ν•˜λŠ” 것이 μ•„λ‹Œ, μ‚¬μš©μžκ°€ μœ„μΉ˜ν•œ νŽ˜μ΄μ§€μ— ν•΄λ‹Ήν•˜λŠ” λ°μ΄ν„°λ§Œμ„ λ°˜ν™˜ν•˜λ„λ‘ API 섀계가 μ΄λ£¨μ–΄μ§€λŠ”λ° 이λ₯Ό νŽ˜μ΄μ§€λ„€μ΄μ…˜ 방법둠이라고 λΆ€λ₯Έλ‹€.

 

 

νŽ˜μ΄μ§€λ„€μ΄μ…˜ μ’…λ₯˜

일반적으둜 많이 μ‚¬μš©λ˜λŠ” νŽ˜μ΄μ§€λ„€μ΄μ…˜ κΈ°λ²•μ—λŠ” Offset 기반 νŽ˜μ΄μ§€λ„€μ΄μ…˜κ³Ό Cursor(Keyset) 기반 νŽ˜μ΄μ§€λ„€μ΄μ…˜μ΄ μ‘΄μž¬ν•œλ‹€.

Offset 기반

Offset 기반 νŽ˜μ΄μ§€λ„€μ΄μ…˜ 기법은 νŽ˜μ΄μ§€λ„€μ΄μ…˜μ„ λ‹€λ£° λ•Œ κ°€μž₯ 많이 λ“±μž₯ν•˜λŠ” 방식이닀.

λ°”λ‘œ μœ„μ—μ„œ μ˜ˆμ‹œλ‘œ λ“  κ²Œμ‹œνŒ κ΅¬ν˜„μ—λ„ 주둜 이 방식이 μ‚¬μš©λœλ‹€.

μ˜ˆμ‹œλ₯Ό μœ„ν•΄ post ν…Œμ΄λΈ”μ΄ μ•„λž˜μ™€ 같이 μž‘μ„±λ˜μ–΄ μžˆλ‹€κ³  κ°€μ •ν•˜μž. 이 λ•Œ 전체 row μˆ˜λŠ” 50,000이고, κΈ°μ€€ DBλŠ” Mysql이닀.

CREATE TABLE post (
    id INT AUTO_INCREMENT PRIMARY KEY,
    user_id INT,
    name VARCHAR(30),
    content TEXT,
    created_at TIMESTAMP
);

CREATE INDEX idx_post_created_at ON post(created_at);

이 λ•Œ Offset 기반 νŽ˜μ΄μ§€λ„€μ΄μ…˜ μΏΌλ¦¬λŠ” μ•„λž˜μ™€ 같이 μž‘μ„±λœλ‹€.

SELECT * 
FROM post
ORDER BY created_at DESC
LIMIT 20 OFFSET 20 * {page - 1};

쿼리가 μƒλ‹Ήνžˆ κ°„λ‹¨ν•˜κ²Œ κ΅¬μ„±λ˜λŠ”λ°, μ΄λŠ” Offset 기반 νŽ˜μ΄μ§€λ„€μ΄μ…˜μ˜ μž₯점 쀑 ν•˜λ‚˜μ΄λ‹€. ν΄λΌμ΄μ–ΈνŠΈ μΈ‘μ—μ„œλŠ” νŽ˜μ΄μ§€λ„€μ΄μ…˜μ„ κ΅¬ν˜„ν•˜κΈ° μœ„ν•΄ μ„œλ²„ 츑에 λ‹¨μˆœνžˆ νŽ˜μ΄μ§€ 정보와 νŽ˜μ΄μ§€ λ‹Ή 데이터 수, λ§ˆμ§€λ§‰μœΌλ‘œ μ •λ ¬ κΈ°μ€€ λ§Œμ„ μ „λ‹¬ν•˜λ©΄ λœλ‹€.

μœ„ 쿼리둜 첫번째 νŽ˜μ΄μ§€λ₯Ό μ‘°νšŒν•˜λŠ” μƒν™©μ˜ μ‹€ν–‰ κ³„νšμ€ μ•„λž˜μ™€ κ°™λ‹€.

1 νŽ˜μ΄μ§€ 쑰회 μ‹€ν–‰ κ³„νš

μ‹€ν–‰ κ³„νšμ„ 잘보면 쿼리가 DBμ—μ„œ μ ‘κ·Όν•œ rows μˆ˜κ°€ 단 20에 κ·ΈμΉ˜λŠ” 것을 μ•Œ 수 μžˆλ‹€. μ΄λŠ” 전체 50,000개의 rowλ₯Ό μ „λΆ€ ν™•μΈν•˜μ§€ μ•Šκ³  ν•„μš”ν•œ λΆ€λΆ„λ§Œμ„ 읽어내고 μžˆλ‹€κ³  λ³Ό 수 μžˆλ‹€. μ΄λŠ” 인덱슀λ₯Ό 톡해 이미 정렬이 λ˜μ–΄ μžˆλŠ” μƒν™©μ—μ„œ λ”± ν•„μš”ν•œ 데이터 λ§ŒνΌμ„ 읽을 수 μžˆμ—ˆκΈ° λ•Œλ¬Έμ΄λ‹€.

μ΄λŸ¬ν•œ νŒ¨ν„΄ λ•Œλ¬Έμ— μ΅œμ ν™”λœ Offset 기반 νŽ˜μ΄μ§€λ„€μ΄μ…˜ ν™˜κ²½μ—μ„œλŠ” ν…Œμ΄λΈ”μ˜ 데이터가 아무리 λ§Žμ•„λ„ 첫번째 νŽ˜μ΄μ§€λ₯Ό μ½μ–΄μ˜€λŠ”λ° μ˜€λž˜κ±Έλ¦¬μ§€ μ•ŠλŠ”λ‹€.

이제 λ‘λ²ˆμ§Έ νŽ˜μ΄μ§€λ₯Ό μ‘°νšŒν•˜λŠ” μ‹€ν–‰ κ³„νšμ„ ν™•μΈν•΄λ³΄μž.

2 νŽ˜μ΄μ§€ 쑰회 μ‹€ν–‰ κ³„νš

μ΄λ²ˆμ—λ„ 20개의 λ°μ΄ν„°λ§Œμ„ 읽을 κ²ƒμœΌλ‘œ 생각할 수 μžˆμ§€λ§Œ LIMIT OFFSET μΏΌλ¦¬λŠ” κ·Έλ ‡κ²Œ λ™μž‘ν•˜μ§€ μ•ŠλŠ”λ‹€.

λ§Œμ•½ LIMIT A OFFSET B라고 ν•œλ‹€λ©΄, DB μ˜΅ν‹°λ§ˆμ΄μ €λŠ” A+B만큼의 데이터λ₯Ό ν…Œμ΄λΈ”μ—μ„œ 읽어낸 λ’€ λΆˆν•„μš”ν•œ 뢀뢄은 λ²„λ¦¬λŠ” μ‹μœΌλ‘œ λ™μž‘ν•œλ‹€. DBκ°€ 인덱슀 탐색을 μˆ˜ν–‰ν•  λ•Œ μ˜€ν”„μ…‹ 정보λ₯Ό 수직적 탐색에 μ΄μš©ν•  수 μ—†κΈ° λ•Œλ¬Έμ— 이와 같이 λ™μž‘ν•œλ‹€.

λ”°λΌμ„œ Offset 기반 νŽ˜μ΄μ§€λ„€μ΄μ…˜μ€ νŽ˜μ΄μ§€κ°€ λ’€λ‘œ 갈수둝 읽어야 ν•  λ°μ΄ν„°μ˜ μ΄λŸ‰μ΄ λ§Žμ•„μ Έ μ„±λŠ₯이 μ €ν•˜λ˜λŠ” νŠΉμ„±μ„ κ°–λŠ”λ‹€.

μš”μ•½

  • κ΅¬ν˜„, 쿼리 ꡬ성이 μƒλ‹Ήνžˆ κ°„λ‹¨ν•˜λ‹€.
  • μ΅œμ ν™”λœ Offset 기반 νŽ˜μ΄μ§€λ„€μ΄μ…˜ ν™˜κ²½μ—μ„œλŠ” ν…Œμ΄λΈ” λ‚΄ 데이터가 아무리 λ§Žμ•„λ„ 첫 νŽ˜μ΄μ§€ 데이터λ₯Ό κ°€μ Έμ˜€λŠ”λ° μ˜€λž˜κ±Έλ¦¬μ§€ μ•ŠλŠ”λ‹€.
  • 확인해야 ν•  νŽ˜μ΄μ§€κ°€ λ’€λ‘œ 갈수둝, 읽어야 ν•˜λŠ” λ°μ΄ν„°μ˜ μ΄λŸ‰μ΄ μ„ ν˜•μ μœΌλ‘œ μ¦κ°€ν•˜μ—¬ μ„±λŠ₯이 μ €ν•˜λœλ‹€.

 

Cursor(Keyset) 기반

Cursor 기반 νŽ˜μ΄μ§€λ„€μ΄μ…˜ 기법은 λ¬΄ν•œ μŠ€ν¬λ‘€μ„ κ΅¬ν˜„ν•  λ•Œ ν”νžˆ μ ‘ν•  수 μžˆλŠ” 방식이닀.

μœ„μ—μ„œ μ‚¬μš©ν•œ ν…Œμ΄λΈ”μ„ κ·ΈλŒ€λ‘œ μ‚¬μš©ν•˜λŠ” μƒν™©μ—μ„œ Cursor 기반 νŽ˜μ΄μ§€λ„€μ΄μ…˜ μΏΌλ¦¬λŠ” μ•„λž˜μ²˜λŸΌ μž‘μ„±ν•  수 μžˆλ‹€.

SELECT *
FROM post
WHERE created_at < {:cursor}
ORDER BY created_at DESC
LIMIT 20;

μœ„ 쿼리λ₯Ό 보면 μ•Œ 수 μžˆμ§€λ§Œ, Cursor 기반 νŽ˜μ΄μ§€λ„€μ΄μ…˜μ„ κ΅¬ν˜„ν•˜κΈ° μœ„ν•΄μ„œλŠ” ν΄λΌμ΄μ–ΈνŠΈ μΈ‘μ—μ„œ μ»€μ„œ 데이터λ₯Ό λ”°λ‘œ μœ μ§€ν•˜λ‹€κ°€ μš”μ²­ μ‹œ 포함해주어야 ν•œλ‹€. μœ„ μ˜ˆμ‹œμ—μ„œλŠ” 크게 μ–΄λ €μ›Œλ³΄μ΄μ§€ μ•Šμ§€λ§Œ, μ •λ ¬ 기쀀이 λ§Žμ•„μ§€λŠ” ν™˜κ²½μ—μ„œλŠ” ν΄λΌμ΄μ–ΈνŠΈ μΈ‘μ΄λ‚˜ μ„œλ²„ μΈ‘ λͺ¨λ‘ κ΅¬ν˜„μ— 뢀담이 될 수 μžˆλ‹€.

ν•΄λ‹Ή 방식을 ν™œμš©ν•  λ•Œ κ°€μž₯ λ¨Όμ € μ‹€ν–‰λ˜λŠ” 쿼리의 μ‹€ν–‰ κ³„νšμ€ μ•„λž˜μ™€ κ°™λ‹€.

Cursor νŽ˜μ΄μ§€λ„€μ΄μ…˜ λ°©μ‹μ˜ 첫 쿼리

μ‹€ν–‰ κ³„νšμ„ 보면 Offset 기반 νŽ˜μ΄μ§€λ„€μ΄μ…˜ λ°©μ‹μ˜ 1νŽ˜μ΄μ§€ μš”μ²­ 쿼리와 μ™„μ „νžˆ λ™μΌν•˜λ‹€. 이λ₯Ό 톡해 Cursor 기반 νŽ˜μ΄μ§€λ„€μ΄μ…˜ 방식 λ˜ν•œ ν…Œμ΄λΈ”μ„ μ „λΆ€ ν™•μΈν•˜λŠ” 것이 μ•„λ‹ˆλΌ ν•„μš”ν•œ λΆ€λΆ„μ˜ 데이터 λ§Œμ„ 효율적으둜 읽을 수 μžˆμŒμ„ μ•Œ 수 μžˆλ‹€.

이제 μŠ€ν¬λ‘€μ„ 쑰금 더 λ‚΄λ €μ„œ λ‹€μŒ 20개의 데이터λ₯Ό λΆˆλŸ¬μ˜€λŠ” μƒν™©μ˜ 쿼리λ₯Ό μž‘μ„±ν•΄λ³΄μž. μœ„ 쿼리λ₯Ό μ‹€ν–‰ν–ˆμ„ λ•Œ λ§ˆμ§€λ§‰ λ°μ΄ν„°μ˜ created_at 칼럼 값은 '2023-12-30 10:35:25' μ΄λ―€λ‘œ 이λ₯Ό μ»€μ„œλ‘œ ν™œμš©ν•˜μž. μ‹€ν–‰ κ³„νšμ€ μ•„λž˜μ™€ κ°™λ‹€.

Cursor νŽ˜μ΄μ§€λ„€μ΄μ…˜ λ°©μ‹μ˜ λ‘λ²ˆμ§Έ 쿼리

νŽ˜μ΄μ§€μ˜ 값이 컀짐에 따라 읽어야 ν•˜λŠ” λ°μ΄ν„°μ˜ 양도 μ„ ν˜•μ μœΌλ‘œ μ¦κ°€ν•˜λŠ” Offset 기반 λ°©μ‹κ³ΌλŠ” 달리, Cursor 기반 방식은 μ—¬μ „νžˆ 20개의 rowλ§Œμ„ 읽어낸닀. μ΄λŠ” DBκ°€ μ»€μ„œ 값을 톡해 인덱슀 탐색 μ‹œμž‘ 지점을 κ³§λ°”λ‘œ 찾을 수 있기 λ•Œλ¬Έμ— κ°€λŠ₯ν•œ 일이닀.  λ”°λΌμ„œ ν•΄λ‹Ή 방식은 μ»€μ„œκ°€ λ’€λ‘œ 많이 κ°„ μƒν™©μ—μ„œλ„ 항상 μΌμ •ν•œ μ„±λŠ₯을 보μž₯ν•˜λŠ” νŠΉμ„±μ„ κ°–λŠ”λ‹€. 

κ·Έλ ‡λ‹€λ©΄ κ΅¬ν˜„ λ‚œμ΄λ„μ μΈ 문제만 ν•΄κ²°ν•˜λ©΄ 항상 Cursor 기반 νŽ˜μ΄μ§€λ„€μ΄μ…˜μ„ μ‚¬μš©ν•΄μ•Ό ν•˜λŠ” 것이 μ•„λ‹ˆλƒκ³  이야기할 수 μžˆλ‹€. ν•˜μ§€λ§Œ 이에도 μ œμ•½μ΄ μ‘΄μž¬ν•œλ‹€. λˆˆμΉ˜κ°€ λΉ λ₯Έ μ‚¬λžŒλ“€μ΄λΌλ©΄ 이미 μ•Œμ•„μ±˜κ² μ§€λ§Œ, μœ„ 예제 μΏΌλ¦¬λŠ” 큰 문제λ₯Ό μ•ˆκ³  μžˆλ‹€. λ§Œμ•½ μ™„μ „νžˆ 같은 μ‹œμ μ— μž‘μ„±λœ postκ°€ μ‘΄μž¬ν•˜μ—¬ created_at 칼럼이 μ™„μ „νžˆ λ™μΌν•œ rowκ°€ μ‘΄μž¬ν•  수 μžˆλ‹€λ©΄? 이 경우 Cursor 기반 방식은 좜λ ₯ 데이터에 일관성을 보μž₯ν•  수 μ—†λ‹€. cursor κ°’μœΌλ‘œ μ‚¬μš©λ˜λŠ” 데이터 집합은 ν…Œμ΄λΈ” μƒμ—μ„œ μœ μΌμ„±μ΄ 보μž₯λ˜μ–΄μ•Ό ν•œλ‹€λŠ” 것이닀. λ¬Όλ‘  cursor 셋에 μΉΌλŸΌμ„ μΆ”κ°€ν•˜μ—¬ μœ μΌμ„±μ„ 보μž₯ν•  수 μžˆμ§€λ§Œ 이 경우 인덱슀의 μˆ˜μ • 및 크기 증가, κ΅¬ν˜„μ˜ λ³΅μž‘μ„±μ΄ μ¦κ°€ν•˜λŠ” 문제 등이 λ°œμƒν•œλ‹€. 

λ˜ν•œ Cursor 기반 방식은 ν•œλ²ˆμ— μ—¬λŸ¬ 데이터λ₯Ό κ±΄λ„ˆ λ›°μ–΄μ•Ό ν•˜λŠ” μš”κ΅¬μ‚¬ν•­(ex. λΆˆμ—°μ†μ μΈ νŽ˜μ΄μ§€ 이동)μ—μ„œλŠ” 쒋은 λ°©μ•ˆμ΄ μ•„λ‹ˆλ‹€. μ™œ κ·ΈλŸ°κ±΄μ§€λŠ” μ—¬κΈ°κΉŒμ§€ 글을 μ½μ—ˆλ‹€λ©΄ κ³§λ°”λ‘œ μ΄ν•΄ν•˜λ¦¬λΌ μƒκ°ν•œλ‹€.

μš”μ•½

  • Cursor 기반 방식은 Offset 기반 방식에 λΉ„ν•΄ κ΅¬ν˜„μ΄ λ³΅μž‘ν•˜λ‹€.
  • μ΅œμ ν™”λœ Cursor 기반 νŽ˜μ΄μ§€λ„€μ΄μ…˜ ν™˜κ²½μ—μ„œλŠ” ν…Œμ΄λΈ” λ‚΄ 데이터가 아무리 λ§Žμ•„λ„ λΉ λ₯Έ 응닡 속도λ₯Ό 보일 수 μžˆλ‹€. λ˜ν•œ μ»€μ„œκ°€ 점차 λ’€λ‘œ 가더라도 μΌμ •ν•œ μ„±λŠ₯을 보μž₯ν•œλ‹€.
  • Cursor 기반 λ°©μ‹μ—μ„œ μ»€μ„œ κ°’μœΌλ‘œ ν™œμš©λ˜λŠ” 데이터 셋은 ν…Œμ΄λΈ” λ‚΄μ—μ„œ μœ μΌμ„±μ΄ 보μž₯λ˜μ§€ μ•Šμ„ 경우 κ²°κ³Ό μ§‘ν•©μ˜ 일관성을 보μž₯ν•  수 μ—†λ‹€.
  • ν•œλ²ˆμ— μ—¬λŸ¬ 데이터(νŽ˜μ΄μ§€)λ₯Ό κ±΄λ„ˆ λ›°μ–΄μ•Ό ν•˜λŠ” μš”κ΅¬μ‚¬ν•­μ΄ μ‘΄μž¬ν•œλ‹€λ©΄ 쒋은 선택지가 μ•„λ‹ˆλ‹€.

 

 

μ—¬λ‹΄

μœ„μ—μ„œ μ‚΄νŽ΄λ³Έ νŽ˜μ΄μ§€λ„€μ΄μ…˜ 기법듀은 μ–Έμ œκΉŒμ§€λ‚˜ ν…Œμ΄λΈ” μΈλ±μŠ€κ°€ μ μ ˆν•˜κ²Œ ꡬ성이 λ˜μ–΄ μžˆμ–΄μ•Ό μ„±λŠ₯ 상 이점을 λ³Ό 수 μžˆλ‹€.

μΈλ±μŠ€κ°€ 없어도 쿼리의 결과집합이 λ‹¬λΌμ§€κ±°λ‚˜ ν•˜μ§€λŠ” μ•Šμ§€λ§Œ κ²°κ΅­ μ •λ ¬λœ 데이터λ₯Ό 기반으둜 λΆ€λΆ„ 처리λ₯Ό μˆ˜ν–‰ν•˜λŠ” 방식이기 λ•Œλ¬Έμ— ν•„μš”ν•œ 데이터 λ§Œμ„ DBμ—μ„œ 읽기 μœ„ν•΄μ„  μ •λ ¬ μž‘μ—…μ„ κ±΄λ„ˆ λ›Έ 수 μžˆλŠ” 인덱슀 섀계가 ν•„μš”ν•˜λ‹€.

λ§Œμ•½ 그렇지 μ•Šλ‹€λ©΄, 맀번 ν…Œμ΄λΈ” 전체λ₯Ό ν™•μΈν•œ λ’€ λͺ©ν‘œ 데이터λ₯Ό λ”°λ‘œ κ±ΈλŸ¬μ£ΌλŠ” μ‹€ν–‰ κ³„νšμ΄ λ„μΆœλ˜κΈ° λ•Œλ¬Έμ— νŽ˜μ΄μ§€λ„€μ΄μ…˜ λ„μž…μ„ ν†΅ν•œ μ„±λŠ₯ κ°œμ„ μ˜ 이점은 λ°”λž„ 수 μ—†λ‹€.