실행 계획 읽기
예상 계획과 실제 계획을 보고, 스캔과 탐색이 어떻게 다른지 봅니다.
50만 행이 필요합니다
4부까지는 507행짜리 표로 충분했습니다. 읽은 페이지가 2 와 14 로 갈리는 것만 보면 되었기 때문입니다. 5부는 다릅니다. 계획이 왜 그렇게 잡히는지, 무엇을 고치면 달라지는지는 규모가 있어야 숫자로 드러납니다.
lab-bigdata.sql 을 받아
실행하십시오. Board.POSTS 는 그대로 두고
같은 모양의 Board.POSTS_BIG 을 50만 행으로
만듭니다. 앞 단원들의 예제 결과는 그대로 유지됩니다.
-- lab-setup.sql 로 MssqlLab 을 만든 뒤에 실행합니다. -- 만드는 데 몇 초 걸립니다.
| 행수 | 공지 | 회원7의글 | 인덱스제목 |
|---|---|---|---|
| 500,000 | 166,666 | 25,000 | 62,500 |
| 인덱스 | 페이지 | MB |
|---|---|---|
| PK_POSTS_BIG | 7,305 | 57 |
| IX_POSTS_BIG_list | 4,519 | 35 |
| IX_POSTS_BIG_user | 866 | 6 |
엔진이 정한 방법을 봅니다
SQL 은 무엇을 원하는지만 적습니다. 어떻게 가져올지는 엔진이 정합니다 — 어느 인덱스를 쓸지, 어떤 순서로 조인할지, 정렬을 어디서 할지. 그 결정을 적어 둔 것이 실행 계획입니다.
계획은 두 가지로 봅니다.
| 예상 계획 | 실제 계획 | |
|---|---|---|
| 실행 | 하지 않습니다 | 합니다 |
| 행 수 | 통계로 어림한 값 | 어림값 + 실제로 나온 값 |
| 문장으로 | SET SHOWPLAN_TEXT ON | SET STATISTICS PROFILE ON |
| SSMS 에서 | Ctrl+L | Ctrl+M 뒤 실행 |
둘의 차이가 곧 진단입니다. 어림값과 실제가 크게 어긋나면 엔진이 잘못된 전제로 계획을 골랐다는 뜻이고, 그때부터 느려집니다.
SSMS 를 사용한다면 그래픽 계획을 보는 편이 훨씬 낫습니다. 이 단원이 문장으로 보이는 것은 화면에 옮길 수 있기 때문이고, 실제로 진단할 때는 아이콘 위에 마우스를 올려 예상·실제 행 수를 보는 쪽이 빠릅니다.
다섯 문장을 나란히 놓습니다
같은 표에서 조금씩 다른 다섯 문장을 실행하고 계획과 읽은 페이지를 함께 봅니다.
SET STATISTICS IO ON; SELECT num FROM Board.POSTS_BIG WHERE num = 250000; -- (가) SELECT num FROM Board.POSTS_BIG WHERE content = N'없는본문'; -- (나) SELECT TOP (20) num, title FROM Board.POSTS_BIG WHERE board_num = 2 ORDER BY group_num DESC, sort_no; -- (다) SELECT COUNT(*) FROM Board.POSTS_BIG WHERE user_num = 7; -- (라) SELECT title FROM Board.POSTS_BIG WHERE user_num = 7; -- (마)
| 문장 | 논리적 읽기 | 계획의 핵심 |
|---|---|---|
| (가) 클러스터형 키로 한 건 | 3 | Clustered Index Seek |
| (나) 인덱스가 없는 열로 | 7,333 | Clustered Index Scan + Filter |
| (다) 목록 20건 | 3 | Top + Index Seek |
| (라) 회원 한 명의 글 세기 | 47 | Index Seek |
| (마) 회원 한 명의 글 제목 | 4,535 | Index Scan |
계획은 오른쪽 아래에서 왼쪽 위로 읽습니다
문장으로 보면 가장 깊이 들여쓴 것이 먼저 실행됩니다. 거기서 나온 행이 위로 올라가면서 걸러지고 묶입니다.
SET SHOWPLAN_TEXT ON; GO SELECT num FROM Board.POSTS_BIG WHERE content = N'없는본문'; GO SELECT TOP (20) num, title FROM Board.POSTS_BIG WHERE board_num = 2 ORDER BY group_num DESC, sort_no; GO SET SHOWPLAN_TEXT OFF;
가장 아래에 있는 것이 몇 행을 내보내는가가 그 문장의 값입니다. (나)는 아래에서 50만 행이 올라오고 위에서 0행이 남습니다. 49만 9,999행을 읽어 버린 셈입니다. (다)는 아래에서 20행만 올라옵니다.
자주 보게 되는 연산자
| 연산자 | 무엇을 합니까 | 보이면 |
|---|---|---|
| Index Seek | 정렬을 따라 그 자리로 들어갑니다 | 좋습니다 |
| Index Scan | 인덱스를 처음부터 끝까지 읽습니다 | 범위가 넓으면 정상입니다 |
| Clustered Index Scan | 표를 처음부터 끝까지 읽습니다 | 조건이 있는데 보이면 살펴봅니다 |
| Key Lookup | 인덱스에 없는 열을 표에서 가져옵니다 | 건수가 많으면 문제입니다(3.5) |
| Filter | 올라온 행을 걸러 냅니다 | 아래에서 너무 많이 올라온 것입니다 |
| Sort | 정렬합니다 | 인덱스 순서와 다르다는 뜻입니다 |
| Nested Loops | 바깥 행마다 안쪽을 찾습니다 | 한쪽이 작을 때 어울립니다 |
| Hash Match | 해시 표를 만들어 맞춥니다 | 양쪽이 클 때 어울립니다 |
연산자 이름을 외우는 것보다 "아래에서 몇 행이 올라오는가" 를 보는 것이 먼저입니다. 느린 문장은 대개 필요 없는 행을 너무 많이 읽어 올린 뒤 위에서 버리는 모양을 하고 있습니다.
예상과 실제를 견줍니다
엔진은 몇 행이 나올지 어림한 값을 보고 계획을 고릅니다. 그 어림이 맞으면 계획도 대개 맞습니다. 어긋나면 그때부터 느려집니다.
SET STATISTICS PROFILE ON 은 계획의 단계마다
어림값과 실제로 나온 행 수를 함께 냅니다.
SET STATISTICS PROFILE ON; -- 2026년 12월 이후에 올라온 글을 셉니다. SELECT COUNT(*) FROM Board.POSTS_BIG WHERE reg_date >= '2026-12-01'; SET STATISTICS PROFILE OFF;
| Rows | EstimateRows | 연산자 |
|---|---|---|
| 19,041 | 20,893.5 | Index Scan(IX_POSTS_BIG_list) |
같은 문장을 지역 변수로 바꾸면 어림이 무너집니다. 4.1 에서 본 변수가 여기서 다시 나옵니다.
SET STATISTICS PROFILE ON; DECLARE @d datetime2(0) = '2026-12-01'; SELECT COUNT(*) FROM Board.POSTS_BIG WHERE reg_date >= @d; SET STATISTICS PROFILE OFF;
| 적은 방법 | 실제 | 어림 | 차이 |
|---|---|---|---|
| 값을 그대로 | 19,041 | 20,893.5 | 가깝습니다 |
| 지역 변수로 | 19,041 | 150,000 | 여덟 배 부풀렸습니다 |
계획을 세우는 시점에 변수 안의 값을 모르기 때문입니다.
문장이 컴파일될 때 @d 에는 아직 아무것도 들어
있지 않습니다. 그래서 통계를 찾아보지 못하고 "부등호 조건은 대개
30%" 라는 고정 비율을 씁니다.
이 문장에서는 다행히 계획이 같았습니다. 그러나 어림이 여덟 배 어긋나면 조인 방식이나 정렬 위치가 잘못 잡히는 일이 흔합니다. 왜 어긋나는지와 무엇으로 고치는지는 5.3 에서 다룹니다.
어림과 실제를 견주는 것이 진단의 첫걸음입니다. 느린 문장을 만나면 계획을 열고 어느 단계에서 둘이 벌어지는지부터 찾으십시오. 대개 그 아래가 원인입니다.
무엇을 먼저 봅니까
- 가장 아래에서 몇 행이 올라오는가. 결과가 20행인데 아래에서 50만 행이 올라온다면 그것이 문제입니다.
- 어림과 실제가 벌어지는 단계. 거기가 잘못된 전제가 들어간 자리입니다.
- Scan 이 있는데 조건도 있는가. 조건이 있는데 표를 다 읽고 있다면 인덱스가 없거나 사용하지 못하는 모양입니다(3.5 · 5.4).
- Key Lookup 이 몇 번 도는가. 3.5 에서 본 대로 건수가 많아지면 스캔보다 비싸집니다.
- Sort 가 있는가. 인덱스 순서로 낼 수 있으면 사라집니다
(3.5 의
IX_POSTS_list).
비용 백분율은 참고만 하십시오. 계획에 붙는 백분율은 실제
시간이 아니라 어림값으로 계산한 비용의 비율입니다. 어림이
틀렸다면 그 비율도 틀립니다. 실제로 오래 걸린 자리를 찾으려면 실제 행 수와
STATISTICS IO 를 보는 편이 확실합니다.
직접 해보기
(나) 문장은 결과가 한 건도 없는데 7,333장을 읽었습니다. 계획을 열어 까닭을 설명하고, 읽기를 줄일 방법이 있는지 말해 보세요.
(라)와 (마)는 WHERE user_num = 7 로 같은데
읽기가 47장과 4,535장입니다. 계획을 견주어 까닭을 찾고,
(마)를 줄이는 방법을 적어 보세요.
Board.POSTS_BIG 은 5부 내내 사용하므로
지금은 두십시오. 5.8 을 마친 뒤
DROP TABLE Board.POSTS_BIG; 으로 지웁니다.
- 실행 계획은 엔진이 정한 실행 방법입니다. 예상 계획은
SET SHOWPLAN_TEXT ON, 실제 계획은SET STATISTICS PROFILE ON으로 봅니다. - 문장으로 보면 가장 깊이 들여쓴 것이 먼저 실행되고 위로 올라갑니다.
- 가장 아래에서 몇 행이 올라오는지를 먼저 보십시오. 결과가 20행인데 50만 행이 올라온다면 그것이 원인입니다.
- 50만 행에서 3장과 7,333장이 갈립니다. 조건에 사용한 열에 인덱스가 있느냐 없느냐입니다.
- 어림과 실제가 벌어지는 단계가 진단의 출발점입니다. 지역 변수를 조건에 사용하면 어림이 20,893.5 에서 150,000 으로 무너집니다 — 컴파일 시점에 값을 모르기 때문입니다(5.3).
- 계획의 비용 백분율은 어림값으로 계산한 것입니다. 어림이 틀리면 그 비율도 틀립니다.
- 같은 조건이라도 무엇을 가져오느냐에 따라 계획이 갈립니다(47장과 4,535장). 3.5 의 커버링이 여기서 다시 나옵니다.