특화된 인덱싱 전략으로 쿼리 성능을 강화하기
James Reed
Infrastructure Engineer · Leapcell

소개
더 빠른 데이터 검색과 더 반응성이 좋은 애플리케이션에 대한 끊임없는 추구 속에서 데이터베이스 최적화는 개발의 초석이 됩니다. 데이터 세트가 확장되고 쿼리가 복잡해짐에 따라 정보에 액세스하는 효율성은 사용자 경험과 시스템 확장성에 직접적인 영향을 미칩니다. 전통적인 인덱싱은 견고한 기반을 제공하는 경우가 많지만, 많은 실제 시나리오에서는 최고 성능을 발휘하기에 부족합니다. 이 글에서는 종종 과소평가되는 두 가지 강력한 인덱싱 기법, 즉 포괄 인덱스(covering indexes)와 부분 인덱스(partial indexes)를 깊이 있게 살펴봅니다. 메커니즘과 전략적 응용을 이해함으로써 개발자와 데이터베이스 관리자는 I/O 작업을 크게 줄이고 쿼리 실행을 가속화하며 궁극적으로 뛰어난 데이터 경험을 제공할 수 있습니다. 이러한 특화된 인덱스 유형이 기존 B-트리 구조를 넘어 데이터베이스가 데이터와 상호 작용하는 방식을 근본적으로 변화시키는 대상 최적화를 제공하는 방법을 살펴보겠습니다.
최적화를 위한 특화된 인덱싱 이해하기
포괄 인덱스와 부분 인덱스의 구체적인 내용을 다루기 전에, 해당 유용성의 기반이 되는 몇 가지 핵심 개념을 파악하는 것이 중요합니다.
인덱스: 데이터베이스 인덱스의 핵심은 데이터베이스 검색 엔진이 데이터 검색 속도를 높이는 데 사용할 수 있는 특수 조회 테이블입니다. 책 전체를 읽지 않고도 특정 주제가 논의되는 페이지를 직접 가리키는 책 뒤쪽의 인덱스와 같다고 생각하면 됩니다.
기본 키 인덱스: 테이블의 각 행에 대한 고유 식별자로, 빠른 액세스를 보장하고 데이터 무결성을 강제하기 위해 자동으로 인덱싱됩니다.
클러스터형 인덱스: 테이블에서 데이터 행의 물리적 저장 순서를 결정합니다. 테이블은 클러스터형 인덱스를 하나만 가질 수 있습니다. 종종 기본 키가 클러스터형 인덱스로 사용됩니다.
비클러스터형 (보조) 인덱스: 데이터 행의 물리적 순서를 지정하지 않는 인덱스입니다. 실제 데이터 행에 대한 포인터를 포함합니다. 테이블은 여러 개의 비클러스터형 인덱스를 가질 수 있습니다.
인덱스 스캔: 데이터베이스는 전체 인덱스를 읽어 관련 행을 찾습니다. 이는 일반적으로 전체 테이블 스캔보다 빠르지만, 최적화되지 않은 경우 필요한 것보다 더 많은 데이터를 읽는 결과를 초래합니다.
인덱스 시크: 데이터베이스는 책의 인덱스를 사용하여 페이지를 찾는 것과 유사하게 인덱스를 사용하여 데이터의 특정 위치로 직접 이동합니다. 이것이 가장 빠른 데이터 검색 방법입니다.
포괄 인덱스: 데이터로 가는 지름길
A covering index(also known as an index-only scan)는 쿼리에 필요한 모든 열을 포함하는 특수 유형의 비클러스터형 인덱스입니다. 즉, 데이터베이스는 실제 테이블의 데이터 행에 액세스하지 않고도 인덱스 자체에서 필요한 모든 데이터를 검색할 수 있습니다. 이는 종종 쿼리에서 가장 느린 부분인 디스크 I/O를 줄여주므로 중요한 최적화입니다. 데이터베이스는 인덱스를 사용하여 행의 물리적 위치를 찾고 나머지 열을 기본 테이블에서 가져오는 "북마크 조회" 또는 "키 조회" 작업을 피합니다.
작동 방식:
포괄 인덱스를 생성할 때 빠른 조회를 위한 열(키 열)뿐만 아니라 일반 쿼리에서 필요할 수 있는 추가 열( "포함된" 열 또는 "페이로드" 열)도 지정합니다.
CustomerID
, Name
, Email
, City
, LastOrderDate
열이 있는 Customers
테이블을 고려해 보겠습니다.
CREATE TABLE Customers ( CustomerID INT PRIMARY KEY, Name VARCHAR(255), Email VARCHAR(255), City VARCHAR(100), LastOrderDate DATE ); INSERT INTO Customers (CustomerID, Name, Email, City, LastOrderDate) VALUES (1, 'Alice Smith', 'alice@example.com', 'New York', '2023-10-15'), (2, 'Bob Johnson', 'bob@example.com', 'Los Angeles', '2023-11-20'), (3, 'Charlie Brown', 'charlie@example.com', 'New York', '2024-01-05');
다음과 같은 쿼리를 자주 실행하는 경우:
SELECT Name, Email FROM Customers WHERE City = 'New York';
City
에 대한 표준 인덱스는 'New York'의 고객을 조회한 다음 Customers
테이블로 돌아가 Name
과 Email
을 가져옵니다.
이제 포괄 인덱스를 만들어 보겠습니다.
-- PostgreSQL 예: CREATE INDEX idx_city_name_email ON Customers (City) INCLUDE (Name, Email); -- SQL Server 예: CREATE NONCLUSTERED INDEX idx_city_name_email ON Customers (City) INCLUDE (Name, Email); -- MySQL/MariaDB 예 (모든 열을 인덱스에 포함하여 포괄 인덱스가 암시적으로 처리됨): CREATE INDEX idx_city_name_email ON Customers (City, Name, Email);
이 포괄 인덱스를 사용하면 SELECT Name, Email FROM Customers WHERE City = 'New York';
쿼리가 실행될 때 데이터베이스는 'New York'에 대한 인덱스 항목에서 Name
과 Email
을 직접 검색하여 기본 Customers
테이블을 방문하지 않아도 됩니다. 이것은 인덱스 전용 스캔(index-only scan)으로, 특히 대규모 테이블에서 훨씬 더 빠른 실행으로 이어집니다.
포괄 인덱스의 응용 시나리오:
- 보고 쿼리: 보고 또는 표시를 위해 특정 열 집합을 가져오는 쿼리입니다.
- 자주 액세스되는 하위 집합: 특정 필터 조건에 대해 몇 개의 특정 열이 거의 항상 함께 검색되는 경우입니다.
- 행 수가 많은 대규모 테이블: 모든 쿼리에 대해 전체 행에 액세스하는 것이 비용이 많이 드는 경우입니다.
절충점: 포괄 인덱스는 읽기 성능을 향상시키지만 디스크 공간 사용량이 증가하고 인덱스에도 업데이트가 필요하므로 쓰기 작업(INSERT, UPDATE, DELETE)이 약간 느려집니다. 이러한 요인 간의 균형을 맞추는 것이 중요합니다.
부분 인덱스: 가장 중요한 것에 집중하기
A partial index(also known as a filtered index or sparse index)는 테이블의 행 하위 집합만 인덱싱하는 인덱스입니다. 모든 행을 인덱싱하는 대신 인덱스 생성 중에 WHERE
절을 적용하여 해당 조건을 만족하는 행만 인덱스에 포함되도록 합니다. 이는 특정 조건으로 자주 필터링하는 쿼리를 최적화하는 데 매우 유용하며, 특히 해당 조건이 전체 데이터의 작은 부분을 차지하는 경우 더욱 그렇습니다.
작동 방식:
테이블의 일부만 인덱싱함으로써 부분 인덱스는 여러 가지 이점을 제공합니다.
- 인덱스 크기 감소: 더 작은 인덱스 크기는 디스크 공간을 덜 차지하고 인덱스 스캔을 더 빠르게 만듭니다.
- 더 빠른 인덱스 업데이트: 유지 관리할 항목이 적어 기본 테이블의 쓰기 작업 속도가 향상됩니다.
- 캐시 활용도 향상: 더 작은 인덱스가 버퍼 캐시에 더 잘 맞으므로 디스크 읽기 횟수가 줄어듭니다.
OrderID
, CustomerID
, OrderDate
, Status
, Amount
열이 있는 Orders
테이블을 상상해 보겠습니다.
CREATE TABLE Orders ( OrderID INT PRIMARY KEY, CustomerID INT, OrderDate DATE, Status VARCHAR(50), -- 예: 'pending', 'shipped', 'cancelled', 'returned' Amount DECIMAL(10, 2) ); INSERT INTO Orders (OrderID, CustomerID, OrderDate, Status, Amount) VALUES (101, 1, '2023-01-01', 'shipped', 150.00), (102, 2, '2023-01-02', 'pending', 200.00), (103, 1, '2023-01-03', 'shipped', 50.00), (104, 3, '2023-01-04', 'cancelled', 300.00), (105, 2, '2023-01-05', 'pending', 120.00);
'pending' 주문을 찾는 일반적인 쿼리가 다음과 같은 경우:
SELECT OrderID, OrderDate, Amount FROM Orders WHERE Status = 'pending';
Status
에 대한 전체 인덱스는 'shipped', 'cancelled', 'returned' 주문도 인덱싱합니다. 'pending' 주문이 전체의 작은 비율을 차지하는 경우 비효율적입니다.
이제 'pending' 주문에 대한 부분 인덱스를 만들어 보겠습니다.
-- PostgreSQL 예: CREATE INDEX idx_pending_orders ON Orders (OrderDate, Amount) WHERE Status = 'pending'; -- SQL Server (필터링된 인덱스) 예: CREATE NONCLUSTERED INDEX idx_pending_orders ON Orders (OrderDate, Amount) WHERE Status = 'pending';
이 부분 인덱스를 사용하면 Status = 'pending'
을 구체적으로 대상으로 하는 쿼리는 이 작고 더 집중된 인덱스를 사용할 수 있습니다. 다른 상태 유형의 경우 데이터베이스는 다른 인덱스나 전체 테이블 스캔을 기본값으로 사용합니다. OrderDate
및 Amount
열은 부분 인덱스에 포함되어 일반 쿼리에 대한 포괄적인 부분 인덱스로 만들어지며, 필요한 데이터를 인덱스에서 직접 검색합니다.
부분 인덱스의 응용 시나리오:
- 드물게 참인 조건:
WHERE
절 조건이 행의 작은 비율에 대해 참인 경우(예:is_active = TRUE
,status = 'pending'
,error_code IS NOT NULL
). - 아카이브된 데이터: 오래된 데이터는 거의 액세스되지 않지만 유지해야 하는 경우 활성 데이터에 대한 부분 인덱스를 생성할 수 있습니다.
- 유지 관리 오버헤드 줄이기: 매우 높은 쓰기 볼륨을 가진 테이블에서 쿼리 중 작은 하위 집합만 속도 향상을 위해 인덱싱해야 하는 경우입니다.
절충점: 부분 인덱스는 WHERE
절을 충족하는 쿼리에만 유용합니다. 일치하지 않는 쿼리는 부분 인덱스를 사용할 수 없습니다. 가장 일반적이고 중요한 필터 조건을 식별하는 것이 필수적입니다.
결론
포괄 인덱스와 부분 인덱스는 기본적인 인덱싱을 넘어 특정 워크로드에 대해 쿼리 성능을 크게 향상시키는 정교한 데이터베이스 최적화 전략을 나타냅니다. 포괄 인덱스는 필요한 모든 데이터를 인덱스에서 직접 제공하여 디스크 I/O를 최소화하고, 부분 인덱스는 관련 하위 집합만 인덱싱하여 인덱스 크기와 유지 관리를 줄입니다. 이러한 기법을 신중하게 적용함으로써 개발자는 느리고 리소스 집약적인 쿼리를 번개처럼 빠른 작업으로 변환하여 데이터 규모가 커져도 애플리케이션이 계속 응답하고 효율적으로 작동하도록 보장할 수 있습니다. 이러한 특화된 인덱스를 이해하고 올바르게 구현하는 것은 모든 데이터베이스 전문가의 강력한 도구이며, 뛰어난 사용자 경험을 지원하는 대상 성능 향상을 가능하게 합니다.