들어가며
Redshift를 운영하다 보면 갑자기 쿼리가 느려지거나, 특정 세션이 멈춰있거나, 락 때문에 다른 작업이 블로킹되는 상황을 자주 마주합니다.
인터넷을 검색하면 수많은 튜닝 가이드가 나오지만, 정작 실제 Redshift 시스템 테이블 이름이 틀리거나, 존재하지 않는 컬럼을 참조하는 경우가 많습니다. ChatGPT나 Gemini에게 물어봐도 일반 PostgreSQL 기준으로 답변하거나, 실제로는 작동하지 않는 쿼리를 알려주는 경우가 대부분입니다.
이 글에서는 AWS 공식 문서에 명시된 실제 시스템 뷰와 정확한 컬럼명을 기반으로, 프로비저닝 클러스터 환경에서 바로 사용할 수 있는 트러블슈팅 쿼리와 튜닝 포인트를 정리했습니다.
1. Redshift 시스템 뷰 체계 이해하기
Redshift는 여러 카테고리의 시스템 뷰를 제공하며, 프로비저닝 클러스터와 서버리스에서 사용 가능한 뷰가 다릅니다.
1.1 프로비저닝 클러스터 전용 뷰
프로비저닝 환경에서만 사용 가능한 시스템 뷰들입니다.
STL (System Table Log) 뷰
- 디스크에 영구 저장된 로그 기반
- 시스템 히스토리 정보 제공
- 7일간의 로그 보존 (자동 관리)
- 예: STL_QUERY, STL_SESSIONS, STL_WLM_QUERY
STV (System Table Virtual) 테이블
- 현재 시스템 데이터의 실시간 스냅샷
- 인메모리 가상 테이블 (영구 저장 안 됨)
- 예: STV_LOCKS, STV_SESSIONS, STV_RECENTS
SVL (System View Log) 뷰
- STL 테이블의 조인 및 요약 뷰
- 기본 클러스터 쿼리 정보
- 예: SVL_QLOG, SVL_QUERY_SUMMARY
SVCS (System View Concurrency Scaling) 뷰
- 동시성 확장 클러스터 + 기본 클러스터 정보
- 예: SVCS_QUERY_SUMMARY
1.2 공통 사용 가능 뷰
프로비저닝과 서버리스 모두에서 사용 가능합니다.
SYS 모니터링 뷰
- 최신 통합 모니터링 뷰 (AWS 권장)
- 예: SYS_QUERY_HISTORY, SYS_QUERY_DETAIL
SVV (System View Visible) 뷰
- 데이터베이스 객체 메타데이터
- 예: SVV_TABLES, SVV_TABLE_INFO
2. 락(Lock) 모니터링 및 해결
2.1 현재 락 상태 확인 (STV_LOCKS)
-- 현재 발생 중인 모든 락 확인
SELECT
table_id,
last_update,
lock_owner,
lock_owner_pid,
lock_status,
lock_owner_start_ts,
lock_owner_end_ts
FROM
STV_LOCKS
ORDER BY
last_update DESC;
주요 컬럼:
- lock_status: 'Holding' (락 보유 중) / 'Waiting' (락 대기 중)
- lock_owner: 트랜잭션 ID
- lock_owner_pid: 락을 보유한 프로세스 ID
2.2 락과 테이블명 매칭
-- 락 정보에 테이블명 추가
SELECT
l.table_id,
TRIM(t.name) AS table_name,
l.lock_owner,
l.lock_owner_pid,
l.lock_status,
l.lock_owner_start_ts,
DATEDIFF(second, l.lock_owner_start_ts, GETDATE()) AS lock_duration_sec
FROM
STV_LOCKS l
LEFT JOIN
STV_TBL_PERM t ON l.table_id = t.id
ORDER BY
lock_duration_sec DESC;
2.3 블로킹 체인 추적
-- 누가 누구를 블로킹하고 있는지 확인
SELECT
w.lock_owner_pid AS waiting_pid,
w.table_id,
TRIM(t.name) AS waiting_on_table,
h.lock_owner_pid AS blocking_pid,
DATEDIFF(second, w.lock_owner_start_ts, GETDATE()) AS wait_duration_sec
FROM
STV_LOCKS w
JOIN
STV_LOCKS h
ON w.table_id = h.table_id
AND w.lock_owner_pid <> h.lock_owner_pid
LEFT JOIN
STV_TBL_PERM t ON w.table_id = t.id
WHERE
w.lock_status = 'Waiting'
AND h.lock_status = 'Holding'
ORDER BY
wait_duration_sec DESC;
2.4 락을 보유한 세션의 현재 쿼리 확인
-- 락을 잡고 있는 세션이 실행 중인 쿼리 확인
SELECT
l.lock_owner_pid AS pid,
l.table_id,
TRIM(t.name) AS table_name,
l.lock_status,
s.user_name,
s.db_name,
s.starttime AS session_start,
DATEDIFF(minute, s.starttime, GETDATE()) AS session_duration_min,
r.status AS query_status,
LEFT(r.query, 100) AS current_query
FROM
STV_LOCKS l
JOIN
STV_SESSIONS s ON l.lock_owner_pid = s.process
LEFT JOIN
STV_TBL_PERM t ON l.table_id = t.id
LEFT JOIN
STV_RECENTS r ON s.process = r.pid AND r.status = 'Running'
WHERE
l.lock_status = 'Holding'
ORDER BY
session_duration_min DESC;
3. 세션 관리
3.1 활성 세션 조회 (STV_SESSIONS)
-- 현재 연결된 모든 세션 확인
SELECT
process AS pid,
user_name,
db_name,
starttime,
DATEDIFF(minute, starttime, GETDATE()) AS session_duration_min
FROM
STV_SESSIONS
WHERE
user_name <> 'rdsdb' -- 시스템 사용자 제외
ORDER BY
starttime DESC;
3.2 오래 실행 중인 세션 찾기
-- 1시간 이상 유지된 세션
SELECT
s.process AS pid,
s.user_name,
s.db_name,
s.starttime,
DATEDIFF(hour, s.starttime, GETDATE()) AS hours_active,
q.query,
LEFT(q.querytxt, 80) AS last_query
FROM
STV_SESSIONS s
LEFT JOIN
STL_QUERY q ON s.process = q.pid
AND q.endtime IS NULL
WHERE
DATEDIFF(hour, s.starttime, GETDATE()) > 1
AND s.user_name <> 'rdsdb'
ORDER BY
hours_active DESC;
3.3 유휴 세션 확인
-- 장시간 아무 작업도 하지 않는 세션
SELECT
s.process AS pid,
s.user_name,
s.db_name,
s.starttime,
DATEDIFF(hour, s.starttime, GETDATE()) AS session_hours,
q.endtime AS last_query_time,
DATEDIFF(minute, q.endtime, GETDATE()) AS idle_minutes
FROM
STV_SESSIONS s
LEFT JOIN
(SELECT pid, MAX(endtime) AS endtime
FROM STL_QUERY
GROUP BY pid) q ON s.process = q.pid
WHERE
s.user_name <> 'rdsdb'
AND DATEDIFF(minute, COALESCE(q.endtime, s.starttime), GETDATE()) > 30
ORDER BY
idle_minutes DESC;
3.4 특정 세션 강제 종료
-- 세션 종료 (PID 확인 후 실행)
SELECT pg_terminate_backend(12345); -- 실제 PID로 대체
-- 또는 특정 사용자의 모든 세션 종료
SELECT pg_terminate_backend(process)
FROM STV_SESSIONS
WHERE user_name = 'problem_user';
⚠️ 주의사항:
- 운영 환경에서는 신중하게 사용
- 가능하면 해당 사용자와 커뮤니케이션 후 실행
- 트랜잭션이 롤백되므로 데이터 정합성 확인 필요
4. 실행 중인 쿼리 모니터링
4.1 현재 실행 중인 쿼리 (STV_RECENTS)
-- 지금 실행되고 있는 쿼리 확인
SELECT
userid,
status,
starttime,
duration / 1000000 AS duration_sec,
user_name,
db_name,
LEFT(query, 100) AS query_preview,
pid
FROM
STV_RECENTS
WHERE
status = 'Running'
ORDER BY
starttime ASC;
4.2 장시간 실행 중인 쿼리
-- 30초 이상 실행 중인 쿼리
SELECT
userid,
user_name,
db_name,
starttime,
duration / 1000000 AS duration_sec,
LEFT(query, 100) AS query_preview,
pid
FROM
STV_RECENTS
WHERE
status = 'Running'
AND duration > 30000000 -- 30초 = 30,000,000 마이크로초
ORDER BY
duration DESC;
4.3 STL_QUERY로 상세 히스토리 확인
-- 최근 1시간 동안 실행된 쿼리 (완료/실패 포함)
SELECT
query,
userid,
xid AS transaction_id,
pid,
database,
starttime,
endtime,
DATEDIFF(second, starttime, endtime) AS duration_sec,
aborted,
LEFT(querytxt, 100) AS query_text
FROM
STL_QUERY
WHERE
starttime >= DATEADD(hour, -1, GETDATE())
AND userid > 1 -- 시스템 쿼리 제외
ORDER BY
starttime DESC
LIMIT 50;
4.4 느린 쿼리 TOP 10
-- 가장 오래 걸린 쿼리 (최근 24시간)
SELECT
query,
userid,
database,
starttime,
endtime,
DATEDIFF(second, starttime, endtime) AS duration_sec,
LEFT(querytxt, 100) AS query_preview
FROM
STL_QUERY
WHERE
starttime >= DATEADD(day, -1, GETDATE())
AND userid > 1
AND aborted = 0 -- 성공한 쿼리만
ORDER BY
duration_sec DESC
LIMIT 10;
5. WLM(Workload Management) 모니터링
5.1 WLM 큐 대기 확인
-- WLM 큐에서 대기 중인 쿼리
SELECT
query,
userid,
service_class,
slot_count,
total_queue_time / 1000000 AS queue_time_sec,
total_exec_time / 1000000 AS exec_time_sec,
queue_start_time,
exec_start_time,
service_class_name
FROM
STL_WLM_QUERY
WHERE
queue_start_time >= DATEADD(hour, -1, GETDATE())
AND total_queue_time > 5000000 -- 5초 이상 대기
ORDER BY
total_queue_time DESC
LIMIT 20;
5.2 서비스 클래스별 평균 대기/실행 시간
-- 각 WLM 큐의 평균 성능
SELECT
service_class,
service_class_name,
COUNT(*) AS query_count,
AVG(total_queue_time / 1000000) AS avg_queue_sec,
AVG(total_exec_time / 1000000) AS avg_exec_sec,
MAX(total_queue_time / 1000000) AS max_queue_sec,
MAX(total_exec_time / 1000000) AS max_exec_sec
FROM
STL_WLM_QUERY
WHERE
service_class > 4
AND queue_start_time >= DATEADD(day, -1, GETDATE())
GROUP BY
service_class, service_class_name
ORDER BY
service_class;
5.3 WLM 큐별 동시 실행 쿼리 수
-- 현재 각 큐에서 실행 중인 쿼리 수
SELECT
w.service_class,
w.service_class_name,
COUNT(*) AS running_queries,
SUM(w.slot_count) AS total_slots_used
FROM
STL_WLM_QUERY w
JOIN
STL_QUERY q ON w.query = q.query
WHERE
q.endtime IS NULL
AND w.service_class > 4
GROUP BY
w.service_class, w.service_class_name
ORDER BY
running_queries DESC;
6. 테이블 및 디스크 사용량
6.1 테이블 크기 및 통계 (SVV_TABLE_INFO)
-- 데이터베이스 내 대용량 테이블 TOP 20
SELECT
"schema",
"table",
size AS size_mb,
tbl_rows,
estimated_visible_rows,
skew_rows,
ROUND(skew_rows::FLOAT / NULLIF(tbl_rows, 0) * 100, 2) AS skew_pct,
unsorted,
stats_off,
TRIM(diststyle) AS dist_style,
TRIM(sortkey1) AS sort_key
FROM
SVV_TABLE_INFO
WHERE
"schema" NOT IN ('pg_catalog', 'information_schema')
ORDER BY
size DESC
LIMIT 20;
확인 포인트:
- skew_rows: 데이터 분산 불균형
- skew_pct: 20% 이상이면 Distribution Key 재검토 필요
- unsorted: 20% 이상이면 VACUUM 필요
- stats_off: 5% 이상이면 ANALYZE 필요
6.2 데이터 스큐가 심한 테이블
-- 데이터 분산이 불균등한 테이블 찾기
SELECT
"schema",
"table",
size AS size_mb,
tbl_rows,
skew_rows,
ROUND(skew_rows::FLOAT / NULLIF(tbl_rows, 0) * 100, 2) AS skew_pct,
TRIM(diststyle) AS dist_style
FROM
SVV_TABLE_INFO
WHERE
"schema" NOT IN ('pg_catalog', 'information_schema')
AND skew_rows > 20
ORDER BY
skew_pct DESC
LIMIT 20;
6.3 디스크 사용량 상세 (SVV_DISKUSAGE)
-- 특정 테이블의 컬럼별 디스크 사용
SELECT
name AS table_name,
col,
slice,
COUNT(*) AS block_count,
SUM(size) / 1024.0 / 1024.0 AS total_mb
FROM
SVV_DISKUSAGE
WHERE
name = 'your_table_name'
GROUP BY
name, col, slice
ORDER BY
slice, col;
7. 쿼리 성능 분석
7.1 쿼리 실행 계획 (STL_EXPLAIN)
-- 특정 쿼리의 실행 계획 확인
SELECT
query,
nodeid,
parentid,
LEFT(plannode, 80) AS plan_node,
LEFT(info, 80) AS info
FROM
STL_EXPLAIN
WHERE
query = 12345 -- 실제 query ID로 대체
ORDER BY
nodeid;
7.2 쿼리 세그먼트별 성능 (SVL_QUERY_SUMMARY)
-- 특정 쿼리의 단계별 실행 시간
SELECT
query,
stm AS stream,
seg AS segment,
step,
maxtime / 1000000 AS max_time_sec,
avgtime / 1000000 AS avg_time_sec,
rows,
bytes / 1024.0 / 1024.0 AS mb_processed,
TRIM(label) AS step_label,
is_diskbased,
is_rrscan,
is_delayed_scan
FROM
SVL_QUERY_SUMMARY
WHERE
query = 12345 -- 실제 query ID로 대체
ORDER BY
stm, seg, step;
주요 확인 사항:
- DS_DIST_* 라벨: 데이터 재분산 발생 (비효율)
- DS_BCAST_* 라벨: 브로드캐스트 조인 (작은 테이블은 OK)
- is_diskbased = 't': 디스크 기반 작업 (메모리 부족)
7.3 디스크 기반 작업 확인
-- 메모리 부족으로 디스크를 사용한 쿼리 단계
SELECT
query,
segment,
step,
TRIM(label) AS operation,
rows,
bytes / 1024.0 / 1024.0 AS mb_processed,
workmem / 1024.0 / 1024.0 AS workmem_mb,
maxtime / 1000000 AS max_time_sec
FROM
SVL_QUERY_SUMMARY
WHERE
query = 12345
AND is_diskbased = 't'
ORDER BY
segment, step;
8. VACUUM 및 ANALYZE 관리
8.1 VACUUM 실행 이력
-- 최근 VACUUM 작업 확인
SELECT
xid,
table_id,
TRIM(status) AS status,
rows,
sortedrows,
blocks,
eventtime,
reclaimable_rows,
reclaimable_space_mb
FROM
STL_VACUUM
WHERE
eventtime >= DATEADD(day, -7, GETDATE())
AND status NOT LIKE 'VacuumBG%' -- 백그라운드 제외
ORDER BY
eventtime DESC;
8.2 VACUUM이 필요한 테이블 식별
-- Unsorted 비율이 높은 테이블
SELECT
"schema",
"table",
unsorted AS unsorted_pct,
size AS size_mb,
tbl_rows,
TRIM(sortkey1) AS sort_key
FROM
SVV_TABLE_INFO
WHERE
unsorted > 20 -- 20% 이상 unsorted
AND "schema" NOT IN ('pg_catalog', 'information_schema')
ORDER BY
unsorted DESC, size DESC
LIMIT 20;
8.3 테이블별 마지막 VACUUM 시간
-- 각 테이블의 최근 VACUUM 작업
SELECT
v.table_id,
TRIM(t.name) AS table_name,
MAX(v.eventtime) AS last_vacuum_time,
DATEDIFF(day, MAX(v.eventtime), GETDATE()) AS days_since_vacuum
FROM
STL_VACUUM v
JOIN
STV_TBL_PERM t ON v.table_id = t.id
WHERE
v.status = 'Finished'
GROUP BY
v.table_id, t.name
ORDER BY
days_since_vacuum DESC;
8.4 ANALYZE 통계 업데이트
-- 특정 테이블 ANALYZE
ANALYZE your_schema.your_table;
-- 전체 데이터베이스 ANALYZE
ANALYZE;
8.5 ANALYZE 실행 이력
-- 최근 ANALYZE 작업 확인
SELECT
xid,
table_id,
schema_name,
table_name,
TRIM(status) AS status,
rows,
modified_rows,
is_auto,
starttime,
endtime,
DATEDIFF(second, starttime, endtime) AS duration_sec
FROM
STL_ANALYZE
WHERE
starttime >= DATEADD(day, -7, GETDATE())
ORDER BY
starttime DESC;
8.6 ANALYZE가 필요한 테이블
-- 통계가 오래된 테이블
SELECT
"schema",
"table",
stats_off,
size AS size_mb,
tbl_rows
FROM
SVV_TABLE_INFO
WHERE
stats_off > 5 -- 통계가 5% 이상 부실
AND "schema" NOT IN ('pg_catalog', 'information_schema')
ORDER BY
stats_off DESC
LIMIT 20;
9. 에러 및 경고 모니터링
9.1 쿼리 에러 확인
-- 최근 발생한 시스템 에러
SELECT
userid,
pid,
recordtime,
errcode,
LEFT(file, 50) AS file_name,
linenum,
LEFT(context, 80) AS context,
LEFT(error, 200) AS error_message
FROM
STL_ERROR
WHERE
recordtime >= DATEADD(day, -1, GETDATE())
ORDER BY
recordtime DESC
LIMIT 50;
9.2 COPY 작업 에러
-- COPY 명령어 실패 상세
SELECT
query,
starttime,
filename,
line_number,
colname,
type,
col_length,
err_code,
TRIM(err_reason) AS error_reason,
LEFT(raw_line, 100) AS raw_data
FROM
STL_LOAD_ERRORS
WHERE
starttime >= DATEADD(day, -7, GETDATE())
ORDER BY
starttime DESC
LIMIT 20;
9.3 특정 테이블의 COPY 에러
-- 특정 테이블 로딩 에러 히스토리
SELECT
le.query,
le.starttime,
le.filename,
le.line_number,
le.colname,
TRIM(le.err_reason) AS error_reason,
LEFT(q.querytxt, 100) AS copy_command
FROM
STL_LOAD_ERRORS le
JOIN
STL_QUERY q ON le.query = q.query
WHERE
le.starttime >= DATEADD(day, -30, GETDATE())
AND LOWER(q.querytxt) LIKE '%your_table_name%'
ORDER BY
le.starttime DESC;
10. 케이스별 시나리오
케이스 1: 테이블에 INSERT가 안 될 때
Step 1. 해당 테이블의 락 확인
SELECT
l.table_id,
TRIM(t.name) AS table_name,
l.lock_owner_pid AS blocking_pid,
l.lock_status,
s.user_name,
s.starttime,
DATEDIFF(minute, s.starttime, GETDATE()) AS duration_min
FROM
STV_LOCKS l
JOIN
STV_TBL_PERM t ON l.table_id = t.id
JOIN
STV_SESSIONS s ON l.lock_owner_pid = s.process
WHERE
LOWER(t.name) LIKE '%your_table_name%'
AND l.lock_status = 'Holding';
Step 2. 블로킹 세션의 현재 쿼리 확인
SELECT
q.query,
q.pid,
q.starttime,
DATEDIFF(second, q.starttime, GETDATE()) AS run_time_sec,
LEFT(q.querytxt, 150) AS query_text
FROM
STL_QUERY q
WHERE
q.pid = 12345 -- Step 1에서 확인한 blocking_pid
AND q.endtime IS NULL;
Step 3. 필요시 세션 강제 종료
SELECT pg_terminate_backend(12345); -- blocking_pid
케이스 2: 갑자기 모든 쿼리가 느려질 때
Step 1. WLM 큐 대기 시간 확인
SELECT
query,
userid,
service_class,
total_queue_time / 1000000 AS queue_sec,
total_exec_time / 1000000 AS exec_sec,
LEFT(query_text, 80) AS query_preview
FROM
STL_WLM_QUERY
WHERE
queue_start_time >= DATEADD(minute, -30, GETDATE())
ORDER BY
total_queue_time DESC
LIMIT 20;
Step 2. 동시 실행 쿼리 수 확인
SELECT COUNT(*) AS concurrent_queries
FROM STV_RECENTS
WHERE status = 'Running';
Step 3. 리소스 집약적 쿼리 식별
SELECT
userid,
user_name,
db_name,
DATEDIFF(second, starttime, GETDATE()) AS run_time_sec,
LEFT(query, 100) AS query_preview,
pid
FROM
STV_RECENTS
WHERE
status = 'Running'
ORDER BY
run_time_sec DESC;
시나리오 3: DELETE 작업이 너무 오래 걸릴 때
확인:
-- DELETE 진행 중인 쿼리 확인
SELECT
query,
userid,
starttime,
DATEDIFF(second, starttime, GETDATE()) AS running_sec,
LEFT(querytxt, 100) AS query_preview
FROM
STL_QUERY
WHERE
endtime IS NULL
AND LOWER(querytxt) LIKE '%delete%'
ORDER BY
starttime ASC;
- Redshift는 행 기반 DELETE가 비효율적
- 대량 삭제는 CREATE TABLE AS SELECT (CTAS) 방식으로 대체 권장
- 또는 파티션 단위 DROP 고려
11. 튜닝 체크리스트
| 항목 | 확인 쿼리 | 임계값 | 조치 사항 | 기대 효과 |
| Distribution Key | SVV_TABLE_INFO의 skew_rows | > 20% | DISTKEY 재설정 | 데이터 균등 분산 |
| Sort Key | SVV_TABLE_INFO의 unsorted | > 20% | VACUUM 실행 | 스캔 범위 축소 |
| 압축 인코딩 | SVV_TABLE_INFO의 encoded | 'N' | ANALYZE COMPRESSION | 스토리지 절감 |
| VACUUM 주기 | STL_VACUUM 확인 | 주 1회 이상 | 자동화 스케줄 | 정렬 상태 유지 |
| ANALYZE 통계 | SVV_TABLE_INFO의 stats_off | > 5% | ANALYZE 실행 | 쿼리 플랜 최적화 |
| WLM 큐 대기 | STL_WLM_QUERY의 total_queue_time | > 10초 | Auto WLM 활성화 | 동시성 개선 |
| 락 발생 | STV_LOCKS 확인 | 5분 이상 | 장시간 세션 종료 | 블로킹 제거 |
Redshift는 PostgreSQL 기반이지만, 시스템 테이블과 뷰 구조가 완전히 다르며, 특히 프로비저닝 클러스터 환경에서는 STL/STV/SVL 뷰를 적극 활용해야 합니다.
특히 트랜잭션 락 관련해서는 STV_LOCKS와 STV_SESSIONS, STV_TBL_PERM을 조합해서 실시간으로 추적하는 것이 핵심입니다. 일반 RDBMS처럼 단일 뷰로 모든 정보를 볼 수 없기 때문에, 여러 시스템 뷰를 조인해서 사용해야 합니다.
이 글에서 소개한 쿼리들은 모두 AWS 공식 문서에 명시된 실제 시스템 뷰와 정확한 컬럼명을 기반으로 작성되었으므로, 프로비저닝 클러스터 환경에서 바로 사용할 수 있습니다.
참고 자료
'Develop > DATA Engineering' 카테고리의 다른 글
| Apache Iceberg란 무엇인가? 데이터 레이크하우스의 핵심 기술 (0) | 2025.11.28 |
|---|---|
| GitOps로 Kafka Connect 커넥터를 체계적으로 관리하기 (0) | 2025.11.01 |
| [MSK+Kafka Connect] 실시간 CDC 파이프라인 튜닝 & DB 로그 메커니즘 (0) | 2025.09.30 |
| [MSK+Kafka Connect] 트리거 없는 실시간 DB 동기화 구현하기 (0) | 2025.08.31 |
| 대용량 데이터를 전송하는 방법(1) - Message Queue (0) | 2024.01.07 |