두 가지 인덱스 전략을 JMeter로 테스트한 결과를 분석해보자. 동일한 데이터와 쿼리에 대해 각각 다른 인덱스 전략을 적용했을 때 성능이 어떻게 달라지는지 확인해보자.
SELECT o.order_id,
r.restaurant_id,
r.name AS restaurant_name,
c.food_type,
m.name AS menu_name,
o.order_status,
o.order_type,
o.created_at,
o.updated_at
FROM p_order o
JOIN p_menu m ON m.menu_id = o.menu_id
JOIN p_restaurant r ON r.restaurant_id = m.restaurant_id
JOIN p_category c ON c.category_id = r.category_id
WHERE o.user_id = 'testsec2'
AND o.is_deleted = false
AND o.order_status <> 'PENDING'
ORDER BY o.created_at DESC
LIMIT 10 OFFSET 0;

| 항목 | 값 | 설명 |
|---|---|---|
| 표본 수 (Samples) | 4000 | 총 요청 개수 |
| 평균 응답 시간 | 2088ms | 요청당 평균 응답 시간 |
| 최소 응답 시간 | 35ms | 최소 응답 시간 (비정상적으로 낮아 보임) |
| 최대 응답 시간 | 8507ms | 가장 오래 걸린 요청의 응답 시간 |
| 표준 편차 | 878.35 | 응답 시간 분포의 변동성 |
| 오류율 (%) | 0.00% | 실패율 |
| 처리량 (Throughput) | 35.2/sec | 초당 처리된 요청 수 |
| 수신 KB/초 | 133.53 KB/s | 서버에서 클라이언트로 수신된 데이터량 |
| 평균 바이트 크기 | 3888 bytes | 요청당 평균 데이터 크기 |
| 전송 KB/초 | 12.50 KB/s | 클라이언트에서 서버로 전송한 데이터량 |
@Table(name = "p_order", indexes = {
@Index(name = "idx_porder_user_created_status",
columnList = "user_id, created_at, is_deleted, order_status, menu_id, "
+ "order_id, order_type, updated_at")
})

| 항목 | 값 | 설명 |
|---|---|---|
| 표본 수 (Samples) | 4000 | 총 요청 개수 |
| 평균 응답 시간 | 697ms | 요청당 평균 응답 시간 |
| 최소 응답 시간 | 22ms | 최소 응답 시간 (비정상적으로 낮아 보임) |
| 최대 응답 시간 | 3551ms | 가장 오래 걸린 요청의 응답 시간 |
| 표준 편차 | 428.11 | 응답 시간 분포의 변동성 |
| 오류율 (%) | 0.00% | 실패율 |
| 처리량 (Throughput) | 137.2/sec | 초당 처리된 요청 수 |
| 수신 KB/초 | 520.89 KB/s | 서버에서 클라이언트로 수신된 데이터량 |
| 평균 바이트 크기 | 3888 bytes | 요청당 평균 데이터 크기 |
| 전송 KB/초 | 46.63 KB/s | 클라이언트에서 서버로 전송한 데이터량 |
@Table(name = "p_order", indexes = {
@Index(name = "idx_porder_user_created_status",
columnList = "user_id, created_at, is_deleted, order_status, menu_id, "
+ "order_id, order_type, updated_at")
})
| 측정 항목 | 커버링 인덱스 | 복합 인덱스 | 개선율 |
|---|---|---|---|
| 평균 응답 시간 | 2088ms | 697ms | 67% 개선 |
| 최대 응답 시간 | 8507ms | 3551ms | 58% 개선 |
| 처리량 | 35.2/sec | 137.2/sec | 290% 향상 |
| 표준 편차 | 878.35 | 428.11 | 51% 감소 |
처리량의 극적인 향상: 복합 인덱스를 사용했을 때 초당 처리 요청 수가 약 4배 가까이 증가했다. 35.2/sec에서 137.2/sec로 향상되었다.
평균 응답 시간 대폭 감소: 평균 응답 시간이 2088ms에서 697ms로 약 67% 개선되었다. 이는 사용자 경험에 직접적인 영향을 미치는 중요한 지표다.
최악의 경우 시나리오 개선: 최대 응답 시간도 8507ms에서 3551ms로 크게 줄어들었다. 이는 시스템 안정성과 일관성 측면에서 중요한 개선점이다.
응답 시간 일관성 향상: 표준 편차가 878.35에서 428.11로 감소해 응답 시간의 변동성이 크게 줄었다. 이는 사용자들이 더 일관된 경험을 할 수 있음을 의미한다.
두 테스트에서 동일한 인덱스 컬럼 구성을 사용했지만, 실제 데이터베이스 엔진에서 인덱스를 활용하는 방식에 차이가 있었던 것으로 보인다. 복합 인덱스 접근 방식이 주어진 쿼리 패턴에 훨씬 효율적으로 작동했다.
주어진 쿼리는 WHERE 절에서 user_id, is_deleted, order_status 조건을 사용하고 ORDER BY created_at DESC로 정렬하는데, 복합 인덱스 구성에서 이 조건들이 최적화되어 처리된 것으로 판단된다.
WHERE o.user_id = 'testsec2'
AND o.is_deleted = false
AND o.order_status <> 'PENDING'
ORDER BY o.created_at DESC
인덱스 순서가 user_id, created_at, is_deleted, order_status...로 되어 있어, WHERE 조건에 맞는 행을 빠르게 찾고 created_at으로 정렬된 결과를 효율적으로 가져올 수 있었다.
이번 테스트 결과는 적절한 인덱스 전략이 데이터베이스 성능에 얼마나 큰 영향을 미칠 수 있는지 명확하게 보여준다. 복합 인덱스 접근 방식은 커버링 인덱스에 비해 약 3-4배 더 나은 처리량과 67% 더 빠른 응답 시간을 제공했다.
실제 프로덕션 환경에서는 이런 성능 차이가 서버 자원 활용, 사용자 경험, 그리고 시스템 확장성에 상당한 영향을 미칠 수 있다. 따라서 데이터베이스 쿼리 패턴을 면밀히 분석하고 그에 맞는 최적의 인덱스 전략을 수립하는 것이 중요하다.

| 측정 항목 | 인덱스 없음 | 커버링 인덱스 | 복합 인덱스 |
|---|---|---|---|
| 표본 수 | 4,000 | 4,000 | 4,000 |
| 평균 응답 시간 | 6,306ms | 2,088ms | 697ms |
| 최소 응답 시간 | 232ms | 35ms | 22ms |
| 최대 응답 시간 | 15,287ms | 8,507ms | 3,551ms |
| 표준 편차 | 1,687.58 | 878.35 | 428.11 |
| 오류율 (%) | 0.00% | 0.00% | 0.00% |
| 처리량 (Throughput) | 28.0/sec | 35.2/sec | 137.2/sec |
| 수신 KB/초 | 106.25 KB/s | 133.53 KB/s | 520.89 KB/s |
| 평균 바이트 크기 | 3,888 bytes | 3,888 bytes | 3,888 bytes |
| 전송 KB/초 | 9.95 KB/s | 12.50 KB/s | 46.63 KB/s |
이 표를 통해 인덱스 없음 → 커버링 인덱스 → 복합 인덱스로 가면서 성능이 점진적으로 향상되는 것을 명확하게 볼 수 있다. 특히 복합 인덱스의 경우 인덱스가 없는 상황에 비해 처리량이 약 5배 향상되고, 평균 응답 시간은 약 9배 단축되었다.
커버링인덱스는 사실 단일 테이블의 모든 컬럼에 인덱스를 걸어줘야하는데, 결국 join해서 찾아야하다보니 이런 결과가 나왔다.
성능이 더 안좋은 그냥 복합인덱스, 잘못걸린 인덱스의 사례가 아닐까 싶다.