메뉴 닫기

MSSQL 인덱스 컬럼 선택 기준과 최적화 전략

📊 MSSQL 인덱스 컬럼 선택 기준과 최적화 전략

⚡ 조회·필터링·정렬 성능을 극대화하는 인덱스 구성 노하우

데이터베이스를 다루다 보면, 성능 문제의 상당수가 잘못된 인덱스 설계에서 비롯됩니다.
특히 MSSQL에서는 어떤 컬럼에 인덱스를 적용하느냐에 따라 조회 속도, 필터링 처리, 정렬 성능이 크게 달라집니다.
하지만 무작정 모든 컬럼에 인덱스를 추가하는 것은 오히려 성능 저하를 유발할 수 있죠.
따라서, 실제 쿼리 패턴과 데이터 특성을 고려해 신중하게 인덱스 컬럼을 선정하는 것이 중요합니다.
오늘은 자주 조회되는 컬럼, WHERE 조건에서 필터링되는 컬럼, 그리고 ORDER BY 절에 사용되는 컬럼을 기준으로 MSSQL 인덱스를 최적화하는 방법을 살펴보겠습니다.

이 글에서는 인덱스 설계 시 꼭 알아야 할 컬럼 선택 기준부터, 실무에서 자주 활용되는 구성 전략, 그리고 불필요한 인덱스를 피하는 방법까지 구체적으로 안내합니다.
DBA나 개발자 모두가 참고할 수 있는 실전 가이드로, 단순한 이론이 아닌 실제 프로젝트에서 성능 향상을 이끌어낼 수 있는 팁을 제공합니다.
특히 트래픽이 많은 환경이나 대용량 데이터를 다루는 경우, 올바른 인덱스 설계가 얼마나 중요한지 실제 사례를 통해 이해할 수 있도록 구성했습니다.



🔍 MSSQL 인덱스 개념과 역할

인덱스는 테이블 데이터를 빠르게 찾기 위한 자료구조로, 도서관의 색인처럼 원하는 행을 최소한의 I/O로 찾아가게 돕습니다.
MSSQL에서는 크게 클러스터형 인덱스(데이터 페이지가 키 순서대로 정렬)와 비클러스터형 인덱스(별도 B-Tree에 키와 포인터 저장)로 나뉩니다.
클러스터형은 테이블당 1개만 가능하고, 비클러스터형은 여러 개를 둘 수 있어 조회 패턴에 맞춘 세밀한 최적화가 가능합니다.
인덱스가 있으면 옵티마이저가 Index Seek로 목표 범위를 빠르게 좁히고, 불필요한 정렬과 테이블 스캔을 줄여 응답 시간을 단축합니다.

다만 인덱스는 공짜가 아닙니다.
INSERT, UPDATE, DELETE 시 인덱스도 함께 갱신되어 쓰기 부하와 저장 공간이 늘어납니다.
따라서 “많을수록 좋다”가 아니라, 실제 쿼리에서 자주 쓰이는 컬럼을 선별해 만드는 것이 핵심입니다.
이 글 전반에서 일관되게 따를 원칙은 다음과 같습니다.
자주 조회되는 컬럼, WHERE 절의 필터링에 사용되는 컬럼, ORDER BY의 정렬이 필요한 컬럼을 대상으로 인덱스를 구성합니다.
이는 불변의 기준으로, 성능에 직접적인 영향을 주는 컬럼에만 집중함으로써 읽기/쓰기 균형을 맞춥니다.

🧭 인덱스가 빠르게 만드는 이유

인덱스 B-Tree는 루트 → 중간 → 리프 페이지로 이어지는 계층을 통해 탐색 깊이를 줄입니다.
선택도가 높은(값이 다양해 특정 범위가 작게 걸리는) 컬럼일수록 효과가 큽니다.
또한 정렬된 키 순서를 활용하므로, 정렬 필요가 있는 쿼리는 적절한 인덱스만으로도 Sort 연산을 피할 수 있습니다.
반대로, 계산이나 함수가 컬럼에 적용되면 SARGable(인덱스 사용 가능)하지 않게 되어 Seek가 아닌 Scan으로 전락할 수 있으니 WHERE 절에서 컬럼을 그대로 비교하는 습관이 중요합니다.

🧩 실무 관점의 기본 원칙

  • 🔎자주 조회되는 컬럼에 인덱스를 둡니다. 빈번한 SELECT에서 자주 나타나는 조건과 조인 키를 우선으로 고려합니다.
  • 🧱필터링 조건 컬럼(WHERE, JOIN ON)에 인덱스를 둡니다. 비교 연산 시 SARGable 형태를 유지하고, 함수/형변환을 컬럼에 직접 적용하지 않습니다.
  • ↕️정렬이 필요한 컬럼(ORDER BY, GROUP BY)은 인덱스 키 순서를 활용해 Sort 비용을 줄입니다. 필요한 경우 포함 컬럼(INCLUDE)로 커버링을 노립니다.
  • ⚖️쓰기 부하(INSERT/UPDATE/DELETE)와 저장 공간을 고려해 최소한의 인덱스로 유지합니다. 중복/유사 인덱스는 통합합니다.

🧪 간단 예시로 보는 효과

CODE BLOCK
-- 자주 조회 + 필터 + 정렬을 동시에 만족하는 인덱스
CREATE NONCLUSTERED INDEX IX_Order_Customer_Date
ON dbo.[Order](CustomerId, OrderDate)
INCLUDE (TotalAmount);

-- WHERE OrderDate BETWEEN ... AND ...
-- ORDER BY OrderDate DESC
-- 커버링(포함 컬럼)으로 Key Lookup 최소화

💎 핵심 포인트:
자주 조회되고 필터링 조건정렬에 쓰이는 컬럼을 중심으로 인덱스를 설계하면, Seek/커버링을 통해 I/O를 줄이고 정렬 비용을 최소화할 수 있습니다.

⚠️ 주의: 함수/형변환이 컬럼에 먼저 적용되면 인덱스가 무력화될 수 있습니다. 또한 낮은 선택도의 불리언/플래그 컬럼은 단독 인덱스 효과가 제한적일 수 있으니, 실제 실행 계획과 통계를 바탕으로 판단하세요.

📊 인덱스 컬럼 선택의 핵심 기준

어떤 컬럼을 인덱스로 삼을지 결정하는 일은 성능 최적화의 출발점입니다.
선택 기준이 명확하지 않으면 중복 인덱스가 늘고, 쓰기 성능이 악화되며, 실제로는 사용되지 않는 인덱스가 공간만 차지하게 됩니다.
이 섹션에서는 MSSQL에서 인덱스 컬럼을 고를 때 반드시 검토해야 할 핵심 요소를 정리합니다.
이 글의 전체 원칙은 변하지 않습니다.
자주 조회되는 컬럼, WHERE와 JOIN의 필터링 조건에 쓰이는 컬럼, 그리고 ORDER BY나 GROUP BY 등 정렬/집계에 관여하는 컬럼을 중심으로 인덱스를 구성하는 것입니다.

🧮 선택도와 카디널리티

선택도(Selectivity)는 특정 조건이 전체 행 중 얼마나 적은 범위를 고르는지를 의미합니다.
값이 다양하고 균등하게 분포된 컬럼일수록 선택도가 높아 인덱스 효과가 큽니다.
예를 들어 고객ID, 주문일자는 보통 선택도가 높지만, 활성여부(bit)와 같이 값이 몇 개로 한정된 컬럼은 단독 인덱스 효과가 제한적일 수 있습니다.
분포 통계를 주기적으로 갱신해 옵티마이저가 최신 분포를 반영하도록 관리하는 것도 중요합니다.

🧷 복합 인덱스의 컬럼 순서

복합 인덱스는 좌측 부분 규칙(Leftmost Prefix Rule)을 따릅니다.
따라서 WHERE/JOIN 조건에서 가장 자주, 그리고 선택도가 높은 컬럼을 선두에 배치해야 합니다.
또한 범위 조건(BETWEEN, >, <)이 등장하는 컬럼 뒤의 키는 사용성이 급격히 떨어질 수 있으므로, 동등 비교(=)가 가능한 컬럼을 앞쪽에 두는 것이 일반적으로 유리합니다.
정렬까지 고려해야 한다면 ORDER BY 순서와 방향(ASC/DESC)을 인덱스 키 설계에 함께 반영합니다.

📦 커버링과 INCLUDE 컬럼

인덱스가 필요한 모든 컬럼(조건, 조인, 선택된 컬럼)을 담으면 Key Lookup 없이 리프 페이지에서 바로 결과를 반환할 수 있습니다.
이때 키 순서에 넣기 어려운 출력 전용 컬럼은 INCLUDE로 추가하여 커버링을 달성합니다.
단, INCLUDE가 과도하면 인덱스 크기가 커지고 쓰기 부하가 증가하므로, 조회 빈도와 응답 시간 개선 폭을 기준으로 최소화합니다.

🧪 필터드 인덱스와 부분 데이터

데이터의 일부 구간만 자주 조회된다면 Filtered Index가 효과적입니다.
예: 최근 90일 주문만 자주 조회되는 경우 WHERE OrderDate >= DATEADD(day,-90,GETDATE()) 조건으로 필터드 인덱스를 정의해 크기를 줄이고 캐시 적중률을 올릴 수 있습니다.
단, 필터 조건은 쿼리와 정확히 일치해야 옵티마이저가 안정적으로 선택합니다.

  • 📈선택도가 높은 컬럼을 우선 고려합니다.
  • 🔗JOIN/WHERE의 동등 비교 컬럼을 복합 인덱스의 선두 키로 둡니다.
  • 🗂️출력 전용 컬럼은 INCLUDE로 커버링을 달성합니다.
  • 🧪부분 조회가 잦다면 Filtered Index를 검토합니다.
  • ⚠️함수/형변환이 컬럼에 적용되면 SARGability가 떨어집니다.
CODE BLOCK
-- 복합 인덱스: 동등 비교 컬럼을 선두, 정렬 고려
CREATE NONCLUSTERED INDEX IX_Order_Cust_Date
ON dbo.[Order](CustomerId, OrderDate DESC)
INCLUDE (TotalAmount);

-- 필터드 인덱스: 최근 90일만 대상으로
CREATE NONCLUSTERED INDEX IX_Order_Date_90d
ON dbo.[Order](OrderDate)
WHERE OrderDate >= DATEADD(day, -90, GETDATE());

기준 검토 포인트
선택도 값 분포가 넓고 균등한가, 통계 최신화가 되었는가
컬럼 순서 동등 비교 > 범위, 정렬 방향과 일치 여부
커버링 INCLUDE로 Lookup 제거, 과도한 포함 방지
필터드 쿼리와 동일한 WHERE 조건, 대상 범위 명확화

⚠️ 주의: 데이터 타입이 큰 컬럼(VARCHAR(MAX), NVARCHAR(4000) 등)을 키로 두면 인덱스 폭이 커져 유지 비용이 급증합니다.
가능하면 정규화된 짧은 키(예: INT, BIGINT, DATETIME2)를 사용하고, 긴 텍스트는 INCLUDE 또는 별도 설계로 처리하세요.



⚙️ 자주 조회되는 컬럼 최적화 방법

읽기 성능을 높이려면 무엇보다 자주 조회되는 컬럼을 정확히 찾아내 인덱스로 보호하는 것이 첫걸음입니다.
업무에서 반복되는 조회 패턴은 대체로 일정하며, 이러한 패턴을 인덱스가 잘 받쳐주면 CPU 사용량과 I/O가 눈에 띄게 줄어듭니다.
반대로, 빈도가 낮은 쿼리까지 모두 챙기려 들면 인덱스가 과도해져 쓰기 부하만 늘어납니다.
이 섹션에서는 실제 환경에서 “무엇을, 어떤 순서로” 확인해 자주 조회되는 컬럼을 선별하고, 커버링/포함 컬럼까지 고려해 최소한의 비용으로 최대 성능을 내는 방법을 정리합니다.

📡 조회 빈도 파악: DMV와 Query Store

먼저 “무엇이 자주 조회되는가”를 수치로 확인합니다.
sys.dm_db_index_usage_stats는 인덱스 사용 패턴(Seek/Scan/Lookup/Update)을 보여주고, Query Store는 실행 빈도와 지속적인 상위 쿼리를 파악하는 데 유용합니다.
이 둘을 함께 보며 WHERE/JOIN/ORDER BY에서 반복 출현하는 컬럼을 추려, 인덱스 후보군을 생성합니다.

CODE BLOCK
-- 인덱스 사용 현황 (Seek/Scan/Lookup/Update 횟수)
SELECT DB_NAME() AS dbname, OBJECT_SCHEMA_NAME(s.[object_id]) AS [schema], OBJECT_NAME(s.[object_id]) AS table_name,
       i.name AS index_name, s.user_seeks, s.user_scans, s.user_lookups, s.user_updates
FROM sys.dm_db_index_usage_stats AS s
JOIN sys.indexes AS i
  ON s.[object_id] = i.[object_id] AND s.index_id = i.index_id
WHERE s.database_id = DB_ID()
ORDER BY s.user_seeks DESC;

-- Query Store 상위 빈도 쿼리 (SQL Server 2016+)
SELECT TOP 50 qsrs.count_executions, qsq.query_sql_text
FROM sys.query_store_runtime_stats AS qsrs
JOIN sys.query_store_plan AS qsp ON qsrs.plan_id = qsp.plan_id
JOIN sys.query_store_query AS qsqry ON qsp.query_id = qsqry.query_id
JOIN sys.query_store_query_text AS qsq ON qsqry.query_text_id = qsq.query_text_id
ORDER BY qsrs.count_executions DESC;

🧭 패턴 기반 설계: 조회 열과 출력 열 분리

빈번한 SELECT에서 조건/조인 컬럼은 인덱스의 키 컬럼으로, 화면/리포트에 출력만 필요한 컬럼INCLUDE로 분리합니다.
이렇게 하면 Seek 후 리프 페이지에서 바로 결과를 반환할 수 있어 Lookup을 줄입니다.
또한 동일 테이블에서 여러 화면이 공통으로 사용하는 조건 컬럼이 있다면 이를 중심으로 단일 인덱스를 설계하고, 화면별 차이는 INCLUDE 조합으로 해결해 인덱스 난립을 막습니다.

워크로드 패턴 권장 인덱스 설계
최근 N일 주문 조회 키: (CustomerId, OrderDate DESC), INCLUDE: (TotalAmount, Status)
상세 화면 단건 조회 키: (BusinessKey 또는 Natural Key), 필요 시 UNIQUE 보장
리스트 페이징 + 정렬 키 순서에 ORDER BY 반영, OFFSET/FETCH 커버링

🧪 예시: 조회 최적화 인덱스와 실행 계획

CODE BLOCK
-- 최근 주문 목록을 고객별로 자주 조회 & 정렬
CREATE NONCLUSTERED INDEX IX_Order_Customer_OrderDate
ON dbo.[Order] (CustomerId, OrderDate DESC)
INCLUDE (OrderNo, TotalAmount, Status);

-- 페이지네이션 (정렬 일치)
SELECT OrderNo, OrderDate, TotalAmount, Status
FROM dbo.[Order]
WHERE CustomerId = @CustomerId
ORDER BY OrderDate DESC
OFFSET @Skip ROWS FETCH NEXT @Take ROWS ONLY;

💎 핵심 포인트:
반복적으로 호출되는 조회 쿼리의 조건 열은 키로, 화면 표시 열은 INCLUDE로 나눕니다.
ORDER BY와 키 순서를 일치시켜 Sort를 제거하고, 가능한 한 커버링을 달성하세요.

🧰 유지 관리: 과유불급 원칙

  • 📈상위 20% 쿼리가 전체 트래픽 대부분을 차지합니다. 이들 쿼리의 컬럼만 우선 최적화합니다.
  • 🧹유사/중복 인덱스는 통합하고, 사용이 거의 없는 인덱스는 제거를 검토합니다.
  • 🧮INCLUDE는 최소화하여 인덱스 폭을 제어하고, 통계는 자동/수동으로 최신 상태를 유지합니다.
  • ⚖️읽기 이득과 쓰기 비용을 균형 있게 평가합니다. 쓰기 빈도가 높은 테이블은 인덱스를 절제합니다.

⚠️ 주의: 조회가 잦더라도 선택도가 낮은 플래그/상태 컬럼을 단독 키로 두면 효과가 제한적입니다.
또한 컬럼에 함수나 암시적 형변환이 걸리면 인덱스가 제대로 활용되지 않을 수 있으니 WHERE 절에서 컬럼을 그대로 비교하세요.

🎯 필터링 조건에 적합한 컬럼 선택

WHERE 절과 JOIN ON에서 실제로 데이터를 가려내는 컬럼이 인덱스의 핵심 대상입니다.
특히 선택도가 높고 동등 비교가 가능한 컬럼을 선두 키로 두면 Index Seek로 빠르게 범위를 좁힐 수 있습니다.
반면, 컬럼에 함수·형변환이 적용되거나 데이터 타입/정렬규칙이 맞지 않으면 SARGable 속성이 깨지며 Scan으로 전락합니다.
이 섹션은 “자주 조회되는, 필터링 조건에 사용되는, 정렬이 필요한 컬럼을 대상으로 인덱스를 구성한다”는 원칙을 필터링 관점에서 구체화합니다.

🧮 SARGability를 지키는 조건 작성법

필터 조건은 컬럼 = 상수/변수 형태가 이상적입니다.
컬럼 쪽에 함수/연산이 오면 인덱스 키 정렬을 활용하지 못합니다.
또한 데이터 타입이 다른 비교(예: NVARCHAR ↔ INT)는 암시적 형변환을 유발할 수 있어 피합니다.
날짜 범위는 반개구간 패턴(예: >= 시작 AND < 다음날)으로 명확히 적어 카디널리티 추정을 돕습니다.

CODE BLOCK
-- ✅ SARGable
WHERE OrderDate >= @FromDate
  AND OrderDate <  DATEADD(day, 1, @ToDate);

-- ❌ 비SARGable (컬럼에 함수 적용)
WHERE CONVERT(date, OrderDate) = @TargetDate;

-- ✅ 타입 일치 비교 (암시적 변환 방지)
WHERE CustomerId = @CustomerIdInt;

-- ❌ 컬레이션/타입 불일치로 인덱스 사용 저하 가능
WHERE Email = N'abc@example.com' COLLATE Korean_Wansung_CS_AS;

🧷 동등, 범위, 패턴 검색별 인덱스 전략

필터 유형 권장 설계
동등 비교 (=) 복합 인덱스 선두 키로 배치, 높은 선택도 컬럼을 먼저 둠
범위 (>, <, BETWEEN) 범위 컬럼 뒤 키는 활용도↓.
동등 컬럼을 앞에 두고, 정렬 필요 시 키 방향 일치
LIKE 검색 접두 검색(LIKE ‘abc%’)은 인덱스 사용 가능.
‘%abc’는 불가.
Computed Column + Persisted + 인덱스 고려
다중 조건 자주 결합되는 컬럼 묶음으로 복합 인덱스 설계, INCLUDE로 커버링

🔎 필터드 인덱스와 계산된 컬럼의 활용

Filtered Index는 자주 거르는 부분집합만 대상으로 작게 유지해 캐시 효율을 높입니다.
예를 들어 Status IN (‘Active’,’Paid’) 등 상시 조건이 있다면 필터드 인덱스로 Seek를 보장할 수 있습니다.
또한 Computed ColumnPERSISTED로 정의 후 인덱싱하면, 본래 비SARGable이던 표현식(예: LEFT(Email, 3))을 SARGable로 바꿀 수 있습니다.

CODE BLOCK
-- 최근 30일 + 활성 주문만 조회되는 워크로드
CREATE NONCLUSTERED INDEX IX_Order_Active_30d
ON dbo.[Order](OrderDate)
WHERE Status IN ('Active','Paid')
  AND OrderDate >= DATEADD(day, -30, SYSUTCDATETIME());

-- 접두 검색 최적화: 계산된 컬럼 + 인덱스
ALTER TABLE dbo.Customer ADD EmailPrefix AS LEFT(Email, 3) PERSISTED;
CREATE NONCLUSTERED INDEX IX_Customer_EmailPrefix ON dbo.Customer(EmailPrefix);

-- 사용 시
WHERE Email LIKE @prefix + N'%';

🧰 실무 체크리스트

  • 🧭WHERE/JOIN에 동등 비교가 가능한 컬럼을 복합 인덱스의 선두로 둔다.
  • 🧱범위 조건 컬럼 뒤 키 사용성 저하에 유의하고, 필요한 경우 인덱스를 분리한다.
  • 🔤데이터 타입/컬레이션을 일치시켜 암시적 변환을 제거한다.
  • 🎛️상시 조건은 Filtered Index로 작고 빠르게 유지한다.
  • 🧪비SARGable 표현식은 Persisted Computed Column으로 전환해 인덱싱한다.

💎 핵심 포인트:
필터링 조건은 동등 비교를 먼저, 범위는 뒤로, 정렬은 키 방향과 맞추는 것이 기본입니다.
함수/형변환을 컬럼에 적용하지 말고, 필요한 경우 Filtered/Computed 전략으로 SARGability를 확보하세요.

⚠️ 주의: 선택도가 낮은 플래그 컬럼을 단독 인덱스로 만들면 효익이 제한적입니다.
또한 여러 조건을 OR로 묶으면 인덱스 사용이 어려워질 수 있으니, 가능한 경우 UNION ALL이나 별도 인덱스 활용을 검토하세요.



📌 정렬 성능을 높이는 인덱스 설계

정렬은 쿼리에서 가장 비용이 큰 연산 중 하나입니다.
특히 대량 데이터에서 ORDER BYGROUP BY, DISTINCT, 윈도 함수는 메모리 그랜트와 TempDB 사용을 유발해 전체 응답 시간을 지연시킵니다.
핵심은 인덱스 키의 정렬 순서를 활용해 옵티마이저가 별도의 Sort 연산 없이 Ordered Scan/Seek으로 결과를 내도록 만드는 것입니다.
이 섹션에서는 “자주 조회되는, 필터링 조건에 사용되는, 정렬이 필요한 컬럼을 대상으로 인덱스를 구성한다”는 기준을 정렬 관점에서 구체화합니다.

🧭 ORDER BY와 키 순서의 일치

복합 인덱스는 키 컬럼 순서와 방향(ASC/DESC)을 가집니다.
ORDER BY가 인덱스 키 정의와 완전히 일치하면 Sort 없이 결과를 반환할 수 있습니다.
일반적으로 동등 조건(=)으로 고정되는 컬럼을 선두에 두고, 그 뒤에 정렬 컬럼을 배치하면 필터와 정렬을 동시에 만족시킬 수 있습니다.
또한 SQL Server는 단일 컬럼 방향에 대해 역방향 스캔을 지원하므로, 같은 컬럼 집합이라면 ASC 인덱스 하나로 DESC 정렬도 대개 처리 가능합니다.
다만 다중 컬럼에서 혼합 방향(예: A ASC, B DESC)이 필요하면, 해당 방향을 인덱스 키 정의에 반영하거나 별도 인덱스를 고려해야 합니다.

CODE BLOCK
-- 고객별 최신 주문을 최근순으로 나열 (필터 + 정렬 일치)
CREATE NONCLUSTERED INDEX IX_Order_Cust_OrderDateDesc
ON dbo.[Order](CustomerId ASC, OrderDate DESC)
INCLUDE (OrderNo, TotalAmount, Status);

-- 쿼리
SELECT OrderNo, OrderDate, TotalAmount, Status
FROM dbo.[Order]
WHERE CustomerId = @CustomerId
ORDER BY OrderDate DESC;
-- ➜ Index Seek + Ordered Scan으로 Sort 제거

⚡ TOP N, 페이징, 커버링의 삼박자

TOP N 또는 OFFSET/FETCH 페이징은 정렬과 결합될 때 특히 인덱스 효과가 큽니다.
선두 키로 필터 컬럼을 고정하고, 그 다음에 정렬 컬럼을 두면 정확히 필요한 범위만 읽어도 됩니다.
여기에 INCLUDE로 화면에 필요한 열을 커버링하면 Key Lookup을 제거해 I/O를 더 줄일 수 있습니다.

CODE BLOCK
-- TOP N: 최신 결제 완료 주문 20건
CREATE NONCLUSTERED INDEX IX_Order_Status_OrderDate
ON dbo.[Order](Status ASC, OrderDate DESC)
INCLUDE (OrderNo, CustomerId, TotalAmount);

SELECT TOP (20) OrderNo, CustomerId, TotalAmount, OrderDate
FROM dbo.[Order]
WHERE Status = 'Paid'
ORDER BY OrderDate DESC;

-- 페이징: OFFSET/FETCH와 키 방향 일치
SELECT OrderNo, TotalAmount, OrderDate
FROM dbo.[Order]
WHERE Status = 'Paid'
ORDER BY OrderDate DESC
OFFSET @Skip ROWS FETCH NEXT @Take ROWS ONLY;

🧩 그룹화, 집계, 윈도 함수 최적화

GROUP BY는 그룹 키가 인덱스 선두에 있으면 해시 대신 스트리밍 집계가 가능해지는 경우가 많습니다.
윈도 함수(ROW_NUMBER() OVER (PARTITION BY ... ORDER BY ...))도 PARTITION BY 컬럼 → ORDER BY 컬럼 순서로 키를 맞추면 정렬 비용을 줄일 수 있습니다.
단, 윈도 함수는 여전히 정렬이 필요할 수 있으므로, 분할 컬럼을 선두로 두고 결과 집합을 최소화하는 필터를 적극 활용하세요.

시나리오 권장 인덱스/팁
부서별 최근 가입자 순 키: (DeptId, JoinDate DESC), INCLUDE: (UserName, Email)
월별 매출 합계 GROUP BY 키: (SaleYear, SaleMonth), 집계 전 최근 기간만 필터링
ROW_NUMBER 파티션 + 정렬 키: (PartitionCols…, OrderCols…), 필요한 컬럼만 SELECT

💎 핵심 포인트:
정렬을 줄이는 최단 경로는 키 순서와 ORDER BY의 일치, 필터 → 정렬 흐름에 맞춘 복합 키, 그리고 커버링으로 Lookup 제거입니다.

🛡️ 실제 운영에서의 주의사항

  • 🔀혼합 방향 정렬(A ASC, B DESC)이 상시 필요하면 키 방향을 그대로 정의하거나, 용도별로 별도 인덱스를 고려합니다.
  • 🧮긴 문자열을 정렬 키로 두면 폭이 커져 비효율적입니다. 가능한 숫자/날짜 키를 사용하고, 표시용 텍스트는 INCLUDE로 이동합니다.
  • 🧠정렬 전 필터링을 최대한 수행해 입력 크기를 줄입니다(최근 N일, 특정 상태 등).
  • 🧪실행 계획에서 Sort, Memory Grant, Spill(TempDB)을 확인하고, 인덱스/통계를 조정합니다.

⚠️ 주의: OFFSET이 큰 페이징은 후반 페이지로 갈수록 건너뛰기 비용이 커집니다.
가능하면 Keyset Pagination(마지막 키 기준 > 비교)으로 전환해 불필요한 스캔을 줄이세요.

💡 TIP: 혼합 방향 요구가 잦다면, 계산된 컬럼을 활용해 정렬 키를 통일하는 방법도 있습니다(예: 오름차순 기준으로 내림차순을 -값으로 치환한 Persisted Computed Column 인덱싱).

자주 묻는 질문 (FAQ)

🔎 어떤 컬럼에 인덱스를 먼저 걸어야 하나요?
실사용 쿼리에서 자주 조회되는 컬럼, WHERE/JOIN의 필터링 조건에 쓰이는 컬럼, ORDER BY/Group By의 정렬/집계 컬럼을 최우선으로 인덱싱합니다.
이 원칙이 MSSQL 인덱스 설계의 핵심 기준입니다.
⚖️ 쓰기 트래픽이 많은 테이블에도 인덱스를 늘려도 될까요?
인덱스는 DML마다 갱신 비용이 듭니다.
상위 트래픽 쿼리의 핵심 컬럼에만 최소 개수로 적용하고, 유사/중복 인덱스는 통합합니다.
조회 이득과 쓰기 비용을 수치로 비교해 의사결정하세요.
🔁 ASC 인덱스로 DESC 정렬도 빠르게 되나요?
단일 컬럼은 역방향 스캔이 가능해 ASC 인덱스로 DESC도 처리하는 경우가 많습니다.
다중 컬럼의 혼합 방향(A ASC, B DESC)이면 인덱스 키 방향을 일치시키거나 별도 인덱스를 고려하세요.
🧮 선택도가 낮은 플래그 컬럼은 어떻게 다루나요?
단독 인덱스 효과가 제한적입니다.
보통 자주 결합되는 고선택도 컬럼과 함께 복합 인덱스의 뒤쪽에 두거나, 자주 쓰는 값만 대상으로 Filtered Index를 검토합니다.
🔤 LIKE 검색은 인덱스로 빨라질 수 있나요?
접두 검색(LIKE N'abc%')은 인덱스 사용이 가능합니다.
반대로 '%abc'는 불가합니다.
필요 시 Persisted Computed Column으로 접두 키를 만들어 인덱싱하는 방법을 사용하세요.
🎯 Filtered Index를 만들 때 주의할 점은 무엇인가요?
필터 조건은 실제 쿼리의 WHERE와 정확히 일치해야 안정적으로 선택됩니다.
상시 조건(예: Status IN ('Active','Paid'), 최근 N일 등)일 때 효과가 크며, 범위를 너무 넓히면 일반 인덱스와 차별성이 사라집니다.
📦 INCLUDE 컬럼은 얼마나 넣어야 하나요?
커버링을 위해 출력 전용 컬럼을 넣되, 과도한 INCLUDE는 인덱스 폭과 쓰기 비용을 키웁니다.
가장 자주 쓰는 쿼리의 SELECT 목록을 기준으로 최소화하세요.
🧭 인덱스 효과는 어떻게 확인하고 유지하나요?
실행 계획에서 Seek/Scan/Sort/Lookup을 확인하고, sys.dm_db_index_usage_stats로 사용/갱신 패턴을 점검합니다.
Query Store로 상위 쿼리를 추적하며, 통계를 최신으로 유지해 옵티마이저의 추정을 돕습니다.

🧭 인덱스 컬럼 선택 기준 총정리

MSSQL 인덱스 설계의 출발점은 실사용 쿼리입니다.
이 글에서 제시한 핵심 원칙은 자주 조회되는 컬럼, WHERE·JOIN의 필터링 조건에 쓰이는 컬럼, ORDER BY·GROUP BY 등 정렬/집계에 관여하는 컬럼을 중심으로 인덱스를 구성하는 것입니다.
복합 인덱스는 좌측 부분 규칙을 따르므로 동등 비교가 가능한 고선택도 컬럼을 선두에 배치하고, 정렬이 필요하다면 키 방향을 쿼리와 일치시켜 Sort 비용을 줄입니다.
출력 전용 컬럼은 INCLUDE로 커버링해 Lookup을 최소화하고, 일부 구간만 자주 쓰인다면 Filtered Index로 크기와 캐시 효율을 개선합니다.
쓰기 부하와 저장 공간은 항상 비용으로 인식하고, DMV·Query Store로 사용 현황을 주기적으로 점검해 중복 인덱스를 통합하세요.

또한 SARGability를 지키기 위해 컬럼에 함수·형변환을 직접 적용하지 말고, 필요한 경우 Persisted Computed Column을 통해 인덱싱 가능한 형태로 전환합니다.
페이징이나 TOP N 시나리오에서는 필터 → 정렬 → 커버링의 순서로 설계해 최소 범위만 읽도록 만듭니다.
운영 환경에서는 통계 최신화, 메모리 그랜트와 TempDB 스필 여부, Sort 존재 유무를 실행 계획에서 확인하며 지속적으로 미세 조정하는 것이 성능의 관건입니다.
이 원칙들을 일관되게 적용하면 읽기 성능은 안정적으로 향상되고, 쓰기 비용과 스토리지는 통제 가능한 수준으로 유지됩니다.


🏷️ 관련 태그 : MSSQL인덱스, 인덱스설계, 복합인덱스, 커버링인덱스, 필터드인덱스, SARGable, 실행계획튜닝, QueryStore, 선택도카디널리티, ORDERBY최적화