쿼리 실행 계획의 기본 개념: cost, rows, width
SQL 쿼리를 작성하면, PostgreSQL이 바로 실행하지 않아. 먼저 자기 "두뇌" — 쿼리 옵티마이저를 돌려서 실행 계획을 만들어. 이 계획은 지도에서 경로를 짜는 것과 비슷해: PostgreSQL이 데이터를 얻기 위해 어떤 작업을 어떤 순서로 할지 계산하는 거지.
쿼리 옵티마이저는 쿼리를 실행할 수 있는 모든 경로를 평가해: 테이블을 순차적으로 스캔할지, 인덱스를 쓸지, 필터링이나 정렬을 어떻게 할지 등등. 그리고 가장 "저렴한"(리소스 기준) 방법을 찾으려고 해. 즉, 실행 시간과 서버 리소스 사이에서 적당한 타협점을 찾는 거지.
실행 계획의 주요 파라미터
자, 이제 본격적으로 PostgreSQL이 EXPLAIN 명령 결과에서 보여주는 파라미터들을 파헤쳐보자. 먼저 간단한 예시부터 볼게:
EXPLAIN
SELECT * FROM students WHERE age > 20;
결과는 대충 이렇게 나올 거야:
Seq Scan on students (cost=0.00..35.00 rows=7 width=72)
Filter: (age > 20)
이제 이 수상한 단어들과 숫자들을 해부해보자.
1. cost (실행 비용)
cost — 쿼리 실행에 필요한 리소스의 추정치야. 이 파라미터는 두 부분으로 나뉘어:
- Startup Cost: 작업을 시작할 때 드는 비용(예: 인덱스 준비 등).
- Total Cost: 전체 작업을 끝내는 데 드는 총 비용.
예시:
cost=0.00..35.00
0.00— Startup Cost야.35.00— Total Cost야.
cost 값이 낮을수록 PostgreSQL은 그 계획을 더 선호해. 하지만 cost는 상대적인 값이야. 초나 밀리초 단위가 아니라, PostgreSQL 내부의 평가 기준이야.
2. rows (예상 행 개수)
rows는 PostgreSQL이 이 단계에서 반환하거나 처리할 것으로 예상하는 행의 개수를 보여줘. 우리 예시에서는:
rows=7
이건 PostgreSQL이 age > 20 필터가 7개의 행을 반환할 거라고 예측했다는 뜻이야. 이 데이터는 PostgreSQL이 테이블에 대해 수집한 통계에서 가져와. 만약 통계가 오래됐으면, 예측이 틀릴 수도 있어. 그러면 비효율적인 계획이 나올 수도 있지.
3. width (행의 바이트 크기)
width — 이 단계에서 반환되는 각 행의 평균 크기(바이트 단위)야. 예시에서는:
width=72
즉, 반환되는 각 행이 평균적으로 72바이트를 차지한다는 뜻이야. width는 컬럼 데이터 크기와 행 식별자나 기타 메타데이터 등 부가적인 오버헤드까지 포함해.
이건 앱 다운로드랑 비슷해. "무게"(width에 해당)가 크면, 인터넷이 빨라도(cost에 해당) 다운로드에 시간이 더 걸릴 수 있지.
실행 계획 해석 예시
실제 예시를 보자. students라는 테이블이 있다고 해보자:
CREATE TABLE students (
id SERIAL PRIMARY KEY,
name VARCHAR(100),
age INTEGER,
major VARCHAR(50)
);
그리고 다음 쿼리를 실행한다고 하자:
EXPLAIN
SELECT * FROM students WHERE age > 20 AND major = 'CS';
결과는 이렇게 나올 수 있어:
Seq Scan on students (cost=0.00..42.50 rows=3 width=164)
Filter: ((age > 20) AND (major = 'CS'))
- Seq Scan: PostgreSQL이
students테이블을 순차적으로 스캔해. 즉, 모든 행을 하나씩 훑는다는 뜻이야. - cost=0.00..42.50: 작업 실행 비용.
Startup Cost는0.00, 전체 비용은42.50이야. - rows=3:
age > 20 AND major = 'CS'필터가 3개의 행을 반환할 거라고 예상해. - width=164: 각 행이 평균적으로 164바이트를 차지해.
이제 PostgreSQL이 어떻게 결정을 내리는지, 그리고 쿼리의 약점을 어떻게 찾을 수 있는지 알겠지? 예를 들어, cost가 높으면 쿼리가 무거울 수 있다는 신호야. rows가 많으면, 필터 조건을 다시 생각해볼 필요가 있어.
cost가 실제로 어떻게 동작할까?
age 컬럼에 인덱스를 추가해보자:
CREATE INDEX idx_age ON students(age);
이제 쿼리를 다시 실행해보면:
EXPLAIN
SELECT * FROM students WHERE age > 20 AND major = 'CS';
결과가 이렇게 바뀔 수 있어:
Bitmap Heap Scan on students (cost=4.37..20.50 rows=3 width=164)
Recheck Cond: (age > 20)
Filter: (major = 'CS')
-> Bitmap Index Scan on idx_age (cost=0.00..4.37 rows=20 width=0)
Index Cond: (age > 20)
뭐가 달라졌을까?
Seq Scan대신Bitmap Heap Scan이 사용됐어: PostgreSQL이 먼저idx_age인덱스에서 조건에 맞는 행을 찾고, 그 다음 테이블에서 데이터를 가져와.cost가 확 줄었지:Startup Cost는4.37,Total Cost는20.50이야.- 인덱스 덕분에 작업이 훨씬 효율적이 됐어.
시각화: Seq Scan과 Index Scan의 차이
비교가 더 쉬우라고 작은 표를 만들어봤어:
| 작업 | 설명 | 예시 |
|---|---|---|
| Seq Scan | 테이블 전체를 읽음 | 모든 행을 다 훑음 |
| Index Scan | 인덱스를 사용함 | 인덱스를 통해 빠르게 행을 찾음 |
주의할 점과 흔한 실수
실행 계획 파라미터를 쓸 때는 몇 가지 함정이 있어. 예를 들어, cost가 낮다고 항상 실행이 더 빠른 건 아니야. 데이터베이스 통계가 오래됐으면(예: 테이블을 대량으로 수정한 후), 계획이 정확하지 않을 수 있어. ANALYZE 명령으로 통계를 갱신해줘. 자세한 건 다음 강의에서 다룰게.
인덱스는 꼭 필요한 곳에만 쓰는 게 좋아. 너무 많이 만들면 공간도 차지하고, 쓰기 작업이 느려질 수 있어.
GO TO FULL VERSION