데이터베이스를 다루다 보면, 특히 수백만 건 이상의 대용량 데이터를 처리할 때 쿼리 속도가 급격히 느려지는 경험을 자주 하게 됩니다. "인덱스를 추가하면 빨라진다"라는 이야기는 많이 들어봤지만, 실무에서는 그보다 훨씬 더 다양한 원인과 해결책이 존재합니다. 이번 글에서는 실제 실무에서 자주 마주치는 대용량 데이터 쿼리 성능 저하 사례 3가지와 그 해결 방법을 소개합니다. 같이 한 번 알아봐보시죠!
불필요한 전체 테이블 스캔(Full Table Scan) 줄이기
문제 상황
한 전자상거래 플랫폼에서 최근 30일 주문 건수를 조회하는 쿼리가 10초 이상 걸렸습니다.
데이터는 orders 테이블에 약 5천만 건이 저장되어 있었고, 다음과 같은 쿼리를 사용하고 있었습니다.
SELECT COUNT(*)
FROM orders
WHERE order_date >= NOW() - INTERVAL 30 DAY;
실행 계획(EXPLAIN)을 확인해보니, 인덱스가 전혀 사용되지 않고 전체 테이블 스캔이 발생하고 있었습니다.
원인 분석
order_date 컬럼에는 인덱스가 없었습니다.
DB 엔진이 모든 레코드를 읽어 조건을 확인하는 방식으로 동작하고 있었음.
개선 방법
인덱스 추가
CREATE INDEX idx_order_date ON orders(order_date);
그리고 EXPLAIN으로 다시 실행 계획을 확인해보니, 인덱스 범위 스캔(Index Range Scan)이 적용되어 조회 속도가 비약적으로 향상되었습니다.
결과
개선 전: 10.2초 소요
개선 후: 0.15초 소요 (약 68배 향상)
💡 팁: WHERE 조건에 자주 쓰이는 컬럼은 꼭 인덱스를 추가하세요.
다만, 카디널리티(값의 다양성)가 낮은 컬럼(예: 성별, TRUE/FALSE)은 인덱스 효율이 낮을 수 있습니다.
서브쿼리 대신 조인(Join)으로 변경
문제 상황
마케팅 리포트를 만들 때, 고객별 최신 주문 내역을 가져오는 쿼리가 30초 이상 걸렸습니다.
SELECT customer_id, product_id
FROM orders
WHERE order_id IN (
SELECT MAX(order_id)
FROM orders
GROUP BY customer_id
);
원인 분석
내부 서브쿼리가 orders 테이블 전체를 스캔한 뒤, 외부 쿼리에서 다시 검색을 반복.
결과적으로 테이블 접근이 2번 이상 반복됨.
개선 방법
서브쿼리 → CTE + JOIN 형태로 변환
WITH latest_orders AS (
SELECT customer_id, MAX(order_id) AS max_order_id
FROM orders
GROUP BY customer_id
)
SELECT o.customer_id, o.product_id
FROM orders o
JOIN latest_orders lo
ON o.customer_id = lo.customer_id
AND o.order_id = lo.max_order_id;
결과
개선 전: 30.8초 소요
개선 후: 0.9초 소요 (약 34배 향상)
💡 팁: 서브쿼리는 읽기 쉬울 수 있지만, 큰 데이터에서는 조인 방식이 더 효율적입니다.
특히 상관 서브쿼리(Correlated Subquery)는 가능한 한 피하는 것이 좋습니다.
집계(Aggregation) 로직 사전 계산하기
문제 상황
데이터 분석 팀이 매일 조회하는 카테고리별 월간 매출 통계 쿼리가 1분 이상 걸렸습니다.
SELECT category_id, SUM(price * quantity) AS total_sales
FROM orders
WHERE order_date >= '2024-01-01'
AND order_date < '2024-02-01'
GROUP BY category_id;
원인 분석
매번 원시 데이터(raw data) 전체를 읽어 집계 계산을 수행.
1개월 치 데이터가 약 1천만 건에 달해 계산 비용이 매우 큼.
개선 방법
사전 집계 테이블(Aggregated Table) 도입
매일 새벽 배치 프로세스에서 카테고리별 매출 합계를 미리 계산해 저장합니다.
CREATE TABLE monthly_sales_summary (
month DATE,
category_id INT,
total_sales DECIMAL(15,2),
PRIMARY KEY (month, category_id)
);
배치 작업 예시:
INSERT INTO monthly_sales_summary (month, category_id, total_sales)
SELECT DATE_FORMAT(order_date, '%Y-%m-01') AS month,
category_id,
SUM(price * quantity) AS total_sales
FROM orders
GROUP BY month, category_id;
조회 시:
SELECT category_id, total_sales
FROM monthly_sales_summary
WHERE month = '2024-01-01';
결과
개선 전: 62.4초 소요
개선 후: 0.01초 소요 (약 6,200배 향상)
💡 팁: 대용량 데이터의 집계는 실시간으로 처리하기보다 ETL 파이프라인이나 물리적 집계 테이블을 활용하면 속도를 극적으로 개선할 수 있습니다.
마무리
이번 글에서 살펴본 3가지 사례는 모두 실무에서 빈번하게 발생하는 성능 문제입니다.
1. 불필요한 전체 테이블 스캔 → 인덱스 추가
2. 서브쿼리 다중 접근 → JOIN으로 변경
3. 대규모 집계 연산 → 사전 계산 테이블 사용
대용량 데이터 환경에서는 단순히 쿼리만 바꾸는 것이 아니라, 데이터 모델링, 인덱스 전략, ETL 구조까지 함께 고려해야 합니다. 무조건 “인덱스를 추가하자”가 아니라, 실행 계획을 분석하고, 데이터 특성에 맞춘 최적화를 하는 것이 핵심입니다.