프로그래밍/CS Essentials

데이터베이스 인덱스 기본 개념 (DB Index Concept)

kugorang 2025. 1. 16. 16:28

들어가며

며칠 전, 모 기업의 직무 테스트를 보러 갔다. CS 기초에 대한 필기 시험이었는데 그 중, 데이터베이스의 Index에 대한 내용을 모두 작성하지 못하여 부끄러운 마음에 다시 공부를 해야겠다는 생각이 들었다.

 

Index는 DB의 높은 TPS(Transcations Per Second)와 빠른 응답 속도의 필요성으로 중요하게 여겨진다. 이 때문에 면접 및 실무에서 자주 다뤄지는 핵심 주제이고, 내가 지원했던 모바일 게임 서버에서 특히 Index 설계가 중요하다는 것을 먼저 밝힌다.

728x90

Index란?

데이터베이스에서 검색 성능을 높이기 위해 특정 컬럼(Column) 혹은 컬럼들의 값을 빠르게 찾아갈 수 있도록 추가적인 자료 구조를 마련해 두는 것을 의미한다. 쉽게 말해 책의 목차색인(index)과 비슷한 개념이다. 책에서 원하는 내용을 찾으려면 처음부터 끝까지 일일이 읽을 수도 있지만, 목차(색인)를 보면 훨씬 빠르게 해당 페이지로 이동할 수 있는 것과 유사하다.

  • 예시
    • 책의 목차: “p. 57 = 3장, p. 120 = 5장”과 같은 형태로, 무엇이 어디 있는지 간략히 표시
    • DB 인덱스: “유저 ID = 1003 → 테이블의 57번 레코드 위치”와 같이, 검색 조건이 실제 데이터가 저장된 위치를 빠르게 안내

인덱스를 두면 SELECT 쿼리(읽기 쿼리) 시 원하는 데이터를 빠르게 찾을 수 있어 응답 지연(latency) 감소 효과를 얻는다. 반면, 인덱스가 존재하면 INSERT, UPDATE, DELETE 등 쓰기 작업에는 인덱스 자료 구조도 함께 갱신해야 하기 때문에 추가 오버헤드가 발생한다.

 

Index 자료 구조

B-Tree 인덱스

B-Tree(Balanced Tree) 기반 인덱스는 가장 흔하게 쓰이는 인덱스 구조이다. MySQL의 InnoDB, PostgreSQL 등 많은 RDB(관계형 데이터베이스)가 기본 인덱스로 B-Tree를 채택하고 있다.

  • 특징
    1. 정렬된 트리 구조를 유지하여, 범위 검색(Range Query)이 유리하다. (ex. WHERE level BETWEEN 10 AND 20)
    2. 트리 높이가 일정 범위 내에서 균형을 이루도록 설계되어, 검색에 드는 시간이 상대적으로 예측 가능하다.
    3. 실제 구현에서는 노드에 데이터가 아닌 페이지 포인터를 저장하는 B+Tree 형태를 쓰는 경우가 많아, 대용량 처리를 더 효율적으로 수행한다.

B+Tree 인덱스

  • B+TreeB-Tree의 변형된 형태로, 데이터를 실제로 저장하는 노드(Leaf Node)는 리프 레벨에만 존재하고, 상위 내부 노드(Internal Node)들은 검색을 위한 키(key)와 자식 노드 포인터만 가지고 있다.
  • 이 구조 덕분에 리프 노드들이 연결 리스트(Linked List) 형태로 이어져 있어 범위 조회(Range Scan)가 매우 효율적이다.
  • 실제로 MySQL(InnoDB), PostgreSQL 등 대부분의 RDB 엔진은 B+Tree 구조를 인덱스 구현에 사용하고 있다. 하지만 보통은 문서에서 편의상 “B-Tree 인덱스”라고 뭉뚱그려 부르는 경우가 많다.

B-Tree vs B+Tree 간단 비교

구분 B-Tree B+Tree
데이터 저장 내부 노드리프 노드에 모두 저장 가능 리프 노드에만 실제 데이터(혹은 데이터 포인터)를 저장
노드 간 연결 내부 노드가 깊어질 때 검색 경로가 달라질 수 있음 리프 노드들이 연결 리스트로 연결되어 있어, 범위 검색 시 순회(Scan)가 편리
활용 사례 파일 시스템, 간단한 DB 구조 등 이론적·실무적으로 다양 대부분의 상용 RDB 엔진이 실제로는 B+Tree 구조 채택 (MySQL, PostgreSQL, MSSQL 등이 대표적 예시)

B+Tree는 인접한 리프 노드를 포인터로 연결해두기 때문에, “WHERE score BETWEEN 100 AND 300” 같은 범위 검색을 할 때 리프 노드들을 순차적으로 쉽게 탐색할 수 있다는 장점이 있다.

 

왜 교재나 문서에서 “B-Tree 인덱스”라고만 할까?

  1. 용어의 단순화
    • 많은 DB 교재나 엔진 문서에서, “B+Tree”라는 정확한 구현을 굳이 구분하지 않고 “B-Tree”로 통칭하여 설명하는 경우가 많다.
    • 이론적인 B-Tree와 B+Tree가 큰 줄기는 같고, 세부 구현이 다를 뿐이므로 개념을 이해하기에는 B-Tree라는 용어만 써도 무방하다고 보는 것이다.
  2. 역사적·관행적 이유
    • 기존에 B-Tree가 제안된 이후, 여러 변형(예: B+Tree, B*Tree)이 나왔다.
    • RDB 엔진 대부분이 결국 B+Tree를 사용하지만, “B-Tree 인덱스”라는 용어가 오랫동안 널리 쓰여 왔기에 관행적으로 유지되는 경향이 있다.
  3. 엔진 문서에서의 단순 표기
    • MySQL의 InnoDB나 PostgreSQL 매뉴얼도 내부적으로 B+Tree 구조라고 언급하면서도, 타이틀은 ‘B-Tree Index’로 부르는 경우가 있다.
    • 사용자가 굳이 B-Tree와 B+Tree 차이를 몰라도, 인덱스 최적화 개념을 이해하는 데 큰 지장이 없다는 인식이 있기 때문이다.

Hash 인덱스

Hash 구조는 빠른 동등 비교(Equal Lookup)에 최적화되어 있다. 예를 들어, “유저 ID가 1001인 레코드”를 정확히 찾을 때 해시 인덱스가 유리하다. 하지만 범위 검색에는 적합하지 않다. MySQL에서 MyISAM, InnoDB 등 주요 스토리지 엔진은 기본적으로 B-Tree 인덱스를 쓰고, Hash 인덱스는 주로 Memory 엔진 등에 활용된다.

기타 특수 인덱스

  • Full-Text Index: 텍스트 검색에 특화된 인덱스(ex. 게시판 글 내용 검색)
  • R-Tree: 공간(Spatial) 검색을 위한 인덱스로, 위치 기반 서비스를 다룰 때 사용
  • GiST / GIN (PostgreSQL): 텍스트, 레인지 타입 등에 사용되는 범용 인덱스 프레임워크

Index 작동 원리와 효과

작동 원리

  1. 인덱스 생성 시
    • DB는 지정된 컬럼의 값을 기반으로 트리(또는 해시) 구조를 만들고, 이를 정렬 또는 해시화하여 빠르게 검색이 가능한 상태로 준비한다.
    • ex. CREATE INDEX idx_user_name ON user_table (name);
  2. 조회(SELECT) 시
    • 쿼리에 WHERE name = 'Alice'와 같은 조건이 있으면, DB 엔진은 우선 인덱스(정렬·해시 구조)를 조회하여 해당 데이터의 실제 물리적 위치(Row Pointer)를 바로 찾아낸다.
    • 테이블 전체를 스캔(Full Table Scan)하지 않고도 원하는 레코드를 빠르게 확인합니다.
  3. 쓰기(INSERT, UPDATE, DELETE) 시
    • 인덱스가 존재하는 컬럼을 수정하거나 새로운 레코드를 삽입·삭제할 때, 인덱스 구조도 같이 변경되어야 한다.
    • 결과적으로 쓰기 성능(특히 대량 데이터 삽입)이 떨어질 수 있으니, 필요한 만큼만 인덱스를 생성하는 것이 중요하다.

조회(SELECT) 시 우선 인덱스를 조회하는 원리

‘책의 목차(색인)’ 역할을 한다는 비유

  • 인덱스(Index)는 테이블 전체 데이터를 일일이 뒤지지 않고, “찾고자 하는 행(Row)이 어디쯤에 있는지”를 빠르게 알려주는 별도의 자료 구조이다.
  • 책을 읽을 때 원하는 주제(키워드)가 몇 페이지에 있는지 알려주는 목차(색인)처럼, 인덱스는 특정 키(예: user_id, username)가 테이블(실제 데이터)에서 어떤 위치에 있는지 빠르게 안내한다.

인덱스 이용 예시

SELECT * FROM user_table WHERE username = 'Alice'
  1. DB 엔진은 username 컬럼에 대한 인덱스가 있는지 확인한다.
  2. 인덱스에 'Alice'라는 키 값이 존재하는지, 그리고 그 값이 테이블(Heap) 내에서 어떤 레코드 위치를 가리키는지 조회한다.
  3. 인덱스가 “username=‘Alice’는 테이블의 1003번 Row Pointer”라고 알려주면, DB 엔진은 테이블 본문(Heap)에서 1003번 행을 찾아 바로 읽어온다.
  4. 이 과정을 통해 테이블 전체를 스캔(Full Table Scan)하지 않아도 되기 때문에, 큰 테이블에서도 매우 빠른 검색이 가능하게 된다.

범위 검색(Range Scan) 예시

SELECT * FROM score_table WHERE score BETWEEN 100 AND 300
  1. B+Tree 인덱스(일반적으로 “B-Tree 인덱스”라고 불리지만, 실무 구현은 B+Tree)라면, 정렬된 트리 구조를 따라 ‘score >= 100’ 지점부터 ‘score <= 300’ 지점까지 리프 노드(Leaf Node)를 순회한다.
  2. 그 과정에서, 100~300 범위에 속하는 스코어 값만 빠르게 읽어올 수 있다.
  3. 테이블 전체를 살펴볼 필요가 없으니, 쿼리 실행 시간이 대폭 단축된다.

 

“SELECT 시 우선 인덱스를 본다”는 것은, 테이블 전체를 뒤지기 전에 인덱스(특정 키 값들의 ‘위치 정보’를 미리 정렬·구조화해 둔 자료)를 사용해 정확히(또는 범위 내에서) 해당 데이터가 있는 곳으로 곧장 이동한다는 뜻이다.

 

장점과 단점

  • 장점: 검색 속도 향상, 쿼리 응답 지연 감소, 범위 검색 및 조건 검색 최적화
  • 단점: 테이블에 대한 DML(INSERT, UPDATE, DELETE) 작업 시 오버헤드 증가, 인덱스를 유지하기 위한 디스크 공간 추가 소모

DML이란?

  • DB의 데이터를 조작(Manipulation)하는 명령어들을 가리킨다.
  • ex. INSERT (행 추가), UPDATE (행 수정), DELETE (행 삭제)

인덱스가 있을 때 DML은 왜 부담이 커질까?

  1. 인덱스 또한 ‘데이터 구조’이기 때문
    • 인덱스는 따로 떨어진, 정렬된 트리나 해시 구조로 유지된다.
    • 즉, 테이블의 데이터를 바꾸면, 인덱스동시에 바뀌어야 한다.
      • INSERT 시 → 새 행(row)이 들어갔으니, 인덱스 구조에도 새 키(key)와 위치(pointer)를 추가
      • UPDATE 시 → 인덱싱된 컬럼의 값이 바뀌면, 인덱스에서도 그 값을 삭제 후 재삽입 혹은 재정렬
      • DELETE 시 → 해당 행이 삭제되면, 인덱스 구조에서도 해당 키와 포인터 정보를 제거
  2. 인덱스 개수만큼 작업량이 늘어남
    • 만약 테이블에 인덱스가 여러 개 있다면, 모든 인덱스 구조에 대해 삽입, 수정, 삭제를 해줘야 한다.
    • 인덱스가 1개인 경우보다, 5개인 경우는 5배 더 많은 조정이 필요할 수 있다.
  3. 데이터 정렬 및 균형(트리 구조) 유지 비용
    • B+Tree 인덱스는 트리 높이를 일정 수준으로 균형 있게 유지해야 하므로, 삽입·삭제 시에 내부적으로 노드 분할(Node Split), 노드 병합(Node Merge), 리밸런싱(Rebalancing) 같은 과정을 수행할 수 있다.
    • 이것이 추가 CPU 연산디스크 I/O를 발생시킨다.

구체적 예시

INSERT INTO user_table (username, score) VALUES ('Bob', 250)
  1. 테이블(Heap)에 삽입
    • user_table에 신규 행이 추가
  2. 인덱스 구조에도 삽입
    • username 컬럼에 인덱스가 있다면, 'Bob'이라는 키를 B+Tree 인덱스에 추가
    • score 컬럼에 인덱스가 있다면, 250이라는 키 역시 트리에 삽입, 정렬 구조 유지
  3. 삽입 건수가 많을수록 인덱스에 대한 업데이트 비용이 누적되어, 쓰기 성능(Write Throughput)이 떨어질 수 있음

작동 원리의 논리적 근거

  1. 데이터 무결성(Consistency) 유지
    • DB는 “인덱스가 실제 데이터와 항상 일치”해야 한다는 원칙(ACID 중 Consistency)을 지켜야 한다.
    • 인덱스에만 삽입·삭제가 이뤄지거나, 테이블 데이터만 변경된 채 인덱스가 방치되면 데이터 불일치가 발생한다.
    • 따라서 테이블의 어떤 행이 변할 때마다, 인덱스 구조도 함께 수정해야 하는 부담이 생긴다.
  2. 정렬 + 포인터 유지
    • 인덱스(B+Tree)는 키값이 정렬된 상태를 유지하기 위해 각 노드가 연결 리스트 형태로 이어지고, 노드 간의 균형을 맞춘다.
    • 중간 지점에 새 값이 들어오면 “노드를 나눠야(Node Split) 하나?”, “리프 노드를 재연결해야 하나?” 같은 작업이 필연적으로 뒤따른다.
  3. 실무적 접근
    • “쓰기가 많은 테이블”은 인덱스 수를 너무 많이 두지 않는 것이 일반적인 권장사항이다.
    • 반면 “조회(SELECT)가 많은 테이블”은 인덱스가 많아도 이점이 클 수 있으므로, 쓰기 부담과 조회 이득의 트레이드오프(Trade-Off)를 항상 고민해야 한다.

Index 설계 고려사항

  1. 자주 쓰는 쿼리 패턴 파악
    • WHERE 조건, JOIN, GROUP BY, ORDER BY가 걸리는 컬럼 위주로 인덱스를 구성한다.
    • 무작정 모든 컬럼에 인덱스를 걸면, 오히려 쓰기 작업 부담이 커진다.
  2. 선택도(Selectivity) 고려
    • 특정 컬럼이 예/아니오(boolean)처럼 값이 한정적이면 인덱스 효율이 떨어진다.
    • ex. 성별(M/F) 컬럼에 인덱스를 걸어봐야 실질적인 필터링 효과가 낮다.
  3. 쓰기 오버헤드와 균형 맞추기
    • 인덱스가 많아질수록 INSERT, UPDATE, DELETE 시 비용이 증가한다.
    • 유저가 매 분마다 데이터를 업데이트하는 컬럼이라면, 인덱스 개수를 최소한으로 유지하거나, 캐싱 솔루션과 함께 운영하는 방법도 고려한다.
  4. 실행 계획(Execution Plan) 분석
    • MySQL의 EXPLAIN, PostgreSQL의 EXPLAIN ANALYZE 등을 통해 인덱스 사용 여부를 확인한다.
    • 불필요한 Full Table Scan이나 잘못된 인덱스 사용을 빠르게 찾아낼 수 있다.

“WHERE, JOIN, GROUP BY, ORDER BY” 컬럼 위주로 인덱스를 구성하는 이유

WHERE 절(조건 필터링)

  • 기본 아이디어: WHERE 절의 조건에 해당하는 컬럼을 인덱스로 두면, 데이터베이스가 테이블 전체를 스캔(Full Table Scan)하는 대신 인덱스를 통해 필요한 데이터 위치만 추려서 빠르게 접근할 수 있다.
SELECT * FROM user_table WHERE username = 'Alice'
  • username 컬럼에 인덱스가 있다면, ‘Alice’를 키로 바로 해당 레코드 위치를 찾게 된다.

JOIN 절(테이블 간 연결)

  • JOIN은 두 개 이상의 테이블을 특정 컬럼을 기준으로 이어붙이는 작업이다.
SELECT * FROM user_table u JOIN score_table s ON u.user_id = s.user_id
  • JOIN하는 컬럼(위 예시의 user_id)에 인덱스가 없다면, 한 테이블을 전부 읽은 뒤 다른 테이블도 전부 읽어가며 매칭해야 하므로 비효율적이다.
  • 인덱스가 있다면, 한 테이블의 특정 ‘user_id’ 값을 빠르게 찾아내고, 연결할 때도 인덱스 기반 검색을 수행하므로 JOIN 성능이 크게 향상된다.

GROUP BY & ORDER BY

  • GROUP BYORDER BY정렬 또는 그룹화를 필요로 한다.
SELECT level, COUNT(*) FROM user_table GROUP BY level
SELECT * FROM user_table ORDER BY created_at DESC
  • 해당 컬럼에 인덱스가 미리 정렬된 형태(주로 B+Tree)로 존재한다면, 추가 정렬 연산(Sorting)을 최소화하여 쿼리 수행 시간을 단축할 수 있다.
  • 특히 MySQL에서는 “Using index for group-by” 혹은 “Using index for order-by” 형태로, 인덱스를 직접 활용해 Sorting/Grouping을 수행하는 최적화가 가능하다.

정리

  • “WHERE, JOIN, GROUP BY, ORDER BY에 자주 등장하는 컬럼”에 대해 우선적으로 인덱스를 구성하는 이유는, DB가 제일 많이 하는 연산(필터링, 조인, 그룹화, 정렬)을 가장 크게 단축하기 위함이다.

MySQL의 EXPLAIN, PostgreSQL의 EXPLAIN ANALYZE란?

EXPLAIN(쿼리 실행 계획 확인)

  • EXPLAIN은 데이터베이스가 쿼리를 실제로 어떻게 실행할 예정인지 “실행 계획(Execution Plan)”을 보여주는 명령어이다.
    • MySQL: EXPLAIN SELECT ...
    • PostgreSQL: EXPLAIN SELECT ...
  • 실행 계획에는 “어떤 테이블을 어떤 순서로 읽는지”, “인덱스를 사용하는지”, “Full Table Scan을 하는지”, “JOIN 방식(Nested Loop, Hash Join 등)은 무엇인지” 등이 요약되어 나타난다.
  • 이를 통해, 인덱스를 만들었는데 정말 사용되고 있는지, “무언가 잘못 설계되어 Full Table Scan이 발생하는지” 등을 사전에 파악할 수 있다.

EXPLAIN ANALYZE(실제 실행 시간까지 확인)

  • PostgreSQL(또는 MySQL 8.0 이상에서 EXPLAIN ANALYZE)은 쿼리를 실제로 실행하고, 그 소요 시간(Timing)을 포함해 보다 상세한 정보를 보여준다.
    • 예: EXPLAIN (ANALYZE, BUFFERS) SELECT ... (PostgreSQL)
  • 일반 EXPLAIN이 이론적(계획만)인 반면, EXPLAIN ANALYZE는 실제 실행을 거쳐 정확한 통계치를 보여준다는 차이가 있다.

어떤 경우에 사용할까?

  • 왜 내 쿼리가 느릴까?”라는 문제를 진단할 때, EXPLAIN을 통해 인덱스가 제대로 사용되고 있는지 확인한다.
  • 대규모 테이블에서 JOIN이나 GROUP BY가 걸린 쿼리의 실행 시간이 너무 길다면, “인덱스가 없는지” 혹은 “인덱스 순서가 안 맞는지” 등을 의심할 수 있다.

 

모바일 게임 서버에서 Index가 중요할까?

당연하겠지만, 정답은 "그렇다"이다.

  • 높은 동시접속 처리
    • 모바일 게임 서버는 순간 동시 접속자 수(CCU, Concurrent Users)가 매우 높으며, 로그인·매치메이킹·인벤토리 조회 같은 잦은 SELECT 쿼리가 쏟아진다.
    • 인덱스를 적절히 설계하지 않으면 Full Table Scan으로 인한 응답 지연이 커지고, 이로 인해 유저 경험이 급격히 나빠질 수 있다.
  • 사용자 정보 조회의 빈번성
    • 유저 프로필, 아이템 소유 정보, 랭킹(예: MMR) 등은 짧은 간격으로 자주 조회되므로, 인덱스를 통해 빠른 검색 속도를 보장해야 한다.
  • 트래픽 급증(스파이크) 상황
    • 이벤트나 패치 배포 직후 유저가 몰릴 때, DB 부하가 크게 증가한다.
    • 효율적인 인덱스 설계를 해두면 과부하 상황에서 DB 병목을 상당 부분 해소할 수 있다.

 

정리

  • 인덱스(Index): 책 목차처럼 “어디에 무엇이 있는지”를 빠르게 참조하기 위한 별도 자료 구조
  • B-Tree: 범용적으로 가장 많이 쓰이는 인덱스 구조 (범위 검색에 유리)
  • Hash: 동등 비교에 최적화, 범위 검색에 불리
  • 장점: 검색 속도 크게 향상
  • 단점: 쓰기 성능 저하, 추가 디스크 공간 소모
  • 모바일 게임 서버에서의 중요성: 높은 동시접속, 잦은 읽기 쿼리에 대응하기 위함

 

마치며

내가 지원한 직무는 백엔드였기 때문에 데이터베이스에 대한 중요도는 너무나 당연하게도 다른 과목보다 더 높았을텐데, 이에 대해 제대로 답안을 작성하지 못한 것이 너무나도 아쉽다. 솔직히 말하면 불합격을 해도 할 말이 없을 것 같다는 생각도 들었다.

 

그래도 이번 사례가 나에게 큰 경험이 되었기에, 만약 다음 기회가 주어진다면 그 때는 적어도 DB Index만큼은 제대로 잘 작성할 것이라는 굳은 의지를 표하며 글을 마친다.

 

참고 자료

728x90