---
term: '데이터베이스 인덱스'
seoTitle: '데이터베이스 인덱스란? 인덱싱의 작동 방식'
headline: '데이터베이스 인덱스란?'
slug: database-index
category: database
shortDefinition: '데이터베이스 인덱스는 테이블 전체를 스캔하는 대신 엔진이 일치하는 행을 바로 찾아가게 해 주는 정렬된 조회 구조입니다.'
relatedTerms:
  - database-queries
  - n-plus-one-query-problem
  - database-schema
  - multi-tenant-database-architecture
contrastsWith:
  - database-queries
faq:
  - question: '데이터베이스 인덱스란 쉽게 말해 무엇입니까?'
    answer: '열 값과 그 행을 가리키는 포인터를 담은 별도의 정렬된 구조로, 보통 B-트리입니다. 책의 색인이 용어와 페이지 번호를 담고 있는 것과 정확히 같습니다. 엔진은 행이 나오기를 바라며 테이블 전체를 읽는 대신, 작은 정렬 구조에서 값을 찾은 뒤 일치하는 행으로 곧장 건너뜁니다.'
  - question: '인덱스가 걸린 쿼리는 얼마나 더 빠릅니까?'
    answer: '로그 시간과 선형 시간의 차이입니다. B-트리 조회는 테이블에 행이 1만 개든 1억 개든 몇 개의 페이지만 건드리지만, 전체 스캔은 모든 것을 읽습니다. 데이터가 작을 때는 둘 다 즉각적으로 느껴집니다. 그래서 누락된 인덱스는 개발 환경에서는 숨어 있다가 프로덕션에서 터집니다. 같은 쿼리가 갑자기 수백만 행을 검사하게 되는 곳이 프로덕션이기 때문입니다.'
  - question: '인덱스는 쓰기를 느리게 합니까?'
    answer: '그렇습니다. 그것이 인덱스의 세금입니다. 인덱스가 걸린 열에 대한 모든 삽입, 갱신, 삭제는 그 열을 참조하는 인덱스 각각도 갱신해야 합니다. 테이블에 인덱스가 6개 있다면 쓰기마다 최대 6번의 추가 구조 갱신이 발생하고, 인덱스가 차지하는 스토리지도 더해집니다. 인덱스는 쓰기 속도와 디스크로 값을 치르고 읽기 속도를 사는 것입니다. 의도적으로 만든 인덱스는 제값을 하고, 잊힌 인덱스는 청구서만 보냅니다.'
  - question: '어떤 열에 인덱스를 만들어야 합니까?'
    answer: '쿼리가 실제로 사용하는 열입니다. 필터(WHERE), 조인 키(일부 엔진은 자동으로 인덱싱하지 않는 외래 키와 포인터 필드 포함), 정렬 열(ORDER BY)이 그것입니다. 여기에 선택도 검사를 더합니다. 수백만 행을 구별하는 이메일 열은 인덱스를 둘 가치가 있지만, 테이블을 반으로 나누는 불리언 플래그는 대개 그렇지 않습니다.'
  - question: '복합 인덱스의 열 순서는 어떻게 정해야 합니까?'
    answer: '등호 조건이 먼저, 범위와 정렬은 나중입니다. 그리고 최좌측 접두사 규칙을 기억해야 합니다. (a, b, c)에 대한 인덱스는 a로, a와 b로, 또는 셋 모두로 필터링하는 쿼리에는 쓰이지만 b만으로 필터링하는 쿼리에는 쓰이지 않습니다. 같은 규칙을 문서 데이터베이스 식으로 표현한 것이 ESR(Equality, Sort, Range)입니다. 열 순서는 복합 인덱스가 작동하느냐, 그저 존재만 하느냐의 차이입니다.'
  - question: '누락되거나 사용되지 않는 인덱스는 어떻게 찾습니까?'
    answer: '누락된 인덱스는 실행 계획(EXPLAIN)을 돌려 큰 테이블에 대한 전체 스캔을 찾습니다. 느리고 자주 실행되는 쿼리의 필터 열이 후보입니다. 사용되지 않는 인덱스는 모든 엔진이 인덱스 사용 통계를 추적하므로, 몇 달째 어떤 쿼리도 건드리지 않은 인덱스는 순수한 쓰기 세금이니 삭제해야 합니다. 실행 계획과 통계, 이 둘이 방법론의 전부입니다.'
  - question: '고유 인덱스, 부분 인덱스, 커버링 인덱스란 무엇입니까?'
    answer: '같은 구조의 특수화입니다. 고유 인덱스는 중복 금지를 제약 조건으로 강제하면서 조회도 빠르게 합니다. 부분 인덱스는 조건에 맞는 행만 담으므로, 미출고 주문처럼 자주 조회되는 부분집합에 대해 작고 빠릅니다. 커버링 인덱스는 쿼리에 필요한 모든 열을 담고 있어서, 엔진이 테이블을 방문하지 않고 인덱스만으로 응답할 수 있습니다.'
  - question: '문서 데이터베이스에서도 인덱싱은 같은 방식으로 작동합니까?'
    answer: '개념적으로 동일합니다. 필드 값에 대한 B-트리이며, 같은 트레이드오프와 ESR 경험칙 아래 같은 최좌측 접두사 논리를 따릅니다. 문서 저장소는 배열 필드에 대한 멀티키 인덱스와 지리공간 변형을 더하며, 운영 규칙도 그대로 통합니다. 쿼리하는 것에 인덱스를 두되, 특히 조인과 include가 따라가는 포인터 필드에 두어야 합니다.'
codeLanguages: [javascript, dart, swift, kotlin]
externalAuthorities:
  - name: 'PostgreSQL index types documentation'
    url: 'https://www.postgresql.org/docs/current/indexes-types.html'
  - name: 'Database index (Wikipedia)'
    url: 'https://en.wikipedia.org/wiki/Database_index'
  - name: 'MongoDB indexes documentation'
    url: 'https://www.mongodb.com/resources/basics/databases/database-index'
  - name: 'PostgreSQL — Using EXPLAIN'
    url: 'https://www.postgresql.org/docs/current/using-explain.html'
cta:
  title: '빠른 쿼리, 눈에 보이는 인덱스'
  text: 'Back4app은 인덱스 관리를 스키마가 있는 곳에 둡니다. 쿼리가 실제로 닿는 MongoDB 기반 저장소에서 클래스별로 대시보드에서 인덱스를 만들고 검토할 수 있습니다. 마이그레이션 스크립트도, 필터에 어떤 필드가 필요한지 추측할 일도 없습니다.'
  linkText: '무료로 시작하기'
  linkUrl: 'https://www.back4app.com/signup'
author: 'Back4app Engineering'
publishedDate: '2026-10-28'
translationKey: database-index
---

**데이터베이스 인덱스는 테이블 전체를 스캔하는 대신 엔진이 일치하는 행을 바로 찾아가게 해 주는 정렬된 조회 구조입니다.** 책의 비유는 정확합니다. 900쪽짜리 책에서 "멱등성"을 처음부터 읽어 가며 찾는 사람은 없습니다. 색인을 확인하고 해당 페이지로 건너뜁니다. 데이터베이스는 모든 쿼리에서 같은 선택을 하며, 건너뛸 *수 있는지* 여부가 5밀리초짜리 쿼리와 5초짜리 쿼리를 가르는 가장 흔한 차이입니다.

## 핵심 요약

| 질문 | 답변 |
| --- | --- |
| 무엇인가 | 정렬된 보조 구조: 값 + 포인터, 책의 색인과 같음 |
| 얻는 것 | 선형 스캔 대신 로그 시간 탐색 – 규모가 커질수록 결정적 |
| 치르는 것 | 모든 쓰기가 모든 인덱스를 갱신하고, 스토리지가 늘어남 |
| 무엇을 인덱싱하나 | 필터, 조인/포인터, 정렬 열 – 선택도가 높다면 |
| 진단 도구 | EXPLAIN: 큰 테이블에 대한 전체 스캔이 신호 |

## 인덱스의 생성과 효과

```sql
-- 주력: 핫한 쿼리의 모양을 그대로 따른 복합 인덱스
CREATE INDEX orders_pending ON orders (status, created_at);

-- 같은 아이디어의 특수화:
CREATE UNIQUE INDEX users_email ON users (email);         -- 제약 조건 + 속도
CREATE INDEX unshipped ON orders (created_at)
  WHERE shipped = false;                                  -- 부분 인덱스: 핫한 부분집합만

-- 실행 계획으로 보는 전후 비교:
EXPLAIN SELECT * FROM orders WHERE status = 'pending' AND created_at > now() - interval '1 day';
--  이전:  Seq Scan on orders   (검사한 행: 4,812,309)
--  이후:  Index Scan using orders_pending   (검사한 행: 1,214)
```

애플리케이션 코드는 쿼리 형태를 통해 같은 물리 법칙을 만납니다. 어떤 표면에서 작성하든, 이 쿼리가 *곧* 인덱스 명세 `(status, createdAt)`입니다:

**JavaScript:**

```javascript
// JavaScript / Node.js — Back4app JS SDK
// The query shape tells you the index: (status, createdAt)
const query = new Parse.Query('Order');
query.equalTo('status', 'pending');       // equality first…
query.greaterThan('createdAt', since);    // …then the range
query.descending('createdAt');            // …sorted by the same column
const backlog = await query.find();
// Indexed: milliseconds at any size. Unindexed: a full collection scan.
```

**Flutter:**

```dart
// Flutter / Dart — Back4app Flutter SDK
// The query shape tells you the index: (status, createdAt)
final query = QueryBuilder<ParseObject>(ParseObject('Order'))
  ..whereEqualTo('status', 'pending')      // equality first…
  ..whereGreaterThan('createdAt', since)   // …then the range
  ..orderByDescending('createdAt');        // …sorted by the same column
final response = await query.query();
// Indexed: milliseconds at any size. Unindexed: a full collection scan.
```

**Swift:**

```swift
// iOS / Swift — Back4app Swift SDK
// The query shape tells you the index: (status, createdAt)
let query = Order.query("status" == "pending", "createdAt" > since)
  .order([.descending("createdAt")])
query.find { result in
  if case .success(let backlog) = result { render(backlog) }
}
// Indexed: milliseconds at any size. Unindexed: a full collection scan.
```

**Kotlin:**

```kotlin
// Android / Kotlin — Back4app Android SDK
// The query shape tells you the index: (status, createdAt)
val query = ParseQuery.getQuery<ParseObject>("Order")
query.whereEqualTo("status", "pending")     // equality first…
query.whereGreaterThan("createdAt", since)  // …then the range
query.orderByDescending("createdAt")        // …sorted by the same column
query.findInBackground { backlog, e -> if (e == null) render(backlog) }
// Indexed: milliseconds at any size. Unindexed: a full collection scan.
```

## 건너뛰기의 작동 방식

```mermaid
flowchart TB
  accTitle: B-트리 인덱스 조회
  accDescr: 쿼리는 얕은 정렬 트리를 루트에서 브랜치 노드를 거쳐 일치하는 값과 행 포인터를 담은 리프까지 내려가며, 테이블 전체를 스캔하는 대신 몇 개의 페이지만 건드립니다.
  R["루트 노드<br/>(범위)"] --> B1["브랜치<br/>A–M"]
  R --> B2["브랜치<br/>N–Z"]
  B2 --> L1["리프: 'pending' → 행 88, 1042, 55913"]
  B1 --> L2["리프 …"]
  L1 --> T["정확히 그 행들만 가져오기"]
```

어디서나 기본 구조는 **B-트리**입니다. 얕고, 정렬되어 있으며, 스스로 균형을 잡습니다. 3–4개 레벨이면 수억 개의 항목을 감당하고, 리프들이 서로 연결되어 있어서 하나의 구조가 등호 조건, 범위, `ORDER BY`를 모두 처리합니다. 이 범용성 때문에 "확신이 없으면 B-트리"가 통용되는 조언이고, 나머지는 전문가 역할의 대안입니다.

## B-트리 vs. 해시 vs. 특수 인덱스

### 어떤 인덱스 유형을 사용해야 할까?

| 유형 | 지원 | 선택해야 할 때 |
| --- | --- | --- |
| B-트리(기본값) | `=`, `<`, `>`, 범위, 정렬, 접두사 | 거의 항상 – 범용 |
| 해시 | 등호 조건만, O(1) | 정확히 일치하는 조회, 그 외에는 없음 |
| 복합 | 다중 열, 최좌측 접두사 | 자주 실행되는 쿼리가 여러 열로 필터링할 때 |
| 고유 | B-트리 + 중복 금지 | 제약 조건과 인덱스를 하나로 |
| 부분 | 조건으로 거른 행의 일부 | 자주 조회되는 부분집합: 미출고, 미읽음, 활성 |
| 커버링 | 인덱스만으로 쿼리에 응답 | 열 목록이 안정적인 읽기 위주 쿼리 |
| 전문 검색(역 인덱스) | 단어 → 문서 | 검색창 |
| 지리공간 | 지점, 영역, 거리 | "내 주변" 쿼리 |

복합 행을 떠받치는 규칙은 둘입니다. **최좌측 접두사** 규칙, 즉 `(a, b, c)`에 대한 인덱스는 `a`, `(a, b)`, `(a, b, c)`에는 쓰이지만 `b`만으로는 결코 쓰이지 않는다는 것과, 열 순서에서 **등호 조건이 먼저, 범위/정렬은 마지막**이라는 것입니다([문서 데이터베이스 인덱싱](https://www.mongodb.com/resources/basics/databases/database-index)에서는 ESR이라는 암기법으로 부르며, 같은 B-트리가 같은 일을 합니다).

## 언제 인덱스를 만들고, 언제 만들지 말아야 할까

**인덱스를 만들 곳:** 자주 쓰는 `WHERE` 필터의 열, 조인 키와 포인터 필드(일부 엔진은 외래 키를 자동으로 인덱싱하지 않습니다. 고전적인 조용한 스캔의 원인입니다), 핫 패스의 `ORDER BY` 열입니다. 그리고 언제나 선택도를 염두에 두어야 합니다. 이메일 열(수백만 개의 고유 값)은 제 몫을 하지만, 상태 플래그(값 3개)는 단독으로는 대개 그렇지 않습니다. 다만 뒤에 범위 조건을 둔 *복합 인덱스의 선두*로는 빛을 발합니다. **인덱스를 만들지 말 곳:** 엔진이 탐색보다 스캔으로 더 빨리 처리하는 작은 테이블, 필수적인 몇 개를 넘어선 쓰기 집중 테이블, 기존 인덱스의 왼쪽 접두사가 이미 처리하는 열, 그리고 어떤 쿼리를 위한 것인지 말할 수 없는 모든 것입니다. 쿼리 없는 인덱스는 순수한 쓰기 세금입니다. 감사 루프는 이렇습니다. 느린 쿼리를 [EXPLAIN](https://www.postgresql.org/docs/current/using-explain.html)으로 돌려 스캔을 찾고, 사용 통계로 죽은 인덱스를 확인하고, 그에 따라 추가하고 삭제합니다.

## 일반적인 사용 사례

- **핫 리스트 쿼리.** 모든 대시보드와 피드 뒤에 있는 상태 + 날짜 필터. 복합 인덱스의 본거지입니다.
- **로그인과 조회 필드.** 이메일, 사용자명, 외부 ID. 고유 인덱스가 제약 조건과 속도를 동시에 처리합니다.
- **조인과 포인터 경로.** 쿼리가 따라가는 모든 외래 키와 포인터. N+1 문제의 조용한 공범은 인덱스 없는 포인터입니다.
- **멀티테넌트 필터.** 테넌트가 선두에 오는 복합 인덱스. 모든 인덱스가 `tenant_id`로 시작한다는 [멀티테넌트 아키텍처 규칙](/glossary/ko/multi-tenant-database-architecture/)입니다.
- **검색과 지리.** B-트리가 처리할 수 없는 쿼리를 떠받치는 역 인덱스와 공간 인덱스입니다.

## 그 인덱스를 추가해야 할까? 결정 매트릭스

| 추가해야 할 때… | 건너뛰어야 할 때… |
| --- | --- |
| 자주 실행되는 쿼리가 그 열로 필터링하거나 정렬함 | 프로덕션의 어떤 쿼리도 사용하지 않음 |
| EXPLAIN이 커지는 테이블에서 스캔을 보여 줌 | 테이블이 작고 계속 작음 |
| 열의 선택도가 높음(고유 값이 많음) | 기존 인덱스의 접두사가 이미 처리함 |
| 조인 키 또는 포인터 필드임 | 테이블이 쓰기 위주이고 읽기는 드묾 |
| 어차피 고유 규칙을 강제해야 함 | 추측 중임 – 먼저 측정해야 함 |

규율을 한 문장으로 요약하면 이렇습니다. 인덱스는 쿼리에 대응해 만들고, 사용량에 비추어 검토하고, 미련 없이 삭제합니다.

## 한계와 트레이드오프

- **쓰기가 읽기의 값을 치릅니다.** 인덱스 하나하나가 쓰기마다 유지해야 할 구조입니다. 대량 적재는 인덱스를 삭제했다가 다시 만드는 편이 더 빠른 것으로 유명합니다.
- **스토리지 비용도 무시할 수 없습니다.** 인덱스는 흔히 테이블 자체의 크기에 맞먹으며, 특히 커버링 인덱스는 디스크를 속도와 맞바꿉니다.
- **결정하는 것은 옵티마이저이지 사용자가 아닙니다.** 선택도가 낮은 인덱스는 정당하게 무시될 수 있고, 오래된 통계는 좋은 인덱스를 부당하게 무시할 수 있습니다. 실행 계획이 진실입니다.
- **순서 실수는 복합 인덱스를 무력화합니다.** `(created_at, status)`와 `(status, created_at)`는 다른 도구이며, 최좌측 접두사 규칙은 아무것도 용서하지 않습니다.
- **인덱스는 쿼리를 고쳐 주지 않습니다.** 선두 와일드카드, 열에 적용한 함수, N+1 루프는 인덱싱을 위에서부터 무력화합니다. 쿼리 형태가 먼저입니다.

## Back4app에서의 인덱스

Back4app은 관리형 데이터베이스, 자동 생성되는 REST 및 GraphQL API, 인증, 파일 스토리지, Cloud Code 서버리스 함수를 결합한 오픈소스 Backend as a Service(BaaS) 플랫폼입니다. 인덱스 관리는 대시보드에서 스키마와 함께 이루어집니다. SDK 쿼리가 실제로 닿는 MongoDB 기반 저장소에서, 위에서 설명한 것과 같은 B-트리와 ESR 논리에 따라 클래스별로 단일 필드 또는 복합 인덱스를 만들고, 검토하고, 삭제할 수 있습니다. 코드 탭의 쿼리와 그 쿼리를 처리하는 인덱스를 같은 곳에서 설계하게 되는데, "쿼리하는 것에 인덱스를 둔다"는 규율이 자리 잡아야 할 곳이 바로 거기입니다.
