MySQL InnoDB 내부 인덱스 구조 이해하기: PK 없는 테이블과 클러스터드 인덱스

Y_Sevin·2024년 4월 30일

😅 필자가 개인적으로 공부하고 남기는 글이라 정보가 미흡할 수 있습니다.
잘못된 내용이나 더 좋은 관점이 있다면 댓글로 알려주시면 정말 감사히 받겠습니다.

보통 실무에서 테이블을 설계할 때 Primary Key(PK) 없이 테이블을 만들 일은 거의 없다.
"각 행을 유니크하게 식별할 수 있어야 한다"는 기본 원칙 때문이기도 하고, ORM/JPA 같은 도구를 쓰면 사실상 PK가 없으면 운영이 힘들기 때문이다.

그런데 MySQL(InnoDB)은 PK가 없는 테이블 생성을 허용한다.
필자는 처음에 이게 이해가 잘 안 됐다.

  • PK가 없으면 행을 식별하기도 어렵고
  • 인덱스도 애매하고
  • 성능도 오히려 떨어질 것 같은데

굳이 왜 허용할까? 라는 생각이 들었다. 🤔
그러다 문서를 보다가 "아… 이래서구나" 싶은 지점이 있었다.

InnoDB는 PK가 없더라도 클러스터드 인덱스(Clustered Index) 를 내부적으로 만든다.

즉, InnoDB 입장에서는 "PK 없는 테이블"도 결국 클러스터드 인덱스 기반 구조로 저장되도록 설계되어 있었던 것이다.


클러스터드 인덱스란?

클러스터드 인덱스는 테이블의 데이터(레코드)가 특정 키 순서로 정렬되어 저장되는 구조이다.

  • "인덱스가 따로 있고 데이터는 따로 있는" 게 아니라
  • 인덱스의 리프 페이지(leaf page)가 곧 데이터 페이지인 형태

그래서 클러스터드 인덱스의 키(= 클러스터링 키)를 기준으로 레코드가 정렬되어 저장된다.

InnoDB에서는 보통 이 클러스터링 키가 Primary Key가 된다.
그래서 흔히 이런 말을 한다.

"InnoDB는 PK를 기준으로 클러스터드 인덱스가 만들어진다."

그리고 필자는 여기서 한 번 오해를 했다.

"PK가 있어야만 클러스터드 인덱스가 존재하는구나?"

근데 실제로는 반대에 가깝다.

  • InnoDB는 항상 클러스터드 인덱스 구조로 데이터를 저장하고
  • PK가 있으면 그 PK를 클러스터링 키로 쓰는 것뿐이다.

즉 "클러스터드 인덱스가 PK 때문에 생긴다"가 아니라,
"InnoDB는 클러스터드 인덱스가 기본이고, PK가 있으면 그걸 키로 쓴다"에 더 가깝다.


PK가 없는 테이블

PK가 없는 테이블이라고 해서 내부 동작이 똑같지는 않다.
InnoDB가 클러스터링 키를 고르는 우선순위가 있기 때문이다.

필자가 정리한 PK 없는 테이블은 크게 두 가지로 나뉜다.

  1. Unique 인덱스(UK)가 존재하는 테이블
  2. PK도 없고, UK도 없는 테이블

이렇게 나누는 이유는 단순하다.

InnoDB가 "클러스터드 인덱스 키로 쓸 만한 후보"를 찾는 방식이 달라지기 때문이다.


PK는 없지만 Unique 인덱스는 있다면?

이 경우 InnoDB는 테이블에 정의된 첫 번째 Unique 인덱스(NOT NULL 조건 포함)를
클러스터링 키로 선택한다.

  • PK가 없으면
  • "그래도 행을 유니크하게 식별 가능한 키"를 찾아야 하고
  • 그게 UK라면 성능/일관성 측면에서 제일 낫다

즉, PK를 안 만들었어도 UK가 사실상 PK 역할을 하게 되는 구조다.

주의: "아무 UK나" 되는 게 아니라, 클러스터링 키로 쓰려면 NOT NULL 조건이 중요한 포인트로 알려져 있다. (NULL 허용이면 유니크 보장이 애매해짐)


PK도 없고 UK도 없다면?

식별 가능한 컬럼이 없는데, InnoDB는 대체 뭘 기준으로 클러스터드 인덱스를 결정할까.

InnoDB는 이때 숨겨진 컬럼(hidden column) 을 하나 사용한다.

  • 각 행(row)에는 내부적으로 row_id 성격의 값이 있고
  • 이 값은 6바이트이며
  • 레코드가 insert될 때 순차적으로 증가한다

그래서 PK/UK가 없으면 InnoDB는 이 row_id를 기반으로
GEN_CLUST_INDEX라는 이름의 클러스터드 인덱스를 만든다.

그리고 이 동작은 MySQL 공식 문서에도 정리되어 있다.

MySQL docs를 살펴보면 아래와 같이 나와있다.

참고: https://dev.mysql.com/doc/refman/8.0/en/innodb-index-types.html


"그럼 왜 굳이 PK 없는 테이블을 허용할까?"

처음엔 이런 생각이 들었다.

PK, UK가 존재하는 경우에는 해당 컬럼을 통해 행에 빠르게 접근할 수 있으니 속도 측면에서 이점이 발생한다.
그런데 PK도 없고 UK도 없는 테이블은 보통 "특정 row를 찍어서 조회"하기보다는, 조건 없이 훑거나(풀스캔) 집계하는 용도로 쓰일 가능성이 높다.
그렇다면 굳이 클러스터드 인덱스를 만들어서 정렬까지 유지하는 게 무슨 의미가 있을까? 라는 의문이 들었다.

근데 지금 와서 보면 이 고민은… 클러스터드 인덱스의 장점을 제대로 고려하지 않은 고민이었다. 😅
클러스터드 인덱스는 데이터가 물리적으로(정확히는 InnoDB 페이지 구조 상) 정렬된 상태로 저장되는 구조이고,
따라서 전체 탐색을 주로 수행하는 테이블이라면 데이터에 순차적으로(sequential) 액세스할 확률이 높아져 I/O 효율이 좋아질 수 있다.

정리하면, PK/UK가 없어서 "조건 탐색"이 애매한 테이블이라도,
클러스터드 구조 자체가 스캔 비용에 영향을 줄 수 있기 때문에 InnoDB는 결국 클러스터드 인덱스 기반 저장을 기본값으로 유지하는 쪽을 택한 것으로 이해했다.

참고: https://stackoverflow.com/questions/18764/whats-the-difference-between-a-table-scan-and-a-clustered-index-scan

풀스캔/범위 스캔이 "생각보다 자주" 나옴

위에서 말한 것처럼, PK 없는 테이블은 특정 row를 찍기보단 스캔/집계 형태로 쓰일 가능성이 있다.
이 경우 클러스터드 구조는 데이터 접근 패턴을 더 "순차적"으로 만들어 줄 수 있고, 결과적으로 스캔 비용에 영향을 줄 수 있다.

PK 없는 테이블을 쓰는 상황 자체가 흔하진 않지만, 예를 들면 이런 경우가 있을 수 있다.

  • 임시성 로그/스테이징 테이블
  • 집계 결과를 잠깐 담는 테이블
  • ETL 중간 테이블
  • 정말 단순한 "append-only" 성격의 데이터

이런 경우는 특정 키로 딱 찍기보다,

  • "최근 N개"
  • "기간 범위"
  • "전체 한번 훑고 집계"

같은 패턴이 더 많을 수 있다.

클러스터드 인덱스는 레코드가 정렬되어 저장되므로
디스크 접근이 순차적(sequential) 으로 일어나기 쉬워지고,
이는 랜덤 I/O가 많은 테이블 스캔보다 유리해질 수 있다.

여기서 자주 언급되는 비교가 이거다.


PK 가 없다면

여기까지 읽으면 "PK 없어도 어차피 InnoDB가 알아서 만들어주네?" 싶을 수 있는데,
실무적으로는 PK를 두는 게 거의 항상 낫다.

왜냐면 "숨겨진 클러스터링 키"가 생기는 순간, 비용이 생긴다.

세컨더리 인덱스(Secondary Index)가 더 비싸진다

InnoDB의 세컨더리 인덱스는 리프에 "PK 값"을 같이 저장한다.
그래서 인덱스로 찾아간 뒤 PK로 다시 클러스터드 인덱스를 타고 실제 레코드를 찾는다.

  • PK가 짧고 고정이면(예: BIGINT)
    → 세컨더리 인덱스도 작고 효율적
  • PK가 없어서 숨겨진 row_id를 쓰거나, 너무 긴 PK를 쓰면
    → 세컨더리 인덱스 리프가 커지고, 캐시 효율이 떨어진다

즉 PK가 없다고 "PK 저장 비용이 사라지는 게 아니라",
다른 형태로 비용이 생길 수 있다.

애플리케이션/ORM 관점에서 다루기 어렵다

PK가 없으면

  • 업데이트/삭제가 곤란해지고
  • 중복 데이터 관리가 힘들어지고
  • 동시성 제어/정합성 유지가 어려워진다

"DB는 된다"와 "운영 가능한 설계"는 다른 문제라서,
실제로는 PK 없는 테이블이 점점 부담으로 돌아올 가능성이 크다.


결론

  • InnoDB는 기본적으로 클러스터드 인덱스 기반 저장 구조를 가진다.

  • 그래서 PK가 없더라도,

    • 적절한 Unique 인덱스가 있으면 그걸 클러스터링 키로 쓰고
    • PK/UK가 모두 없으면 내부 row_id로 GEN_CLUST_INDEX를 만들어 사용한다.
  • 따라서 "PK가 없으면 클러스터드 인덱스가 없다"는 오해는 틀렸고,
    오히려 "PK가 없어도 클러스터드 인덱스는 반드시 있다"가 핵심이다.

  • 다만 PK가 없으면 세컨더리 인덱스 비용/운영 난이도 등으로
    실무적으로는 PK를 두는 게 거의 항상 유리하다.

결론: InnoDB는 PK가 없더라도 모든 테이블을 클러스터드 구조로 유지하기 위해
내부적으로라도 클러스터링 키를 만들고, 그 결과 클러스터드 인덱스는 항상 존재한다…! 😅

profile
매일은 아니더라도 꾸준히 올리자는 마음으로 시작하는 개발블로그😎

0개의 댓글