SARGable에 대해서 알아보자

Database

Posted 2025.11.05 14:00

By recoma

이슈 - 서버 터짐

이번 블로그 내용은 최근에(블로그 작성 시점) 발생한 “서버 작동 중지”라는 섬뜩한 경험을 기반으로 한다.

첫번째 사망

두 달 전, 특정 주제의 푸시 알림을 발송한 직후 서버가 터지는 일이 발생했다. 알림 발송후 약 1분 동안 트래픽이 급상승하면서 RDS Connection Pool이 한계치를 초과했고, 더 이상 데이터베이스에 접속하지 못한 서버가 그대로 다운된 것이다. 당시에는 ‘아, 푸시 알림 때문에 순간적으로 트래픽이 몰려서 생긴 문제구나’라고 단순하게 생각했다. 그래서 급한 대로 SQLAlchemy의 커넥션 풀 관련 설정을 일부 조정하는 선에서 이슈를 마무리했다.

두 번째 사망

그리고 두 달이라는 시간이 흘렀다. 그 사이 수많은 푸시 알림을 보냈지만 서버는 아무 문제 없이 튼튼하게 버텨주었다. 당연히 문제가 해결되었다고 생각했다. 하지만 지난주, 두 달 전과 동일한 주제의 푸시 알림을 다시 발송하자 거짓말처럼 서버는 정확히 똑같은 증상을 보이며 다운되었다.

원인?

뭔가 이상했다. 왜 다른 푸시 알림은 괜찮은데, 유독 이 주제의 푸시 알림에만 서버가 반응하는 걸까? Datadog을 통해 지표를 확인해 보니, 원인은 명확해졌다. 서버가 다운되기 직전, 특정 API의 지연 시간이 비정상적으로 치솟아 있었다.

P{x} Latency란? 전체 요청 중 {x}%가 특정 시간(해당 지표 중 P95에서는 24.4초) 이하로 처리되었음을 의미하는 성능 지표다. 소수의 극단적인 케이스를 제외하고 대부분의 사용자가 경험하는 지연 시간을 파악하는 데 유용하다. P95는 소수의 매우 느린 요청에 대해 파익을 쉽게 할 수 있고, P50은 중위 혹은 평균 응답시간을 파악할때 유용하다.

천문학적인 지연 시간이었다. 해당 API는 특정 주제의 데이터를 조회하는 단순한 로직이었고, 백엔드 로직보다는 데이터베이스 쿼리에서 병목이 발생했을 확률이 압도적으로 높다고 판단했다.

가설을 검증하기 위해 ORM이 생성한 SQL문을 추출하여 MySQL에서 직접 EXPLAIN ANALYZE를 실행했다. 결과는 충격적이었다. 예상대로 엄청난 수의 row를 스캔하고 있었고, cost 또한 매우 높았다. 분명히 해당 쿼리의 WHERE 절에 포함된 날짜 관련 컬럼에는 인덱스가 설정되어 있었는데, 어째서인지 옵티마이저가 인덱스를 전혀 사용하지 않고 있었다. 즉, 날짜 필터 없이 어마어마한 양의 데이터를 부르고 이걸 여러번 순회를 돌다가, 나중가서야 날짜 필터가 돌아갔던 것이다.

-- 문제가 된 쿼리 (예시)
SELECT *
FROM push_target_table
WHERE DATE(created_at) = '2025-11-06';

바로 DATE(created_at) 때문이었다. WHERE 절에서 인덱스가 걸린 컬럼을 함수로 가공하는 순간, 옵티마이저는 더 이상 인덱스를 활용할 수 없게 된다. 이것이 바로 Non-SARGable 쿼리의 대표적인 예다.

SARGable? Non-SARGable?

SARGable

SARGable은 “Search ARGument-able”의 줄임말로, 데이터베이스가 쿼리를 처리할 때 인덱스를 활용하여 효율적으로 검색할 수 있는 형태의 조건절을 의미한다.

SARGable 쿼리는 인덱스 구조를 그대로 활용하여 탐색 범위를 효과적으로 좁힐 수 있다. 예를 들어 WHERE created_at >= '2025-11-06' 같은 조건은 B-Tree 인덱스에서 특정 지점을 바로 찾아 스캔을 시작할 수 있게 해준다.

Non-SARGable

반면 Non-SARGable 쿼리는 인덱스 컬럼 자체를 가공(함수 사용, 연산 등)하기 때문에, 데이터베이스가 조건을 비교하기 위해 테이블의 모든 레코드를 하나씩 꺼내어 함수를 실행해야 한다.

created_at에 인덱스가 있더라도, WHERE DATE(created_at) = '2025-11-06' 조건은 모든 created_at 값에 DATE() 함수를 적용한 후에야 '2025-11-06'과 비교할 수 있다. 이는 결국 인덱스를 타지 못하고 Full Table Scan으로 이어져 성능 저하의 주범이 된다.

SARGable vs Non-SARGable 연산자/함수

위 목록은 대표적인 예시이며, 사용하는 데이터베이스 시스템에 따라 동작이 조금씩 다를 수 있다.

구분 SARGable Non-SARGable
비교 =, > ,<, >=, <=, BETWEEN, IN !=, <>, NOT IN
문자열 LIKE '검색어%' (접두사 검색) LIKE '%검색어' (중간, 접미사 검색), SUBSTRING(), LOWER() 등 함수 사용
날짜/시간 >= '특정일자', < '특정일자' DATE(), YEAR(), MONTH() 등 함수 사용
기타   컬럼에 직접 산술 연산 (column * 10 = 100)

결과

원인을 파악했으니 해결은 간단했다. 인덱스 컬럼인 created_at을 가공하지 않도록 쿼리를 수정하는 것이다. DATE() 함수를 사용하는 대신, 날짜 범위를 직접 지정하는 방식으로 변경했다.

-- 개선된 쿼리
SELECT *
FROM push_target_table
WHERE created_at >= '2025-11-06 00:00:00' AND created_at < '2025-11-07 00:00:00';

수정된 쿼리로 다시 EXPLAIN ANALYZE를 실행하자, cost가 약 85% 감소했고 옵티마이저가 인덱스를 정상적으로 사용하는 것을 확인할 수 있었다.

개선된 코드를 배포하고 10분 뒤, 지표는 즉각적으로 반응했다.

다음 날, 문제의 푸시 알림을 다시 한번 발송했다. 이전과 같이 트래픽은 급증했지만, 이번에는 서버가 다운되지 않았다. API 지표는 놀라울 정도로 안정적이었다.

P95 Latency 기준으로, 평상시에와 트래픽 증폭 시, 성능이 98% 감소되었다.

회고

이번 장애를 통해 두 가지 중요한 교훈을 얻었다.

  1. ORM을 맹신하지 말고, 배포 전에는 반드시 SQL 성능을 측정하자. ORM은 편리하지만, 내가 작성한 코드가 어떤 SQL로 변환되고 어떻게 실행될지 항상 의심하고 확인하는 습관이 필요하다. Postman이나 Jmeter, AB같은 테스트툴도 써 보고 EXPLAIN을 이용해서 리소스가 얼마나 발생하는지 확인하는 습관을 길러야 한다.
  2. SQL 성능 최적화는 개발자의 핵심 역량이다. 단순히 기능 구현을 넘어, 대용량 트래픽 환경에서 서비스 안정성을 책임지기 위해서는 SQL과 데이터베이스에 대한 깊이 있는 학습이 절실히 필요하다는 것을 깨달았다.

늘상 경험하는 거지만, 함부로 대충 개발해서는 안될 것 같다. 로직 하나를 작성하더라도 신중한 테도를 갖도록 하자.

후속 조치?

해당 이슈를 해결하고 나서, 한 가지 의문이 더 생겼다. 이 API는 별도의 필터 파라미터 없이 항상 같은 데이터를 조회하는데, 데이터가 바뀌는 시점은 매일 자정이다. 즉, 하루 동안은 몇 번을 호출하든 동일한 결과를 반환한다.

그렇다면 굳이 API를 호출할 때마다 매번 데이터베이스에 접근해서 커넥션을 소모하는 것이 과연 효율적일까?

가장 먼저 떠오르는 해결책은 캐싱(Caching)이다. 조회 결과를 캐시에 저장해두고 다음 요청부터는 캐시에서 바로 반환하면 DB 부하를 획기적으로 줄일 수 있다. 하지만 과거 캐시와 관련해 여러 이슈를 겪었던 경험이 있어, 이번에는 다른 방법을 모색해보고 싶었다.

그래서 생각한 대안은 배치 프로세스를 통한 데이터 사전 계산(Pre-calculation)이다.

  1. EventBridge를 사용해 매일 00시에 스케줄링된 이벤트를 발생시킨다.
  2. 이벤트는 AWS Lambda 함수를 트리거한다.
  3. Lambda 함수는 최적화된 SQL 쿼리를 실행하여 하루 동안 사용될 데이터를 미리 조회한다.
  4. 조회된 결과는 S3나 ElastiCache와 같은 특정 스토리지에 JSON 형태로 저장한다.
  5. 기존 API는 이제 데이터베이스에 직접 쿼리하는 대신, 이 스토리지에 저장된 데이터를 읽어와 반환하도록 수정한다.

이 방식을 통해 API 요청 시점에는 더 이상 데이터베이스에 접근하지 않게 되므로, 푸시 알림으로 인한 순간적인 트래픽 폭증에도 RDS 커넥션에 대한 걱정 없이 안정적으로 서비스를 운영할 수 있을 것이다.