🎯 인덱스(INDEX) 란 ?
먼저 인덱스(Index) 의 개념에 대해 먼저 알아봅시다.
인덱스는 테이블의 데이터를 더 빠르게 조회하기 위해 사용하는 자료구조 입니다. 책에 있는 목차를 보고 원하는 내용에 더 빠르게 접근할 수 있는 개념과 비슷합니다.
이러한 인덱스를 효율적으로 잘 쓰기 위해서는 선택도에 대해 잘 판단해야 합니다. 즉, 중복되지 않는 값이 많을수록(=선택도가 높을 수록) 인덱스 효과가 좋습니다. 예를들어 주민번호와 이메일같은 값은 선택도가 높아 인덱스를 걸면 효율적으로 접근할 수 있습니다. 반대로 성별, 국가명 등 중복이 많은 값들은 선택도가 낮아 인덱스가 비 효율적입니다.
🎯 어떤 값을 인덱스할까?
현재 설계되어 있는 집 게시물을 나타내는 Home 엔티티에는 집 상태(Home Status) 를 이용한 필터링 조회가 자주발생합니다. 즉, WHERE 절에 Status 가 자주 호출되는 상황입니다. 하지만 자주 필터링 된다해서 인덱스를 걸어버리면, 집 상태가 변경되었을때 (판매중 -> 판매 완료) 인덱스를 수정하는 비용 또한 발생할 수 있습니다.
이때, 집상태 변경에 대한 비용 VS 인덱스를 통해 얻을 수 있는 성능 개선을 비교해 인덱시 적용 여부를 판단해야 합니다.
그리고 지금 설계된 도메인은 루트 애그리거트(ROOT AGREEGATE) 를 기준으로 구현되어 있습니다. 즉, 서로 다른 도메인 영역은 연관관계로 매핑되어 있지 않고 PK만을 필드값으로 가지고 있습니다. 하지만 직접 수동으로 넣어준 PK 값은 JPA 에서 MYSQL 을 사용할때 자동으로 인덱스를 걸어주지 않습니다.
@Entity
@Getter
@Builder
@Setter
@AllArgsConstructor
@NoArgsConstructor
public class Home extends BaseTimeEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "home_id", nullable = false)
private Long id;
@Column(name = "user_id", nullable = false)
private Long userId; //수동으로 넣어준 User PK
...
}
원래 MYSQL 은 PK로 지정해준 값은 클러스터 인덱스로 자동 설정이 되지만, 아래와 같이 직접 PK 를 넣어 관리하면 별도의 인덱스 작업이 필요합니다.
정리해보면 ,
(1) 집 상태 인덱스
(2) 수동 PK 인덱스
두가지 값들에 대해 인덱스를 거는 과정에 대해 알아보겠습니다.
✅ 집 게시글과 User PK 인덱스
@Entity
@Getter
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Table(name = "home", indexes = {
@Index(name = "idx_home_user_id", columnList = "user_id")
})
public class Home extends BaseTimeEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "home_id", nullable = false)
private Long id;
@Column(name = "user_id", nullable = false)
private Long userId;
...
}
Home 클래스 위에 @Table 애노테이션을 이용해 user_id 를 인덱스로 만들어줬습니다. 이를 DB 에서 잘 적용되고 성능상 이점을 가져올 수 있는지 확인할 수 있습니다.
userId 를 이용한 조회 실행 계획
EXPLAIN SELECT * FROM home WHERE user_id = 1;
위 쿼리를 MYSQL 에서 실행하면 다음과 같은 결과를 확인할 수 있습니다.
type | ref | 좋은 성능을 의미하며, 인덱스를 사용해서 조건 필터링이 이루어졌음을 나타냅니다. |
possible_keys | idx_home_user_id | 사용할 수 있는 인덱스 목록 중 하나로, user_id에 대한 인덱스가 존재함을 나타냅니다. |
key | idx_home_user_id | 실제로 사용된 인덱스입니다. 예상대로 user_id 인덱스를 사용하고 있습니다. |
rows | 100 | MySQL이 이 조건에서 스캔할 것으로 예상하는 행의 수입니다. 데이터가 작아서 이 정도로 추정된 것으로 보입니다. |
filtered | 100.00 | 필터링된 행의 비율이 100%라는 뜻으로, 조건(user_id = 1)이 정확하게 매칭됨을 의미합니다. |
실행계획에서 확인할 수 있듯, user_id 인덱스가 잘사용되고 만약 인덱스가 사용되지 않을 경우 type = ALL 로 뜨고, 이는 풀 테이블 스캔을 의미해 비 효율적인 성능을 나타낸 것을 의미합니다.
✅ 복합 인덱스 적용
복합 인덱스는 왼쪽에서 오른쪽으로 순차적으로 작동합니다. 예를 들어 user_id 와 home_status 을 사용한 인덱스가 아래와 같이 있다 가정해봅시다.
INDEX (user_id, home_status)
즉, 아래와 같은 쿼리들은 인덱스를 정상적으로 사용할 수 있습니다.
WHERE user_id=?
WHERE user_id = ? AND home_status = ?
하지만 user_id 를 건너 뛰는 home_status = ? 와 같은 쿼리에서는 인덱스를 사용하지 않습니다.
이러한 복합 인덱스의 특성을 잘 고려해 설계 해야 합니다. 저 같은 경우 Home 에 있는 user_id 와 home_status 는 하나의 쿼리에서 필터링을 동시에 사용하는 메서드들이 많아 user_id, home_status 를 복합 인덱스로 걸어줄 예정입니다.
카디럴리티는 user_id 가 더 높으므로 해당 값을 우선적으로 인덱스를 걸어줘야 합니다. 그리고 HomeAddress 와 1:1 로 매핑되어 있고 HomeAddress 에 있는 필드값에서 WHERE 절을 사용할때 HomeStatus 와 같이 사용하는 메서드가 있어 HomeStatus 의 단일 인덱스도 같이 걸어줬습니다.
@Entity
@Getter
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Table(name = "home", indexes = {
@Index(name = "idx_home_user_status", columnList = "user_id, home_status"),
@Index(name = "idx_home_home_status", columnList = "home_status")
})
public class Home extends BaseTimeEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "home_id", nullable = false)
private Long id;
@Column(name = "user_id", nullable = false)
private Long userId;
@JsonIgnore
@OneToMany(mappedBy = "home", cascade = CascadeType.PERSIST, orphanRemoval = true, fetch = FetchType.LAZY)
private List<HomeImage> images;
@OneToOne(cascade = CascadeType.ALL)
@JoinColumn(name = "home_address_id")
private HomeAddress homeAddress;
@Embedded
private HomeInfo homeInfo;
@Enumerated(EnumType.STRING)
private HomeStatus homeStatus;
...}
자! 정리해보면 동시에 필터링이 걸리는 값들은 복합 인덱스를 고려해볼 수 있고, 이때 카디널리티를 기준으로 높은 값 부터 인덱스를 만들어줘야 합니다. 만약 복합 인덱스에서 우선적으로 선언된 필드 값이 아닌 필드가 다른 쿼리에서 필터링 조건으로 들어갈 수 있다면 단일 인덱스를 별도로 만들어줘 같이 사용하면 높은 성능을 기대할 수 있습니다.
'데이터베이스' 카테고리의 다른 글
[MySQL] 스토리지 엔진 락에 대해 알아보자 (0) | 2025.05.11 |
---|---|
[Database] 전체 텍스트 인덱스(Full-Text Index) 에 대해 알아보자 (0) | 2025.05.07 |
[Database] 커버링 인덱스를 이용한 페이징 성능 개선 (0) | 2025.05.05 |
[Database] No Offset 를 이용한 페이징 조회 개선하기 (0) | 2025.05.04 |
트랜잭션 격리 수준에 대해 알아보자 (0) | 2025.05.02 |