Загружаем каталог…
Загружаем каталог…
프로그래밍을 처음 배울 때 데이터베이스 인덱스는 보통 책의 색인처럼 원하는 데이터를 빠르게 찾도록 도와주는 기능이라고 배운다. CREATE INDEX idx_shipments_tracking_number ON shipments (tracking_number); 이 인덱스가 있으면 데이터베이스가 전체 배송 데이터를 하나씩 확인하지 않고도 특정 운송장 번호를 찾을 수 있다. 기본 개념을 이해하기에는 충분한 설명이다. 하지만 실제 서비스를 개발하면 단순히 “조회가 느리니 인덱스를 추가한다”는 판단만으로는 부족하다. 어떤 조회를 빠르게 만들어야 하는가? 인덱스의 컬럼 순서는 왜 중요한가? 조회 조건에 포함된 컬럼마다 인덱스를 만들어야 하는가? 데이터가 적을 때도 인덱스가 필요한가? 인덱스가 많아지면 저장과 수정에는 어떤 비용이 생기는가? 정렬과 페이지네이션도 같은 인덱스를 사용할 수 있는가? 실제로 인덱스가 사용되는지는 어떻게 확인하는가? 인덱스는 단순히 검색을 빠르게 만드는 기능이 아니다. 인덱스는 자주 사용되는 조회 경로를 미리 구성하는 대신 저장 공간과 데이터 변경 비용을 지불하는 읽기 성능 설계다. 느린 조회는 테이블이 아니라 사용자의 행동에서 시작된다 크리스가 배송 조회 서비스를 개발한다고 생각해 보자. 사용자는 운송장 번호로 배송 상태를 조회하고, 운영자는 처리되지 않은 배송 목록을 날짜순으로 확인한다. CREATE TABLE shipments ( id UUID PRIMARY KEY, tracking_number TEXT NOT NULL, customer_id UUID NOT NULL, status TEXT NOT NULL, destination_postcode TEXT NOT NULL, created_at TIMESTAMPTZ NOT NULL, delivered_at TIMESTAMPTZ ); 처음에는 데이터가 적기 때문에 어떤 조회도 빠르게 동작한다. SELECT * FROM shipments WHERE tracking_number = 'AU123456789'; 배송 데이터가 수십 건뿐이라면 전체 행을 확인해도 사용자가 차이를 느끼기 어렵다. 그러나 데이터가 계속 쌓이면 같은 조회의 비용도 증가한다. 이때 “ shipments 테이블이 느리다”라고 표현하면 문제를 정확히 설명하기 어렵다. 테이블 자체가 빠르거나 느린 것이 아니라 특정한 조회 방식이 현재 데이터 구조와 맞지 않는 것이다. 배송 서비스에는 서로 다른 조회 경로가 존재할 수 있다. 고객 배송 조회 tracking_number = ? 내 배송 목록 customer_id = ? ORDER BY created_at DESC 운영자 미처리 목록 status IN (...) ORDER BY created_at ASC 지역별 배송 통계 destination_postcode = ? AND created_at BETWEEN ? AND ? 같은 테이블을 조회하더라도 조건, 정렬, 반환 범위가 서로 다르다. 하나의 인덱스가 모든 조회를 동일하게 빠르게 만들지는 않는다. 따라서 인덱스를 설계할 때는 테이블 이름보다 먼저 실제 사용자의 행동과 실행되는 쿼리를 확인해야 한다. 인덱스가 없으면 데이터베이스는 확인할 수 있는 행을 직접 읽는다 운송장 번호에 인덱스가 없다고 생각해 보자. SELECT * FROM shipments WHERE tracking_number = 'AU123456789'; 데이터베이스는 조건에 맞는 행을 찾기 위해 많은 배송 데이터를 확인해야 할 수 있다. 개념적으로는 다음과 비슷한 작업이다. const shipment = shipments.find( (shipment) => shipment.trackingNumber === "AU123456789" ); 배열의 앞부분에서 찾을 수도 있지만 마지막까지 확인해야 할 수도 있다. 데이터가 늘어날수록 확인해야 하는 후보도 많아진다. 운송장 번호를 기준으로 인덱스를 만들면 데이터베이스는 별도의 정렬된 탐색 구조를 유지할 수 있다. CREATE INDEX idx_shipments_tracking_number ON shipments (tracking_number); 이제 운송장 번호를 통해 후보 위치를 찾은 뒤 실제 배송 행을 읽을 수 있다. 그러나 인덱스가 있다고 해서 데이터를 읽는 비용이 사라지는 것은 아니다. 인덱스에서 조건에 맞는 위치 탐색 ↓ 해당 위치가 가리키는 테이블 행 확인 ↓ 필요한 컬럼 반환 인덱스 탐색, 테이블 접근, 결과 전송에는 각각 비용이 있다. 조건에 맞는 데이터가 테이블의 대부분이라면 인덱스를 거치는 것보다 전체 데이터를 읽는 편이 더 나을 수도 있다. 인덱스는 항상 사용되는 지름길이 아니다. 데이터베이스는 예상 비용을 비교한 뒤 인덱스를 사용할지 결정한다. 인덱스는 컬럼이 아니라 조회 패턴을 위해 만든다 크리스가 배송 테이블의 여러 컬럼에 각각 인덱스를 추가한다고 생각해 보자. CREATE INDEX idx_shipments_customer_id ON shipments (customer_id); CREATE INDEX idx_shipments_status ON shipments (status); CREATE INDEX idx_shipments_created_at ON shipments (created_at); 각 컬럼이 자주 사용된다는 이유만으로 개별 인덱스를 만들었지만, 실제 화면의 쿼리는 다음과 같을 수 있다. SELECT id, tracking_number, status, created_at FROM shipments WHERE customer_id = $1 ORDER BY created_at DESC LIMIT 20; 이 쿼리는 특정 고객의 배송만 찾은 뒤 최신 순서로 20개를 반환한다. customer_id 인덱스는 고객의 배송을 찾는 데 도움을 줄 수 있지만, 찾은 결과를 다시 created_at 으로 정렬해야 할 수 있다. created_at 인덱스만 사용하면 전체 사용자의 배송이 섞인 상태에서 특정 고객의 데이터를 찾느라 많은 항목을 확인할 수 있다. 실제 조회 패턴을 함께 표현하는 복합 인덱스를 고려할 수 있다. CREATE INDEX idx_shipments_customer_created_at ON shipments (customer_id, created_at DESC); 이 인덱스는 같은 고객의 배송을 created_at 순서로 탐색할 수 있도록 구성된다. 따라서 조건 검색과 정렬을 하나의 접근 경로로 처리할 가능성이 높아진다. 중요한 것은 “어떤 컬럼이 자주 쓰이는가”만 확인하는 것이 아니다. 어떤 컬럼이 같은 쿼리에서 함께 사용되는가? 어떤 조건이 먼저 검색 범위를 줄이는가? 결과를 어떤 순서로 반환해야 하는가? 한 번에 몇 개의 행을 읽는가? 목록의 다음 페이지는 어떻게 찾는가? 인덱스는 컬럼에 붙이는 성능 옵션이 아니라 반복되는 조회 패턴을 데이터 구조로 표현하는 방법이다. 복합 인덱스의 순서는 서비스가 데이터를 찾는 순서를 반영한다 다음 두 인덱스는 같은 컬럼을 포함하지만 같은 역할을 하지 않는다. CREATE INDEX idx_shipments_customer_created ON shipments (customer_id, created_at DESC); CREATE INDEX idx_shipments_created_customer ON shipments (created_at DESC, customer_id); 첫 번째 인덱스는 고객별로 데이터를 모은 뒤 각 고객 안에서 생성 시각 순서를 유지한다. customer-a → 2026-09-25 customer-a → 2026-09-24 customer-b → 2026-09-25 customer-b → 2026-09-23 따라서 다음 조회와 잘 맞는다. SELECT * FROM shipments WHERE customer_id = $1 ORDER BY created_at DESC LIMIT 20; 반면 두 번째 인덱스는 전체 배송을 생성 시각순으로 정리한 뒤 같은 시각 범위 안에서 고객 ID를 사용한다. 전체 최신 배송을 조회할 때는 유용할 수 있지만 고객 한 명의 전체 이력을 찾는 방식에는 적합하지 않을 수 있다. 복합 인덱스는 일반적으로 앞쪽 컬럼을 기준으로 탐색 범위를 구성한다. 따라서 다음 조회를 위해 만든 인덱스의 순서를 단순히 알파벳순이나 테이블 컬럼 순서로 정해서는 안 된다. WHERE customer_id = $1 AND status = 'in_transit' ORDER BY created_at DESC 이 쿼리에는 다음과 같은 인덱스를 검토할 수 있다. CREATE INDEX idx_shipments_customer_status_created ON shipments ( customer_id, status, created_at DESC ); 이 구조는 고객과 상태로 범위를 좁힌 뒤 생성 시각순으로 결과를 읽는 조회 패턴을 반영한다. 그러나 이것이 모든 상황의 정답은 아니다. 다음 요소에 따라 순서는 달라질 수 있다. 각 조건이 결과 범위를 얼마나 줄이는가? 어떤 조건이 항상 존재하는가? 범위 조건이 포함되는가? 어떤 정렬이 필요한가? 다른 중요한 쿼리에서도 인덱스를 재사용해야 하는가? 복합 인덱스의 순서는 컬럼의 중요도 순위가 아니다. 데이터베이스가 특정 쿼리를 위해 데이터를 탐색하는 경로다. 값의 종류가 적으면 단일 인덱스의 효과도 제한될 수 있다 배송 상태에는 몇 가지 값만 존재할 수 있다. type ShipmentStatus = | "pending" | "picked_up" | "in_transit" | "delivered" | "cancelled"; status 컬럼에 인덱스를 만들 수 있다. CREATE INDEX idx_shipments_status ON shipments (status); 하지만 전체 배송의 절반 이상이 delivered 상태라면 다음 조회는 매우 많은 행을 반환한다. SELECT * FROM shipments WHERE status = 'delivered'; 인덱스를 사용하더라도 수많은 인덱스 항목과 테이블 행을 읽어야 한다. 데이터베이스는 전체 테이블을 순차적으로 읽는 편이 더 저렴하다고 판단할 수 있다. 인덱스가 효과적이려면 조건이 읽어야 할 후보를 의미 있게 줄이는 경우가 많아야 한다. 이를 판단할 때는 값의 선택도를 살펴볼 수 있다. 운송장 번호는 대부분 고유하므로 한 행으로 빠르게 좁혀진다. 고객 ID는 특정 사용자의 배송 범위로 줄일 수 있다. 배송 상태는 값의 종류가 적어 단독으로는 많은 행이 남을 수 있다. 생성 날짜는 범위 조건에 따라 결과 수가 크게 달라진다. 상태 조회가 중요하다면 상태만 보지 않고 실제 화면의 조건을 함께 인덱스에 반영할 수 있다. SELECT * FROM shipments WHERE status = 'pending' ORDER BY created_at ASC LIMIT 100; 이 조회에는 다음 인덱스를 검토할 수 있다. CREATE INDEX idx_shipments_status_created ON shipments (status, created_at ASC); 이 인덱스는 특정 상태의 배송을 오래된 순서로 처리하는 운영 화면에 맞춰져 있다. 인덱스의 효과는 컬럼 타입이나 이름만으로 결정되지 않는다. 실제 값의 분포와 반환되는 데이터 양을 함께 확인해야 한다. 모든 배송보다 미처리 배송만 중요하다면 인덱스도 범위를 줄일 수 있다 운영자는 완료된 배송보다 아직 처리 중인 배송을 훨씬 자주 조회할 수 있다. SELECT * FROM shipments WHERE status IN ( 'pending', 'picked_up', 'in_transit' ) ORDER BY created_at ASC; 서비스가 오래 운영될수록 완료된 배송은 계속 쌓인다. 전체 데이터를 대상으로 큰 인덱스를 유지하는 대신 현재 운영에 필요한 일부 행만 인덱싱하는 방법을 검토할 수 있다. CREATE INDEX idx_shipments_active_created ON shipments (created_at ASC) WHERE status IN ( 'pending', 'picked_up', 'in_transit' ); 이 부분 인덱스는 처리 중인 배송만 포함한다. 완료된 배송은 인덱스 크기를 늘리지 않는다. 운영 화면은 오래된 미처리 배송을 순서대로 찾을 수 있다. 배송이 완료되면 해당 행은 인덱스의 대상에서 제외된다. 그러나 부분 인덱스는 정의된 조건과 실제 쿼리가 맞아야 효과를 얻을 수 있다. 다음처럼 모든 상태를 조회하는 화면에는 같은 인덱스가 충분하지 않다. SELECT * FROM shipments WHERE customer_id = $1 ORDER BY created_at DESC; 부분 인덱스는 특정 쿼리에 강하게 맞춰진 선택이다. 읽기 패턴이 분명하지 않은 상태에서 추가하면 다른 조회에는 사용되지 않는 구조가 될 수 있다. 인덱스 범위를 줄이는 것은 단순한 최적화 기술이 아니다. 서비스에서 어떤 데이터가 현재 자주 읽히는지를 표현하는 설계다. 인덱스를 추가할 때마다 쓰기 작업도 함께 늘어난다 운송장 번호, 고객, 상태, 날짜, 우편번호에 인덱스를 모두 만든다고 생각해 보자. CREATE INDEX idx_shipments_tracking_number ON shipments (tracking_number); CREATE INDEX idx_shipments_customer_id ON shipments (customer_id); CREATE INDEX idx_shipments_status ON shipments (status); CREATE INDEX idx_shipments_created_at ON shipments (created_at); CREATE INDEX idx_shipments_postcode ON shipments (destination_postcode); 새로운 배송을 저장할 때 데이터베이스는 테이블 행만 추가하지 않는다. shipments 테이블에 행 저장 + 운송장 번호 인덱스 갱신 + 고객 인덱스 갱신 + 상태 인덱스 갱신 + 생성 시각 인덱스 갱신 + 우편번호 인덱스 갱신 배송 상태가 변경될 때는 상태가 포함된 인덱스도 수정해야 한다. UPDATE shipments SET status = 'delivered', delivered_at = NOW() WHERE id = $1; 인덱스가 많아질수록 다음 비용이 증가할 수 있다. 데이터 삽입과 수정에 필요한 작업 인덱스를 저장하는 디스크 공간 메모리에 유지해야 하는 데이터 백업과 복구에 필요한 시간 스키마 변경과 배포 시 관리해야 하는 구조 사용되지 않는 인덱스를 점검하는 운영 비용 따라서 읽기 쿼리 하나가 빨라졌다는 사실만으로 인덱스가 무료인 것은 아니다. 선택 얻는 것 지불하는 것 인덱스를 추가함 특정 조회 경로의 성능 저장 공간과 쓰기 비용 복합 인덱스를 추가함 조건과 정렬을 함께 처리할 가능성 더 큰 인덱스와 제한된 재사용 범위 부분 인덱스를 추가함 특정 데이터 집합의 작은 인덱스 조건에 맞는 쿼리에서만 사용 가능 인덱스를 추가하지 않음 단순한 쓰기와 적은 저장 공간 데이터 증가에 따른 읽기 비용 인덱스 설계는 읽기 속도만 높이는 작업이 아니라 읽기와 쓰기 사이의 비용을 배분하는 작업이다. 고유 인덱스는 성능보다 서비스의 약속을 지킬 수 있다 배송 서비스에서 운송장 번호는 중복되면 안 된다고 가정해 보자. 애플리케이션에서 먼저 확인할 수 있다. const existingShipment = await shipmentRepository.findByTrackingNumber( trackingNumber ); if (!existingShipment) { await shipmentRepository.create({ trackingNumber, customerId, }); } 그러나 동시에 두 요청이 실행되면 둘 다 운송장 번호가 존재하지 않는다고 판단할 수 있다. 고유 제약 조건을 데이터베이스에 표현해야 한다. ALTER TABLE shipments ADD CONSTRAINT shipments_tracking_number_unique UNIQUE (tracking_number); 데이터베이스는 이 제약을 보장하기 위해 고유한 인덱스 구조를 사용할 수 있다. 이 구조의 첫 번째 목적은 단순히 조회를 빠르게 만드는 것이 아니다. 하나의 운송장 번호는 하나의 배송만 식별한다는 서비스의 약속을 데이터베이스가 보장한다. 외부 배송사마다 같은 형식의 운송장 번호를 사용할 수 있다면 고유성 범위가 달라질 수 있다. ALTER TABLE shipments ADD CONSTRAINT shipments_carrier_tracking_unique UNIQUE (carrier_code, tracking_number); 이제 운송장 번호는 배송사 안에서만 고유하면 된다. 인덱스와 제약 조건은 비슷한 내부 구조를 사용할 수 있지만 설계 의도는 구분해야 한다. 일반 인덱스는 주로 조회 경로를 최적화한다. 고유 제약 조건은 데이터가 지켜야 하는 규칙을 보장한다. 성능 요구가 바뀌면 일반 인덱스는 제거할 수 있다. 그러나 고유 제약을 제거하면 서비스의 데이터 규칙까지 바뀐다. 정렬과 페이지네이션도 인덱스 설계의 일부다 크리스가 고객의 배송 이력을 페이지 단위로 제공한다고 생각해 보자. SELECT * FROM shipments WHERE customer_id = $1 ORDER BY created_at DESC LIMIT 20 OFFSET 10000; customer_id 와 created_at 을 포함한 인덱스는 필터링과 정렬에 도움을 줄 수 있다. 하지만 큰 OFFSET 은 앞의 결과를 건너뛰기 위해 많은 항목을 확인하게 만들 수 있다. 마지막으로 확인한 위치를 기준으로 다음 페이지를 가져올 수 있다. SELECT * FROM shipments WHERE customer_id = $1 AND (created_at, id) < ($2, $3) ORDER BY created_at DESC, id DESC LIMIT 20; 이 조회에 맞춰 인덱스를 구성할 수 있다. CREATE INDEX idx_shipments_customer_cursor ON shipments ( customer_id, created_at DESC, id DESC ); id 는 같은 created_at 값을 가진 여러 배송의 순서를 안정적으로 구분한다. 프론트엔드는 마지막 항목의 값을 다음 요청에 전달할 수 있다. type ShipmentCursor = { createdAt: string; shipmentId: string; }; 이 커서는 새로운 Source of Truth가 아니다. 배송 데이터의 현재 정렬 위치를 표현하는 조회용 값이다. 페이지네이션 방법과 인덱스를 따로 설계하면 둘 중 하나가 다른 하나의 장점을 사용하지 못할 수 있다. 필터, 정렬, 페이지 경계를 하나의 조회 패턴으로 함께 봐야 한다. 인덱스가 있다는 사실보다 실행 계획이 중요하다 인덱스를 생성했다고 해서 데이터베이스가 반드시 사용하는 것은 아니다. CREATE INDEX idx_shipments_status ON shipments (status); 실제 실행 방식을 확인하려면 실행 계획을 살펴봐야 한다. EXPLAIN ANALYZE SELECT * FROM shipments WHERE status = 'delivered'; 실행 계획을 통해 다음 내용을 확인할 수 있다. 전체 테이블을 읽었는가? 인덱스를 사용했는가? 예상한 행 수와 실제 행 수가 비슷한가? 정렬이 별도로 발생했는가? 어느 단계에서 많은 시간과 데이터를 사용했는가? 인덱스가 사용되지 않는다고 해서 곧바로 문제가 있는 것은 아니다. 데이터가 적거나 조건에 맞는 행이 많다면 전체 테이블을 읽는 편이 더 나을 수 있다. 반대로 인덱스를 사용했다고 해서 쿼리가 충분히 빠르다는 뜻도 아니다. 너무 많은 행을 읽거나, 인덱스 사용 후 추가 정렬과 테이블 접근이 많이 발생할 수 있다. 성능 판단은 다음 순서로 진행하는 편이 낫다. 실제 느린 사용자 흐름 확인 ↓ 실행되는 쿼리 확인 ↓ 실행 계획과 읽은 행 수 확인 ↓ 후보 인덱스 설계 ↓ 변경 전후 측정 ↓ 쓰기 비용과 다른 쿼리 영향 확인 인덱스 이름이나 개수보다 중요한 것은 실제 작업량이 줄었는지 확인하는 일이다. ORM이 관계를 표현해도 인덱스까지 자동으로 설계해 주지는 않는다 애플리케이션이
То, что RADAR обнаружил и классифицировал для этой возможности. Это опубликованный источником текст, а не подтверждение, что предложение ещё действует.
인덱스는 검색을 빠르게 만드는 기능이 아니다. 프로그래밍을 처음 배울 때 데이터베이스 인덱스는 보통 책의 색인처럼 원하는 데이터를 빠르게 찾도록 도와주는 기능이라고 배운다. CREATE INDEX idx_shipments_tracking_number ON shipments (tracking_number); 이 인덱스가 있으면 데이터베이스가 전체 배송 데이터를 하나씩 확인하지 않고도 특정 운송장 번호를 찾을 수 있다. 기본 개념을 이해하기에는 충분한 설명이다. 하지만 실제 서비스를 개발하면 단순히 “조회가 느리니 인덱스를 추가한다”는 판단만으로는 부족하다. 어떤 조회를 빠르게 만들어야 하는가? 인덱스의 컬럼 순서는 왜 중요한가? 조회 조건에 포함된 컬럼마다 인덱스를 만들어야 하는가? 데이터가 적을 때도…
Открыть источник