# Index 예시 ## 좋은 예시 ### 예시 1. 자주 조회되는 FK에 단일 컬럼 B-tree 인덱스를 둔다 ```sql CREATE TABLE auth.users ( id bigint GENERATED ALWAYS AS IDENTITY, CONSTRAINT pk_users PRIMARY KEY (id) ); CREATE TABLE auth.sessions ( id bigint GENERATED ALWAYS AS IDENTITY, user_id bigint NOT NULL, expires_at timestamp with time zone NOT NULL, CONSTRAINT pk_sessions PRIMARY KEY (id), CONSTRAINT fk_sessions__users FOREIGN KEY (user_id) REFERENCES auth.users (id) ON DELETE CASCADE ); CREATE INDEX ix_sessions__user_id ON auth.sessions (user_id); ``` **좋은 이유:** - FK는 참조 무결성을 보장하지만, 참조하는 쪽 인덱스는 자동 생성되지 않는다 - 세션을 사용자 기준으로 자주 조회하거나, 사용자 삭제 시 자식 세션을 찾는 경로에 도움이 된다 - 단순하고 신뢰도 높은 기본 패턴이다 이 예시는 PostgreSQL의 FK 동작과 B-tree 기본 사용 패턴에 맞는 안전한 best practice다. ### 예시 2. 복합 검색 패턴에는 왼쪽 컬럼 순서를 고려한 멀티 컬럼 인덱스를 둔다 ```sql CREATE TABLE ordering.orders ( id bigint GENERATED ALWAYS AS IDENTITY, tenant_id bigint NOT NULL, status text NOT NULL, created_at timestamp with time zone NOT NULL, CONSTRAINT pk_orders PRIMARY KEY (id) ); CREATE INDEX ix_orders__tenant_status_created_at ON ordering.orders (tenant_id, status, created_at DESC); ``` **좋은 이유:** - tenant_id = ? AND status = ? AND created_at < ? ORDER BY created_at DESC 같은 패턴에 잘 맞는다 - equality 필터를 왼쪽에 두고, range/정렬 컬럼을 뒤에 둔 전형적인 B-tree 설계다 - 멀티 컬럼 인덱스가 실제 쿼리 패턴에 직접 대응한다 PostgreSQL은 B-tree 멀티 컬럼 인덱스가 leading columns 제약에 가장 효율적이라고 설명한다. ### 예시 3. soft delete 활성 행만 자주 조회되면 partial index를 사용한다 ```sql CREATE TABLE auth.users ( id bigint GENERATED ALWAYS AS IDENTITY, tenant_id bigint NOT NULL, email text NOT NULL, deleted_at timestamp with time zone, CONSTRAINT pk_users PRIMARY KEY (id) ); CREATE INDEX ix_users__tenant_id_email__active ON auth.users (tenant_id, email) WHERE deleted_at IS NULL; ``` **좋은 이유:** - 전체 사용자보다 “삭제되지 않은 사용자”만 자주 조회되는 경우에 맞는 설계다 - hot subset만 인덱싱하므로 전체 인덱스보다 더 작고 유지 비용도 낮을 수 있다 - predicate가 단순하고 쿼리와 맞추기 쉽다 PostgreSQL 공식 문서도 partial index를 이런 subset 최적화에 쓰도록 설명한다. ### 예시 4. 대소문자 무시 검색은 expression index로 맞춘다 ```sql CREATE TABLE auth.users ( id bigint GENERATED ALWAYS AS IDENTITY, email text NOT NULL, CONSTRAINT pk_users PRIMARY KEY (id) ); CREATE UNIQUE INDEX uq_users__lower_email ON auth.users (lower(email)); ``` **좋은 이유:** - 검색과 고유성 규칙이 모두 lower(email) 의미에 맞춰져 있다 - 애플리케이션에서 임시 변환만 하는 것보다 DB 규칙이 더 명확하다 - 표현식 인덱스의 대표적인 안전 사용 사례다 PostgreSQL은 lower(col) 같은 expression index를 공식 지원하고, unique expression index로 단순 unique constraint로 표현하기 어려운 규칙도 강제할 수 있다고 설명한다. ### 예시 5. 목록 조회 최적화가 확실하면 INCLUDE를 보수적으로 사용한다 ```sql CREATE TABLE ordering.orders ( id bigint GENERATED ALWAYS AS IDENTITY, tenant_id bigint NOT NULL, status text NOT NULL, created_at timestamp with time zone NOT NULL, total_amount numeric(19,4) NOT NULL, CONSTRAINT pk_orders PRIMARY KEY (id) ); CREATE INDEX ix_orders__tenant_status_created_at ON ordering.orders (tenant_id, status, created_at DESC) INCLUDE (total_amount); ``` **좋은 이유:** - 자주 실행되는 목록 조회가 tenant_id, status, created_at 기준으로 필터/정렬되고, 응답에는 total_amount가 필요할 때 유효하다 - total_amount는 non-key payload라 uniqueness/탐색 키 의미를 어지럽히지 않는다 - 작은 payload 컬럼만 추가한 보수적 covering index다 PostgreSQL은 INCLUDE가 index-only scan을 돕지만, 인덱스 크기를 키우므로 보수적으로 사용해야 한다고 설명한다. ### 예시 6. jsonb 전체 containment 검색은 GIN을 검토한다 ```sql CREATE TABLE integration.webhook_events ( id bigint GENERATED ALWAYS AS IDENTITY, payload jsonb NOT NULL, received_at timestamp with time zone NOT NULL, CONSTRAINT pk_webhook_events PRIMARY KEY (id) ); CREATE INDEX ix_webhook_events__payload_gin ON integration.webhook_events USING GIN (payload); ``` **좋은 이유:** - payload @> ..., key existence, jsonpath 검색 같은 jsonb 검색 패턴에 맞는다 - B-tree로 해결할 수 없는 composite value 내부 검색을 GIN으로 처리한다 - jsonb를 유지해야 하는 상황에서 가장 전형적인 공식 패턴이다 PostgreSQL은 jsonb 검색에 GIN을 사용할 수 있고, key/key-value search 및 containment에 적합하다고 설명한다. ### 예시 7. 대형 append-only 로그는 BRIN을 검토한다 ```sql CREATE TABLE audit.audit_logs ( id bigint GENERATED ALWAYS AS IDENTITY, occurred_at timestamp with time zone NOT NULL, actor_id bigint, action text NOT NULL, CONSTRAINT pk_audit_logs PRIMARY KEY (id) ); CREATE INDEX ix_audit_logs__occurred_at_brin ON audit.audit_logs USING BRIN (occurred_at); ``` **좋은 이유:** - 아주 큰 로그 테이블에서 occurred_at이 물리 저장 순서와 자연스럽게 상관될 가능성이 높다 - BRIN은 작은 인덱스로 큰 범위를 건너뛸 수 있다 - append-only 성격이 강한 로그/이력성 테이블과 잘 맞는다 PostgreSQL은 BRIN이 매우 큰 테이블과 물리 순서 상관성이 있는 컬럼에 적합하다고 설명한다. ## 나쁜 예시 ### 예시 1. PK가 이미 만든 인덱스를 다시 만든다 ```sql CREATE TABLE auth.users ( id bigint GENERATED ALWAYS AS IDENTITY, email text NOT NULL, CONSTRAINT pk_users PRIMARY KEY (id) ); CREATE INDEX ix_users__id ON auth.users (id); ``` **나쁜 이유:** - PK가 이미 unique B-tree 인덱스를 자동 생성한다 - 같은 의미의 중복 인덱스라 쓰기 비용과 저장 비용만 늘린다 - 운영상 관리 포인트만 증가한다 PostgreSQL 공식 문서는 PK/UNIQUE 제약이 자동으로 인덱스를 만든다고 설명한다. ### 예시 2. 실제 쿼리와 무관하게 긴 멀티 컬럼 인덱스를 만든다 ```sql CREATE INDEX ix_orders__tenant_status_type_created_at_updated_at ON ordering.orders (tenant_id, status, type, created_at, updated_at); ``` **나쁜 이유:** - 어떤 쿼리를 위한 인덱스인지 설명하기 어렵다 - 멀티 컬럼이 길어질수록 유지 비용이 커지고 활용 범위도 애매해진다 - 대부분의 경우 단일/짧은 복합 인덱스 조합이 더 낫다 PostgreSQL도 멀티 컬럼 인덱스는 신중히 사용해야 하고, 3개를 넘는 경우는 드물게만 유효하다고 설명한다. ### 예시 3. 단일 컬럼 DESC 인덱스를 습관적으로 만든다 ```sql CREATE INDEX ix_orders__created_at_desc ON ordering.orders (created_at DESC); ``` **나쁜 이유:** - 단일 컬럼 B-tree는 backward scan이 가능하므로 대개 별도 DESC 인덱스 이점이 없다 - mixed ordering이 아닌데도 특수 정렬을 도입해 관리 복잡도만 올린다 - 실제 쿼리 근거가 부족하다 PostgreSQL은 ordered scan을 forward/backward 모두 지원하고, 단일 컬럼에서는 별도 DESC 인덱스가 대체로 유용하지 않다고 설명한다. ### 예시 4. partial index predicate를 쿼리와 다르게 만든다 ```sql CREATE INDEX ix_users__active_recent ON auth.users (tenant_id, email) WHERE deleted_at IS NULL AND last_login_at > now() - interval '30 days'; ``` **나쁜 이유:** - now() 같은 시간 의존 조건은 안정적인 partial index predicate로 부적절하다 - 쿼리와 predicate가 정확히 맞지 않으면 planner가 인덱스를 잘 쓰지 못한다 - 데이터 분포와 시간 경계가 계속 바뀌므로 유지 신뢰도가 낮다 PostgreSQL은 partial index predicate가 planner가 인식 가능한 형태여야 한다고 설명한다. ### 예시 5. 큰 payload를 INCLUDE에 넣는다 ```sql CREATE INDEX ix_posts__author_created_at ON board.posts (author_id, created_at DESC) INCLUDE (content, metadata_json); ``` **나쁜 이유:** - 큰 text/json payload는 인덱스를 크게 부풀린다 - 인덱스 tuple 크기 제한에 걸릴 수 있고, 검색 자체도 느려질 수 있다 - INCLUDE는 작은 응답용 컬럼에만 보수적으로 써야 한다 PostgreSQL은 wide non-key column을 INCLUDE에 넣는 것을 보수적으로 하라고 명시한다. ### 예시 6. 운영 중 대형 테이블에 일반 CREATE INDEX를 바로 실행한다 ```sql CREATE INDEX ix_events__occurred_at ON audit.audit_logs (occurred_at); ``` **나쁜 이유:** - 일반 CREATE INDEX는 빌드 동안 writes를 막는다 - 운영 중 대형 테이블에서는 서비스 영향이 매우 클 수 있다 - 이런 경우는 CREATE INDEX CONCURRENTLY 여부를 먼저 검토해야 한다 PostgreSQL 공식 문서도 일반 index build는 writes를 block하고, 운영 환경에서는 종종 unacceptable하다고 설명한다. ### 예시 7. jsonb 키 검색에 B-tree를 건다 ```sql CREATE INDEX ix_webhook_events__payload ON integration.webhook_events (payload); ``` **나쁜 이유:** - jsonb @>, ?, @?, @@ 같은 검색은 B-tree 기본 패턴과 맞지 않는다 - composite value 내부 검색은 GIN 같은 전용 접근 방식이 더 적합하다 - 타입/연산자 특성을 무시한 설계다 PostgreSQL은 jsonb key/key-value 검색에 GIN을 공식적으로 권장 가능한 접근 방식으로 설명한다.