오늘은 PostgreSQL 실행 계획의 노드가 뭔지, 그걸 어떻게 읽는지, 그리고 제일 중요한 건 뭔가 잘못됐을 때 그걸 어떻게 알아차리는지 알아볼 거야. 왜 가끔 데이터베이스가 이미 인덱스가 있는데도 리소스를 많이 쓰는 Seq Scan을 쓰는지, 그 이유도 알 수 있을 거야.
PostgreSQL이 쿼리 실행 계획을 만들 때, 그걸 여러 단계로 쪼개는데, 이걸 노드라고 불러. 각 노드는 데이터베이스 서버가 네 쿼리를 처리할 때 거치는 "단계" 같은 거야. 주요 노드 타입은 다음과 같아:
Sequential Scan (Seq Scan)
Seq Scan 또는 순차 스캔은 테이블에서 데이터를 뽑아내는 제일 단순한 방법이야. PostgreSQL은 테이블을 그냥 한 줄씩 다 읽으면서 네 쿼리 조건에 맞는지 확인해.
Seq Scan이 언제 쓰일까?
Seq Scan은 이런 경우에 써:
- 테이블에 쿼리를 빠르게 해줄 만한 인덱스가 없을 때.
- 필터 조건이 너무 넓어서 인덱스가 별로 쓸모없을 때(예를 들어, 전체 데이터의 50% 이상을 뽑아야 할 때).
- PostgreSQL이 인덱스 쓰는 것보다 그냥 테이블을 쭉 읽는 게 더 빠르다고 판단할 때(진짜 작은 테이블에서 가끔 이럴 수 있어).
EXPLAIN SELECT * FROM students WHERE age > 18;
결과 예시:
Seq Scan on students (cost=0.00..35.50 rows=10 width=50)
Filter: (age > 18)
Seq Scan on students를 보면, PostgreSQL이 "students" 테이블을 전부 읽겠다는 뜻이야.
Seq Scan의 문제점: 테이블이 엄청 크면, 순차 스캔은 진짜 오래 걸릴 수 있어.
Index Scan
Index Scan은 인덱스를 이용해서 데이터를 읽는 거야. PostgreSQL에서 인덱스를 만들면, 테이블의 "목차" 같은 게 생기는 거지. 쿼리가 인덱스를 쓸 수 있으면, PostgreSQL은 테이블 전체를 읽지 않고 필요한 부분만 빠르게 찾아가.
Index Scan이 언제 쓰일까?
- 쿼리에서 인덱스가 걸린 컬럼에 필터 조건(
WHERE등)이 있을 때. =,<,>,BETWEEN같은 비교 연산을 쓸 때.
CREATE INDEX idx_students_age ON students(age);
EXPLAIN SELECT * FROM students WHERE age = 18;
결과 예시:
Index Scan using idx_students_age on students (cost=0.15..8.27 rows=1 width=50)
Index Cond: (age = 18)
여기서 Index Scan using idx_students_age는 PostgreSQL이 idx_students_age 인덱스를 쓴다는 뜻이야. 테이블을 한 줄씩 읽는 게 아니라, 인덱스를 통해 훨씬 빠르게 접근하지.
Index Scan의 장점:
- 큰 테이블에서 쿼리 속도가 확 빨라져.
- 디스크에서 읽는 데이터 양이 줄어들어.
Index Scan의 문제점:
쿼리 결과가 너무 많으면(예를 들어, 테이블의 절반 이상), 인덱스를 쓰는 게 오히려 Seq Scan보다 느릴 수도 있어.
Hash Join
Hash Join은 두 테이블을 조인할 때 써(예: ON students.course_id = courses.id). PostgreSQL은 두 테이블 중 더 작은 쪽으로 해시 테이블을 만들고, 그걸로 다른 테이블에서 매칭되는 값을 찾아.
Hash Join이 언제 쓰일까?
INNER JOIN,LEFT JOIN등으로 테이블을 조인할 때.- PostgreSQL이
Hash Join이 다른 조인 방식보다 더 효율적이라고 판단할 때.
EXPLAIN
SELECT *
FROM students
JOIN courses ON students.course_id = courses.id;
결과 예시:
Hash Join (cost=25.00..50.00 rows=10 width=100)
Hash Cond: (students.course_id = courses.id)
-> Seq Scan on students (cost=0.00..20.00 rows=10 width=50)
-> Hash (cost=15.00..15.00 rows=10 width=50)
-> Seq Scan on courses (cost=0.00..15.00 rows=10 width=50)
여기서 Hash Join이 두 테이블을 조인해. PostgreSQL이 먼저 두 테이블 모두 Seq Scan을 하고, 그 다음에 해시 테이블(Hash)을 만들어.
Hash Join의 장점:
- 중간 크기 테이블에서 빠르게 처리할 수 있어.
- 행이 많은 테이블 조인에도 효율적이야.
Hash Join의 문제점:
해시 테이블 크기가 메모리보다 크면, PostgreSQL이 디스크를 써야 해서 조인이 엄청 느려질 수 있어.
실행 계획 분석 예시
실제 예시로 한 번 분석해보자.
쿼리:
EXPLAIN ANALYZE
SELECT *
FROM students
JOIN courses ON students.course_id = courses.id
WHERE students.age > 18;
결과:
Hash Join (cost=35.00..75.00 rows=5 width=100) (actual time=1.00..2.50 rows=5 loops=1)
Hash Cond: (students.course_id = courses.id)
-> Seq Scan on students (cost=0.00..40.00 rows=10 width=50) (actual time=0.50..1.00 rows=7 loops=1)
Filter: (age > 18)
Rows Removed by Filter: 3
-> Hash (cost=25.00..25.00 rows=5 width=50) (actual time=0.30..0.30 rows=5 loops=1)
-> Seq Scan on courses (cost=0.00..20.00 rows=5 width=50) (actual time=0.20..0.25 rows=5 loops=1)
Planning Time: 0.50 ms
Execution Time: 3.00 ms
해석:
Hash Join: 메인 노드야. PostgreSQL이students랑courses테이블을 조인해.actual time: 1.00~2.50ms.rows=5: 쿼리 결과로 5행이 나왔어.
- 안에 들어있는 노드들:
Seq Scan on students:students테이블을 순차적으로 읽으면서(age > 18)필터를 적용해.Rows Removed by Filter = 3: 조건에 안 맞아서 걸러진 행이 3개야.Hash: PostgreSQL이courses테이블로 해시 테이블을 만들어.
노드 비교와 선택
실행 계획을 분석할 때, PostgreSQL이 왜 특정 데이터 처리 방식을 골랐는지 이해하는 게 핵심이야. 가끔은 네가 직접 개입해서 인덱스를 추가하거나 쿼리를 고쳐야 할 수도 있어. 몇 가지 팁을 줄게:
- 큰 테이블에서
Seq Scan이 보이면, 인덱스를 고민해봐. Hash Join이 너무 느리면, PostgreSQL에 할당된 메모리를 확인해봐.EXPLAIN ANALYZE로 예상 값이랑 실제 값(rows,time등)을 비교해봐.
이제 쿼리 실행 계획을 읽고, 노드를 해석하는 기본은 잡았어. 다음 강의에서는 자주 나오는 최적화 문제랑 그 해결법을 다룰 거야.
GO TO FULL VERSION