※ PostgreSQL: Execution Plan Optimization for Bind Variables.
안녕하세요. 듀스트림입니다.
오늘도 재밌는 사례를 하나 가져왔습니다.
고객사에서 오래 걸리는 쿼리에 대해 진단 요청을 받아 방문했습니다.
진단 후, 엔진 및 인덱스 튜닝을 통해 쿼리 수행 시간을 4초에서 0.2초로 단축시켰습니다.
문제는 여기서 끝나지 않았습니다. 다음 날, 고객사로부터 다시 연락이 왔습니다.
"튜닝 후 DB 서버에서 수행할 때는 분명 빠른데, APP에서는 여전히 느립니다."
상황을 듣자마자 떠오르는 게 하나 있어서 질문을 던졌습니다.
"바인드 변수를 사용하고 계신가요?"
예상대로 바인드 변수를 사용하고 있었습니다.
이번 포스팅에서는 바인드 변수를 PostgreSQL 엔진 관점에서 살펴보겠습니다.
1. 바인드 변수란?
바인드 변수는 SQL 쿼리에서 하드코딩된 값 대신 플레이스홀더를 사용하여 실행 시 값이 바인딩되도록 하여 SQL 재사용성과 보안을 강화하는 기법입니다.
2. 바인드 변수의 이점
• 바인드 변수를 사용하면 동일한 SQL 템플릿에 대해 실행 계획을 캐싱하여 성능을 최적화할 수 있습니다.
→ 트랜잭션이 많은 애플리케이션에서 반복적으로 실행되는 SQL 쿼리에 유리합니다.
• SQL 인젝션(SQL Injection) 방어에 효과적입니다.
→ 하드코딩된 값 대신 플레이스홀더를 사용하므로, 사용자 입력 값이 쿼리 구문에 직접 삽입되지 않습니다.
• 바인드 변수는 코드 재사용성과 유지보수성을 높여줍니다.
→ 특정 조건 값만 변경하면서 동일한 쿼리 패턴을 재활용할 수 있습니다.
3. PostgreSQL에서 바인드 변수의 작동 방식
• PREPARE/EXECUTE 사용
→ PostgreSQL은 바인드 변수와 함께 동적 SQL을 지원합니다.
PREPARE stmt (integer, text) AS -- PREPARE는 실행 계획을 생성하고 캐싱합니다.
SELECT * FROM users WHERE id = $1 AND status = $2; -- $1, $2는 바인드 변수입니다.
EXECUTE stmt(35, 'active'); -- EXECUTE는 준비된 계획에 값을 바인딩하여 실행합니다.
• PL/pgSQL에서 사용
→ PL/pgSQL에서 동적 SQL과 함께 바인드 변수를 활용할 수 있습니다.
DO $$
DECLARE
user_id INT := 1;
BEGIN
EXECUTE 'SELECT * FROM users WHERE id = $1' USING user_id; -- USING 키워드는 바인드 변수를 전달하는 데 사용됩니다.
END $$;
4. 주의사항
바인드 변수가 지나치게 일반화된 경우 실행 계획이 비효율적일 수 있습니다.
딱, 이번 사례에 해당합니다.
바인드 변수로 값을 바인딩하면, PostgreSQL은 이 값이 무엇인지 미리 알지 못하므로 일반적인(Generic) 계획을 선택합니다.
5. 해결 방법
• 동적 실행 계획 강제
→ 값에 최적화된 실행 계획(Custom Plan)을 매번 새로 생성하도록 강제합니다.
SET plan_cache_mode = 'force_custom_plan';
PREPARE stmt (integer) AS SELECT * FROM users WHERE id = $1;
EXECUTE stmt(1); -- 실행 계획: Custom Plan 생성
EXECUTE stmt(1000); -- 실행 계획: 값에 맞게 새로 Custom Plan 생성
• 동적 SQL 사용
→ 바인드 변수 대신 동적 SQL에서 값을 직접 포함하여 실행 계획을 매번 새로 생성하도록 유도합니다.
DO $$
DECLARE
user_id INT := 100000;
BEGIN
EXECUTE 'SELECT * FROM users WHERE id = ' || user_id; -- 값이 쿼리에 포함되게 만듭니다.
END $$;
• 특정 실행 계획 유도
→ 특정 유형의 실행 계획을 사용하도록 설정합니다.
(SET enable_seqscan = off;를 사용해 테이블 풀스캔을 비활성화하고 인덱스를 사용을 강제하도록 설정할 수 있습니다.)
+ 이 외에도 pg_hint_plan Extension 사용, STATISTICS 튜닝 등의 방법이 있습니다.
이번 사례에서 제가 사용한 방법은 '동적 실행 계획 강제'입니다.
6. Custom Plan이 적합한 경우
• 데이터 분포가 비대칭적인 경우
→ 예를 들어, age가 대부분 20~30 사이에 몰려 있고, 특정 값(예: age = 100)이 매우 적은 경우.
• 결과 행의 개수가 크게 다른 경우
→ 예를 들어, id < 10은 10개의 행만 반환하지만, id > 100000은 수십만 개의 행을 반환하는 경우.
• 데이터 범위가 넓고 다양할 때
→ 값에 따라 최적의 실행 계획이 달라지는 경우.
7. Genric Plan이 적합한 경우
일반적으로는 Generic Plan이 성능이 좋을 확률이 높습니다
• 데이터 분포가 고른 경우
→ 테이블의 데이터가 고르게 분포되어 있어 특정 값에 최적화할 필요가 없는 경우.
→ 예를 들어, id가 1부터 1,000,000까지 균등하게 분포된 경우.
• 쿼리가 자주 반복되는 경우
→ 동일한 쿼리가 다양한 값으로 여러 번 실행될 때, 실행 계획 생성 비용을 줄이기 위해 Generic Plan이 적합합니다.
→ 예를 들어, "사용자 정보를 검색"하는 API에서 같은 쿼리가 수백, 수천 번 실행될 때.
• 복잡하지 않은 쿼리
→ 조인이나 서브쿼리가 없는 간단한 쿼리에서는 Generic Plan으로도 충분히 효율적인 성능을 낼 수 있습니다.
Generic Plan과 Custom Plan 비교 표
특징 | Generic Plan | Custom Plan |
계획 생성 시점 | PREPARE 시점 | EXECUTE 시점 |
계획 생성 비용 | 낮음 (한 번만 생성) | 높음 (매 실행 시 생성) |
재사용성 | 높음 | 없음 |
값에 대한 최적화 | 없음 | 있음 |
효율성 | 값에 따라 적당히 효율적 | 특정 값에 대해 매우 효율적 |
적합한 상황 | 데이터 분포가 고르고, 쿼리가 반복적으로 실행됨 | 값에 따라 성능 차이가 클 때 |
정리하자면,
• Generic Plan은 실행 계획 생성 비용을 절감하고, 대부분의 경우 적당히 효율적인 성능을 제공합니다.
→ 데이터 분포가 균등하거나, 쿼리가 반복적으로 실행될 때 유리합니다.
• Custom Plan은 특정 값에 따라 실행 계획이 크게 달라질 경우 적합합니다.
→ 하지만 Custom Plan은 매번 실행 계획을 생성하므로, 실행 계획 생성 비용이 부담스러운 워크로드에서는 Generic Plan이 더 효율적입니다
오늘 포스팅은 여기까지입니다.
다음 포스팅도 기대해주세요 :)
'PostgreSQL' 카테고리의 다른 글
PostgreSQL: VACUUM index_cleanup 옵션과 REINDEX의 차이 (0) | 2025.01.17 |
---|---|
PostgreSQL: 옵티마이저(플래너) (0) | 2025.01.14 |
PostgreSQL: 락 체크 쿼리 (0) | 2025.01.07 |
PostgreSQL: PG ↔ Oracle 이관 시 메타데이터 정합성 체크 쿼리 (0) | 2025.01.05 |
PostgreSQL: PG ↔ Oracle 이관 시 데이터 정합성 체크 방법 (0) | 2025.01.04 |