SQL Server 컬럼스토어 인덱스 — OLAP 성능의 핵심

SQL Server 컬럼스토어 인덱스의 열 기반 저장 구조, 행 그룹·세그먼트·델타 스토어의 동작 원리, 클러스터형/비클러스터형 컬럼스토어의 활용 패턴을 설명합니다.

· 5 min read · PALDYN Team

지난 글에서 필터된 인덱스로 크기와 성능을 최적화하는 방법을 살펴봤다. 이번에는 OLAP 분석 쿼리를 수십 배 빠르게 만드는 **컬럼스토어 인덱스(Columnstore Index)**를 깊이 파헤친다.

왜 컬럼 저장인가

전통적인 행 저장(Rowstore)은 행 단위로 데이터를 페이지에 담는다. 단일 행을 읽거나 쓰는 OLTP 작업에 최적이지만, SUM(revenue) 같은 집계에서는 불필요한 열(name, email 등)까지 모두 읽어 I/O가 낭비된다.

컬럼스토어는 열 단위로 데이터를 저장한다. SUM(salary)를 구할 때 salary 세그먼트만 읽어 I/O를 극적으로 줄인다. 같은 열의 값끼리 모여있어 압축률도 월등하다.

행 저장 vs 컬럼 저장 구조 비교

컬럼스토어 인덱스 생성

-- 클러스터형 컬럼스토어 (테이블 자체가 열 저장)
-- 주로 DW/팩트 테이블에 사용
CREATE TABLE sales_fact (
    sale_id      BIGINT         NOT NULL,
    sale_date    DATE           NOT NULL,
    customer_id  INT            NOT NULL,
    product_id   INT            NOT NULL,
    quantity     INT            NOT NULL,
    amount       DECIMAL(18,2)  NOT NULL,
    INDEX csi_sales CLUSTERED COLUMNSTORE
);

-- 비클러스터형 컬럼스토어 (행 저장 테이블에 추가)
-- OLTP 테이블에서 분석 쿼리 가속
CREATE NONCLUSTERED COLUMNSTORE INDEX ncsi_orders
ON orders (order_date, customer_id, amount, status);

내부 구조: 행 그룹과 세그먼트

컬럼스토어는 행 그룹(Row Group) 단위로 관리된다. 각 행 그룹은 약 100만 행을 담는다. 행 그룹 안에서 열마다 세그먼트(Segment) 단위로 압축 저장된다.

-- 행 그룹 상태 확인
SELECT rg.state_desc,
       rg.total_rows,
       rg.deleted_rows,
       rg.size_in_bytes
FROM   sys.column_store_row_groups rg
WHERE  rg.object_id = OBJECT_ID('sales_fact')
ORDER  BY rg.row_group_id;

-- 세그먼트 압축 정보
SELECT cs.column_id,
       c.name AS col_name,
       cs.row_count,
       cs.on_disk_size / 1024.0 AS kb,
       cs.encoding_type
FROM   sys.column_store_segments cs
JOIN   sys.columns c ON c.object_id = cs.object_id
                     AND c.column_id = cs.column_id
WHERE  cs.object_id = OBJECT_ID('sales_fact');

델타 스토어 동작 원리

델타 스토어와 쓰기 처리

컬럼스토어는 대용량 배치 로드에 최적화되어 있지만, 개별 행 INSERT도 지원한다. 새로 삽입된 행은 먼저 **델타 스토어(Delta Store)**에 행 저장 형식으로 쌓인다. 약 100만 행이 쌓이면 Tuple Mover 백그라운드 프로세스가 압축하여 새 행 그룹으로 전환한다.

DELETE는 실제로 행을 지우지 않고 **삭제 비트맵(Delete Bitmap)**에만 표시한다. 물리적 제거는 다음 REORGANIZE 시에 이루어진다.

-- 대량 INSERT (델타 스토어 우회, 직접 압축 행 그룹 생성)
-- TABLOCK 힌트로 최소 로그 + 직접 압축
INSERT INTO sales_fact WITH (TABLOCK)
SELECT ...
FROM   staging_sales;

-- 델타 스토어 강제 압축 (인덱스 재구성)
ALTER INDEX csi_sales ON sales_fact REORGANIZE
WITH (COMPRESS_ALL_ROW_GROUPS = ON);

-- 인덱스 재생성 (완전 재구축)
ALTER INDEX csi_sales ON sales_fact REBUILD;

분석 쿼리 성능 비교

-- 아래 쿼리는 컬럼스토어 인덱스로 수십 배 빨라짐
SELECT YEAR(sale_date)  AS yr,
       MONTH(sale_date) AS mo,
       SUM(amount)      AS total_revenue,
       COUNT(DISTINCT customer_id) AS unique_customers
FROM   sales_fact
WHERE  sale_date >= '2025-01-01'
GROUP  BY YEAR(sale_date), MONTH(sale_date)
ORDER  BY yr, mo;

-- 실행 계획: Batch Mode Columnstore Scan + Batch Mode Hash Aggregate
-- 배치 모드: SIMD 명령으로 벡터 연산, 한 번에 900개 행 처리

OLTP + OLAP 혼합: 비클러스터형 컬럼스토어

SQL Server 2014 이상에서 비클러스터형 컬럼스토어를 OLTP 테이블에 추가하면 행 저장(OLTP용)과 열 저장(분석용)이 공존한다. 단건 INSERT/UPDATE는 행 저장을 사용하고, 집계 쿼리는 옵티마이저가 자동으로 컬럼스토어를 선택한다.

-- OLTP 테이블에 분석 인덱스 추가
CREATE NONCLUSTERED COLUMNSTORE INDEX ncsi_orders_analytics
ON orders (order_date, customer_id, product_id, amount, status);

-- 단건 조회: 행 저장 클러스터 인덱스 사용 (기존 OLTP 성능 유지)
SELECT * FROM orders WHERE order_id = 12345;

-- 집계 쿼리: 옵티마이저가 컬럼스토어 선택
SELECT product_id, SUM(amount)
FROM   orders
WHERE  order_date >= '2026-01-01'
GROUP  BY product_id;

지난 글: SQL Server 필터된 인덱스 — 조건부 인덱스로 공간과 성능 최적화

다음 글: SQL Server In-Memory OLTP — Hekaton 메모리 최적화 테이블


읽어주셔서 감사합니다. 😊