📌 업무 배경
- 공통된 컬럼을 사용하는 12개의 업무 테이블이 존재
- 테이블: APP_A_MST ~ APP_L_MST
- 공통컬럼: doc_id, biz_year, biz_quarter, biz_round
- 각 업무 결재가 완료되면, 다른 업무들의 상태가 결재완료인지 조회, 다음 과정을 수행하는데 필요한 데이터를 생성해야 함
- 결재가 완료시 결재문서의 doc_id가 반환됨
- 특정 문서(doc_id)가 어느 테이블에 속해 있는지 모름
- 해당 문서가 속한 연도 / 분기 / 차수를 기준으로
👉 모든 업무 테이블에서 결재 완료 상태인지 검증 - APP_A_MST ~ APP_L_MST 가 아닌, 하나의 테이블을 사용하면 좋겠지만, 그럴 수 없는 상황
1️⃣ 1차 버전 – UNION ALL 기반 전체 스캔
🔧 접근 방식
- Dynamic SQL을 사용하여 12개 테이블을 UNION ALL 로 모두 합침
- 각 테이블별 결재 완료 여부를 Y/N으로 산출
- MIN(passyn) 으로 최종 결과 도출
더보기
DECLARE @doc_id NVARCHAR(40) = :doc_id;
DECLARE @biz_year NVARCHAR(10);
DECLARE @biz_quarter NVARCHAR(10);
DECLARE @biz_round NVARCHAR(10);
DECLARE @i INT = 1;
DECLARE @tbl NVARCHAR(50);
DECLARE @sql NVARCHAR(MAX);
-- 1. doc_id가 속한 테이블에서 기준 값 찾기
WHILE @i <= 12
BEGIN
SET @tbl = 'APP_' + CHAR(64 + @i) + '_MST';
SET @sql = '
SELECT
@by = biz_year,
@bq = biz_quarter,
@br = biz_round
FROM ' + @tbl + '
WHERE doc_id = ''' + @doc_id + '''
';
EXEC sp_executesql
@sql,
N'@by NVARCHAR(10) OUTPUT, @bq NVARCHAR(10) OUTPUT, @br NVARCHAR(10) OUTPUT',
@by = @biz_year OUTPUT,
@bq = @biz_quarter OUTPUT,
@br = @biz_round OUTPUT;
IF (@biz_year IS NOT NULL) BREAK;
SET @i += 1;
END;
-- 2. 결재 완료 여부 전체 검사
DECLARE @unionSql NVARCHAR(MAX) = '';
DECLARE @j INT = 1;
WHILE @j <= 12
BEGIN
SET @tbl = 'APP_' + CHAR(64 + @j) + '_MST';
SET @unionSql += '
SELECT
CASE WHEN COUNT(*) > 0 THEN ''Y'' ELSE ''N'' END AS pass_yn
FROM ' + @tbl + ' T
INNER JOIN DOC_MASTER D ON D.doc_id = T.doc_id
WHERE D.del_yn = ''N''
AND D.approval_status = ''APPROVED''
AND T.biz_year = ''' + @biz_year + '''
AND T.biz_quarter = ''' + @biz_quarter + '''
AND T.biz_round = ''' + @biz_round + '''
';
IF @j < 12 SET @unionSql += ' UNION ALL ';
SET @j += 1;
END;
-- 3. 이관 테이블 중복 체크
SET @unionSql += '
UNION ALL
SELECT
CASE WHEN COUNT(*) = 0 THEN ''Y'' ELSE ''N'' END
FROM MOVE_MST M
INNER JOIN DOC_MASTER D ON D.doc_id = M.doc_id
WHERE D.del_yn = ''N''
AND M.biz_year = ''' + @biz_year + '''
AND M.biz_quarter = ''' + @biz_quarter + '''
AND M.biz_round = ''' + @biz_round + '''
';
-- 4. 최종 결과
EXEC ('
SELECT
''' + @biz_year + ''' AS biz_year,
''' + @biz_quarter + ''' AS biz_quarter,
''' + @biz_round + ''' AS biz_round,
MIN(pass_yn) AS pass_yn
FROM (' + @unionSql + ') X
');
⚠️ 문제점
❌ 불필요한 전체 스캔
- 이미 하나라도 실패가 확정돼도 끝까지 실행됨
❌ 동적 SQL 문자열 직접 결합
- SQL Injection 위험
- 유지보수 난이도 높음
2️⃣ 2차 버전 – EXISTS 논리식으로 개선
🔧 접근 방식
- UNION ALL 제거
- EXISTS + AND 조건으로 논리 평가
더보기
DECLARE @doc_id NVARCHAR(40) = :doc_id;
DECLARE @biz_year NVARCHAR(10);
DECLARE @biz_quarter NVARCHAR(10);
DECLARE @biz_round NVARCHAR(10);
DECLARE @i INT = 1;
DECLARE @tbl NVARCHAR(50);
DECLARE @sql NVARCHAR(MAX);
-- 1. doc_id가 속한 테이블에서 기준 값 찾기
WHILE @i <= 12
BEGIN
SET @tbl = 'APP_' + CHAR(64 + @i) + '_MST';
SET @sql = '
SELECT
@by = biz_year,
@bq = biz_quarter,
@br = biz_round
FROM ' + @tbl + '
WHERE doc_id = ''' + @doc_id + '''
';
EXEC sp_executesql
@sql,
N'@by NVARCHAR(10) OUTPUT, @bq NVARCHAR(10) OUTPUT, @br NVARCHAR(10) OUTPUT',
@by = @biz_year OUTPUT,
@bq = @biz_quarter OUTPUT,
@br = @biz_round OUTPUT;
IF (@biz_year IS NOT NULL) BREAK;
SET @i += 1;
END;
DECLARE @result CHAR(1);
DECLARE @sql NVARCHAR(MAX) = 'SELECT @r = CASE WHEN ';
DECLARE @j INT = 1;
DECLARE @tbl NVARCHAR(50);
WHILE @j <= 12
BEGIN
SET @tbl = 'APP_' + CHAR(64 + @j) + '_MST';
SET @sql += '
EXISTS (
SELECT 1
FROM ' + @tbl + ' T
INNER JOIN DOC_MASTER D ON D.doc_id = T.doc_id
WHERE D.del_yn = ''N''
AND D.approval_status = ''APPROVED''
AND T.biz_year = ''' + @biz_year + '''
AND T.biz_quarter = ''' + @biz_quarter + '''
AND T.biz_round = ''' + @biz_round + '''
)
';
IF @j < 12 SET @sql += ' AND ';
SET @j += 1;
END;
-- 이관 테이블 중복 방지
SET @sql += '
AND NOT EXISTS (
SELECT 1
FROM MOVE_MST M
INNER JOIN DOC_MASTER D ON D.doc_id = M.doc_id
WHERE D.del_yn = ''N''
AND M.biz_year = ''' + @biz_year + '''
AND M.biz_quarter = ''' + @biz_quarter + '''
AND M.biz_round = ''' + @biz_round + '''
)
THEN ''Y'' ELSE ''N'' END
';
EXEC sp_executesql
@sql,
N'@r CHAR(1) OUTPUT',
@r = @result OUTPUT;
SELECT @biz_year, @biz_quarter, @biz_round, @result AS pass_yn;
✅ 개선점
✔️ 불필요한 결과셋 제거
- COUNT, UNION 제거 → 메모리 사용 감소
⚠️ 문제점
❌ 동적 SQL 문자열 직접 결합
- SQL Injection 위험
- 유지보수 난이도 높음
❌ 단락 평가 불가
- SQL 특성상 모든 EXISTS가 평가됨
3️⃣ 3차(최종) – 단락 평가 + 파라미터화 + 조기 종료
🔧 접근 방식
- 하나라도 실패하면 즉시 종료
- SQL Injection 방지
더보기
DECLARE @doc_id NVARCHAR(40) = :doc_id;
DECLARE @biz_year NVARCHAR(10);
DECLARE @biz_quarter NVARCHAR(10);
DECLARE @biz_round NVARCHAR(10);
DECLARE @i INT = 1;
DECLARE @tbl NVARCHAR(50);
DECLARE @sql NVARCHAR(MAX);
-- 1. doc_id가 속한 테이블에서 기준 값 찾기
WHILE @i <= 12
BEGIN
SET @tbl = 'APP_' + CHAR(64 + @i) + '_MST';
SET @sql = '
SELECT
@by = biz_year,
@bq = biz_quarter,
@br = biz_round
FROM ' + @tbl + '
WHERE doc_id = ''' + @doc_id + '''
';
EXEC sp_executesql
@sql,
N'@by NVARCHAR(10) OUTPUT, @bq NVARCHAR(10) OUTPUT, @br NVARCHAR(10) OUTPUT',
@by = @biz_year OUTPUT,
@bq = @biz_quarter OUTPUT,
@br = @biz_round OUTPUT;
IF (@biz_year IS NOT NULL) BREAK;
SET @i += 1;
END;
DECLARE @pass_yn CHAR(1) = 'Y';
DECLARE @j INT = 1;
DECLARE @tbl SYSNAME;
DECLARE @sql NVARCHAR(MAX);
WHILE @j <= 12
BEGIN
SET @tbl = 'APP_' + CHAR(64 + @j) + '_MST';
SET @sql = '
IF NOT EXISTS (
SELECT 1
FROM ' + QUOTENAME(@tbl) + ' T
INNER JOIN DOC_MASTER D ON D.doc_id = T.doc_id
WHERE D.del_yn = ''N''
AND D.approval_status = ''APPROVED''
AND T.biz_year = @y
AND T.biz_quarter = @q
AND T.biz_round = @r
)
SET @p = ''N''
';
EXEC sp_executesql
@sql,
N'@y NVARCHAR(10), @q NVARCHAR(10), @r NVARCHAR(10), @p CHAR(1) OUTPUT',
@y = @biz_year,
@q = @biz_quarter,
@r = @biz_round,
@p = @pass_yn OUTPUT;
IF @pass_yn = 'N' BREAK;
SET @j += 1;
END;
-- 이관 테이블 중복 체크
IF @pass_yn = 'Y'
BEGIN
SET @sql = '
IF EXISTS (
SELECT 1
FROM MOVE_MST M
INNER JOIN DOC_MASTER D ON D.doc_id = M.doc_id
WHERE D.del_yn = ''N''
AND M.biz_year = @y
AND M.biz_quarter = @q
AND M.biz_round = @r
)
SET @p = ''N''
';
EXEC sp_executesql
@sql,
N'@y NVARCHAR(10), @q NVARCHAR(10), @r NVARCHAR(10), @p CHAR(1) OUTPUT',
@y = @biz_year,
@q = @biz_quarter,
@r = @biz_round,
@p = @pass_yn OUTPUT;
END;
SELECT
@biz_year AS biz_year,
@biz_quarter AS biz_quarter,
@biz_round AS biz_round,
@pass_yn AS pass_yn;
🔥 핵심 개선 포인트
① 조기 종료 (Short-circuit Evaluation)
IF NOT EXISTS (...)
SET @pass_yn = 'N'
BREAK;
✔️ 하나라도 결재 미완료 → 즉시 루프 종료
✔️ 최악의 경우 12회 → 평균 2~3회 검사
📉 실행 시간 대폭 감소
② sp_executesql + 파라미터화
AND T.biz_year = @y
AND T.biz_quarter = @q
AND T.biz_round = @r
✔️ SQL Injection 방지
✔️ 실행 계획 재사용 가능
✔️ 문자열 결합 최소화
③ QUOTENAME 사용
FROM + QUOTENAME(@targetTable)
✔️ 테이블명 주입 공격 차단
✔️ 동적 SQL 보안 강화
🧠 실행계획 관점에서 본 최종 쿼리 구조
처리 흐름 요약
- doc_id가 속한 업무 테이블 탐색
- 기준 값(biz_year, biz_quarter, biz_round) 확보
- 12개 업무 테이블을 순차적으로 EXISTS 검증
- 승인 완료 데이터가 없으면 즉시 종료(Short-circuit)
- 이관 테이블 중복 여부 확인
- 최종 Y / N 반환
인덱스 구성 및 실행계획 고려 사항
- DOC_MASTER 테이블은 이미 PK 기반 인덱스가 적용되어 있음
- APP_A_MST ~ APP_L_MST 테이블에는 기존에 (doc_id) 인덱스가 존재
- 본 쿼리에서는 biz_year, biz_quarter, biz_round 조건으로 접근하므로
해당 컬럼 조합에 대한 인덱스를 추가하여 Index Seek 기반 실행계획을 유도함
⭐ 최종 버전의 장점
✅ 성능
- 불필요한 테이블 접근 제거
- 조기 종료로 평균 실행 시간 최소화
✅ 보안
- 파라미터화
- QUOTENAME 적용
- 문자열 직접 결합 최소화
✅ 유지보수성
- 비즈니스 규칙이 명확
- 조건 추가/변경이 쉬움
- Dynamic SQL 치고는 디버깅 포인트 명확
✍️ 마무리
아래는 성능개선표이다.
| 구분 | 1차 | 2차 | 3차 |
| UNION 사용 | O | X | X |
| 결과셋 생성 | O | X | X |
| 조기 종료 | X | X | O |
| 파라미터화 | X | △ | O |
| 평균 테이블 접근 수 | 12 | 12 | 2~3 |
이번 개선을 통해
SQL에서도 “끝까지 계산하지 않아도 되는 구조”가
성능과 안정성에 얼마나 큰 영향을 주는지 체감할 수 있었다.
'CS > SQL' 카테고리의 다른 글
| MSSQL - 행마다 새로운 시퀀스 부여 (0) | 2026.01.28 |
|---|---|
| Index Merge (0) | 2026.01.06 |
| Covering Index (커버링 인덱스) (0) | 2026.01.06 |
| 인덱스 설계 사고방식 2 (0) | 2026.01.06 |
| 인덱스 설계 사고방식 1 (0) | 2026.01.06 |