Search

SECTION 03 원문 백업

재편집 전 원문 백업 | SECTION 03 | 2026-10-14
조건 하나가 대량 데이터 접근을 만든다.
PostgreSQL에서는 WHERE 절을 '인덱스를 사용할 수 있는 형태'와 '업무 의도에 맞는 형태'로 작성하자.
⚠️
예상 실행계획 안내
•
본 페이지의 실행계획은 실측이 아니라 개념적 예상 계획입니다. PostgreSQL에 접속하거나 실제 SQL을 실행하지 않았습니다.
•
cost, actual rows, 실행시간, Buffers 등 측정값은 기재하지 않습니다.
•
Index Cond, Filter, Seq Scan, Bitmap Heap Scan 등 노드 선택은 실제 DDL·데이터 분포·통계·Collation·옵티마이저 판단에 따라 달라질 수 있습니다.
•
예시의 인덱스 이름은 제공된 실습 스크립트에 나온 정의를 기준으로 하나, 해당 DB에 생성되어 있다는 뜻은 아닙니다. WHERE 조건을 바꾸었다는 사실만으로 Index Scan이 보장되지는 않습니다.
•
이 페이지의 GOOD은 명시된 업무 전제에서만 권장합니다. 특히 OR 분리, 부정 조건 변경은 결과의 동등성 검증이 우선입니다.

이 섹션에서 기억할 10가지 규칙

ID
구분
규칙
03-01
MUST
WHERE에서 인덱스 컬럼을 함수로 가공하지 말자
03-02
MUST
TIMESTAMP 일자 검색은 종료일 미만의 범위를 쓰자
03-03
MUST
문자형 날짜 컬럼을 숫자로 변환해 비교하지 말자
03-04
MUST
정확한 값 비교에는 LIKE 대신 =를 사용하자
03-05
TIP
LIKE의 시작 와일드카드와 접두사 검색을 구분하자
03-06
MUST
선택 조건을 CASE·COALESCE로 뭉개지 말자
03-07
CHECK
다른 테이블의 컬럼을 OR로 묶으면 접근 경로를 점검하자
03-08
MUST
NOT IN 서브쿼리의 NULL 함정을 기억하자
03-09
CHECK
부정 조건을 긍정 조건으로 바꿀 수 있는지 검토하자
03-10
MUST
업무상 필수인 조회 조건을 빠뜨리지 말자
구분: MUST — 기본 작성 원칙 / TIP — 활용 가능한 작성 기법 / CHECK — 실행계획·업무 결과 확인이 필요한 최적화 후보

REMINDER 03-01. WHERE에서 인덱스 컬럼을 함수로 가공하지 말자

MUST · 상황: 2023년 2월 1일에 주문된 데이터 건수를 조회한다.
BAD — 개선 또는 검토가 필요한 SQL
SELECT COUNT(*) AS ord_cnt FROM tr_ord_big AS o WHERE TO_CHAR(o.ord_dtm, 'YYYYMMDD') = '20230201';
SQL
복사
GOOD — 권장 또는 비교할 SQL
SELECT COUNT(*) AS ord_cnt FROM tr_ord_big AS o WHERE o.ord_dtm >= TIMESTAMP '2023-02-01 00:00:00' AND o.ord_dtm < TIMESTAMP '2023-02-02 00:00:00';
SQL
복사
예상 실행계획 — 실제 측정이 아닌 개념적 예시
BAD (tr_ord_big_x01(ord_dtm)이 존재해도 가능) Aggregate -> Index Only Scan 또는 Seq Scan on tr_ord_big Filter: (to_char(ord_dtm, 'YYYYMMDD') = '20230201') - 조건이 원래 ord_dtm의 Index Cond가 되지 못할 수 있음 GOOD (tr_ord_big_x01(ord_dtm)이 존재하고 선택성이 좋을 때) Aggregate -> Index Only Scan using tr_ord_big_x01 Index Cond: (ord_dtm >= ... AND ord_dtm < ...) - 검색 구간으로 바로 접근할 수 있음
Plain Text
복사
기억할 이유: 인덱스는 기본적으로 원본 컬럼 값으로 검색한다. 컬럼을 TO_CHAR로 바꾸면 해당 표현식을 검사하기 위해 광범위한 데이터를 읽을 수 있다.
주의·예외: 원본 실습에서는 BAD가 Index Only Scan이지만 Filter로 남았고, GOOD은 인덱스 검색 조건을 사용했다. 즉, BAD가 반드시 Seq Scan인 것은 아니다. 표현식 인덱스가 별도로 있다면 상황이 달라질 수 있다.
근거: StartUP Tuning PG PDF p.142, SQL-XPERF3-NOGOOD1; MySQL GUIDE p.4 / Oracle GUIDE p.3

REMINDER 03-02. TIMESTAMP 일자 검색은 종료일 미만의 범위를 쓰자

MUST · 상황: 2024년 1월 한 달 동안 발생한 주문을 조회한다.
BAD — 개선 또는 검토가 필요한 SQL
-- 1월 31일 '00:00:00' 이후의 주문을 놓칠 수 있음 SELECT o.ord_no, o.ord_dtm FROM tr_ord_big AS o WHERE o.ord_dtm BETWEEN TIMESTAMP '2024-01-01 00:00:00' AND TIMESTAMP '2024-01-31 00:00:00';
SQL
복사
GOOD — 권장 또는 비교할 SQL
SELECT o.ord_no, o.ord_dtm FROM tr_ord_big AS o WHERE o.ord_dtm >= TIMESTAMP '2024-01-01 00:00:00' AND o.ord_dtm < TIMESTAMP '2024-02-01 00:00:00';
SQL
복사
예상 실행계획 — 실제 측정이 아닌 개념적 예시
BAD / GOOD 모두 가능한 인덱스 검색 형태 Index Scan using tr_ord_big_x01 on tr_ord_big Index Cond: (ord_dtm >= ... AND ord_dtm <=/ < ...) - 계획 노드가 비슷해도 반환 범위가 다름 - 날짜 범위가 충분히 넓으면 Seq Scan이 더 유리할 수도 있음
Plain Text
복사
기억할 이유: BETWEEN은 시작과 끝을 모두 포함한다. TIMESTAMP의 종료일을 자정으로 지정하면 종료일의 나머지 시간이 누락된다. 반열린 구간 [시작, 다음날 또는 다음달)은 누락과 경계 중복을 줄인다.
주의·예외: 업무의 시작·종료일과 시각·타임존 정의를 먼저 확정해야 한다. DATE형과 TIMESTAMP형을 혼동하지 않는다. 이 항목의 핵심은 '성능이 무조건 더 빠르다'가 아니라 결과 정확성과 인덱스 친화적 범위 조건이다.
근거: StartUP Tuning PG PDF p.142; 실습 SQL의 ord_dtm >= TO_DATE(...) / ord_dtm < TO_DATE(...) 패턴; MySQL GUIDE p.5 / Oracle GUIDE p.4

REMINDER 03-03. 문자형 날짜 컬럼을 숫자로 변환해 비교하지 말자

MUST · 상황: 실습에서 추가된 문자형 8자리 YYYYMMDD 컬럼 ord_ymd로 특정 일자를 조회한다.
BAD — 개선 또는 검토가 필요한 SQL
SELECT COUNT(*) AS ord_cnt FROM tr_ord_big AS o WHERE o.ord_ymd::numeric = 20230401;
SQL
복사
GOOD — 권장 또는 비교할 SQL
SELECT COUNT(*) AS ord_cnt FROM tr_ord_big AS o WHERE o.ord_ymd = '20230401';
SQL
복사
예상 실행계획 — 실제 측정이 아닌 개념적 예시
BAD (tr_ord_big_x05(ord_ymd) 존재 시 가능한 계획) Aggregate -> Index Only Scan 또는 Seq Scan on tr_ord_big Filter: ((ord_ymd)::numeric = 20230401) - 원본 문자형 인덱스의 직접 탐색 조건이 아님 GOOD (인덱스 선택 시) Aggregate -> Index Only Scan using tr_ord_big_x05 Index Cond: (ord_ymd = '20230401')
Plain Text
복사
기억할 이유: 실습 스크립트에 등장하는 ord_ymd는 성능 교육 목적으로 추가된 8자리 문자형 컬럼이다. 컬럼을 numeric으로 캐스팅하지 말고 컬럼 형식과 같은 문자열 값을 비교한다.
주의·예외: ord_ymd는 SECTION 01의 원본 ERD에는 표시되어 있지 않다. 문자형 날짜를 별도로 보관하는 반정규화 자체가 일반 권장 사항은 아니다. 원본 예제의 'ord_ymd = 20230401'은 PostgreSQL에서 자료형 불일치 오류가 발생할 수 있으며, 컬럼을 변환해 오류만 피하는 것도 해법이 아니다.
근거: StartUP Tuning PG PDF p.143, SQL-XPERF3-VARCHAR, SQL-CRIX-TR_ORD_BIG_X05

REMINDER 03-04. 정확한 값 비교에는 LIKE 대신 =를 사용하자

MUST · 상황: 주문번호가 아닌, 특정 매장 S033의 주문만 검색한다.
BAD — 개선 또는 검토가 필요한 SQL
SELECT o.ord_no, o.shop_id FROM tr_ord_big AS o WHERE o.shop_id LIKE 'S033';
SQL
복사
GOOD — 권장 또는 비교할 SQL
SELECT o.ord_no, o.shop_id FROM tr_ord_big AS o WHERE o.shop_id = 'S033';
SQL
복사
예상 실행계획 — 실제 측정이 아닌 개념적 예시
BAD (가능한 계획) Seq Scan on tr_ord_big Filter: (shop_id ~~ 'S033') 또는 조건·인덱스에 따라 Index Scan GOOD (tr_ord_big_x02(shop_id) 선택 시) Index Scan using tr_ord_big_x02 Index Cond: (shop_id = 'S033')
Plain Text
복사
기억할 이유: 일치 조건은 정확한 값 비교라는 의도를 드러내고, 일반적인 B-tree 인덱스의 동등 검색과 직접 맞는다. 특수문자가 없는 고정 문자열에 대한 LIKE는 결과가 같을 수 있어도 의도가 불분명해진다.
주의·예외: PostgreSQL의 LIKE가 언제나 인덱스를 사용하지 못한다는 뜻은 아니다. LIKE의 패턴·Collation·연산자 클래스 및 통계에 따라 최적화될 수 있다. 정확한 일치가 아닌 접두사·부분 문자열 검색에는 LIKE가 필요하다.
근거: MySQL GUIDE p.17 / Oracle GUIDE p.13; PostgreSQL 인덱스 사용 여부는 환경별 예상

REMINDER 03-05. LIKE의 시작 와일드카드와 접두사 검색을 구분하자

TIP · 상황: 닉네임이 'Mo'로 시작하는 회원만 검색한다.
BAD — 개선 또는 검토가 필요한 SQL
-- 시작 두 글자가 Mo인지 계산한 다음 비교 SELECT m.mbr_id, m.nick_nm FROM ms_mbr_big AS m WHERE LEFT(m.nick_nm, 2) = 'Mo';
SQL
복사
GOOD — 권장 또는 비교할 SQL
SELECT m.mbr_id, m.nick_nm FROM ms_mbr_big AS m WHERE m.nick_nm LIKE 'Mo%';
SQL
복사
예상 실행계획 — 실제 측정이 아닌 개념적 예시
BAD (일반 nick_nm 인덱스만 있다고 가정) Seq Scan on ms_mbr_big Filter: ("left"(nick_nm, 2) = 'Mo') GOOD (접두사 패턴을 지원하는 인덱스/Collation 전제) Index Scan 또는 Bitmap Index Scan ... Index Cond: (닉네임 접두사의 검색 범위) Filter: (nick_nm ~~ 'Mo%') 또는 Seq Scan (지원 인덱스가 없거나 선택성이 낮을 때)
Plain Text
복사
기억할 이유: 원본 컬럼을 함수로 바꾸지 않으면 PostgreSQL이 접두사 검색 범위를 인덱스와 연결할 여지가 생긴다.
주의·예외: PostgreSQL의 비-C Collation에서는 보통의 B-tree 인덱스만으로 LIKE 'Mo%'의 범위 검색이 보장되지 않는다. 자료형에 맞는 text_pattern_ops/varchar_pattern_ops 인덱스, C Collation 또는 다른 검색 인덱스의 검토가 필요할 수 있다. LIKE '%Mo%'는 '시작'이 아니라 '포함' 검색이므로 GOOD과 의미가 달라 무작정 치환하지 않는다. 부분 문자열 검색이 업무 요구사항이라면 pg_trgm 등을 별도 검토한다.
근거: MySQL GUIDE p.17 / Oracle GUIDE p.13 기반, PostgreSQL의 패턴 검색 조건과 opclass 설명은 추가 보강

REMINDER 03-06. 선택 조건을 CASE·COALESCE로 뭉개지 말자

MUST · 상황: 매장 ID가 전달되면 해당 매장 주문만, 전달되지 않으면 전체 주문을 조회하는 화면의 동적 검색 조건을 작성한다.
BAD — 개선 또는 검토가 필요한 SQL
-- $1이 NULL이면 전체 조회, 아니면 해당 매장만 조회 SELECT o.ord_no, o.shop_id FROM tr_ord_big AS o WHERE CASE WHEN $1::text IS NULL THEN TRUE ELSE o.shop_id = $1::text END;
SQL
복사
GOOD — 권장 또는 비교할 SQL
-- 애플리케이션에서 매장 ID가 있을 때: SELECT o.ord_no, o.shop_id FROM tr_ord_big AS o WHERE o.shop_id = $1; -- 매장 ID가 없을 때는 조건 자체를 제외: SELECT o.ord_no, o.shop_id FROM tr_ord_big AS o;
SQL
복사
예상 실행계획 — 실제 측정이 아닌 개념적 예시
BAD (가능한 계획) Seq Scan on tr_ord_big Filter: CASE WHEN ... THEN true ELSE shop_id = $1 END GOOD (매장 ID가 있고 적합한 인덱스가 선택되면) Index Scan using tr_ord_big_x02 Index Cond: (shop_id = $1) GOOD (매장 ID가 없고 전체 조회인 경우) Seq Scan on tr_ord_big (정상적 선택 가능)
Plain Text
복사
기억할 이유: 조건이 실제 존재할 때는 단순한 컬럼 비교로 만들면 옵티마이저가 검색 조건을 인식하고 인덱스를 검토하기 쉽다. 특히 핵심 SQL에서 모든 검색 케이스를 하나의 조건식으로 밀어 넣는 방식은 주의한다.
주의·예외: 이 예시의 $1은 PostgreSQL 바인드 자리표시자의 개념 표현이다. 실제 앱에서는 드라이버·Mapper의 파라미터 문법에 맞춰 전달해야 한다. 동적 조건을 제거하거나 더하는 과정에서 NULL 의미와 보안 필터를 누락하면 안 된다. PostgreSQL은 매개변수 값과 실행계획 캐시 조건에 따라 복잡한 식도 최적화할 수 있으므로 반드시 BAD가 Seq Scan인 것은 아니다.
근거: MySQL GUIDE p.15~16 (핵심 SQL의 과도한 동적 구조, CASE) / Oracle GUIDE p.12

REMINDER 03-07. 다른 테이블의 컬럼을 OR로 묶으면 접근 경로를 점검하자

CHECK · 상황: 매장이 S033이거나, 회원의 닉네임이 Moon인 주문번호를 조회한다. ord_no가 유일한 주문 PK라는 전제에서 비교한다.
BAD — 개선 또는 검토가 필요한 SQL
SELECT o.ord_no FROM tr_ord_big AS o JOIN ms_mbr_big AS m ON m.mbr_id = o.mbr_id WHERE o.shop_id = 'S033' OR m.nick_nm = 'Moon';
SQL
복사
GOOD — 권장 또는 비교할 SQL
-- 두 조건의 결과를 따로 구한 후 중복 주문을 제거 SELECT o.ord_no FROM tr_ord_big AS o JOIN ms_mbr_big AS m ON m.mbr_id = o.mbr_id WHERE o.shop_id = 'S033' UNION SELECT o.ord_no FROM ms_mbr_big AS m JOIN tr_ord_big AS o ON o.mbr_id = m.mbr_id WHERE m.nick_nm = 'Moon';
SQL
복사
예상 실행계획 — 실제 측정이 아닌 개념적 예시
BAD (가능한 계획) Hash Join Join Cond: (o.mbr_id = m.mbr_id) Filter: ((o.shop_id = 'S033') OR (m.nick_nm = 'Moon')) -> Seq Scan / Bitmap Heap Scan on tr_ord_big -> Seq Scan on ms_mbr_big GOOD (각 브랜치에 적합한 인덱스가 있을 때 가능한 계획) HashAggregate 또는 Sort + Unique -- UNION의 중복 제거 -> Append -> Nested Loop / Hash Join -> Index Scan on tr_ord_big (shop_id = 'S033') -> PK lookup / Scan on ms_mbr_big -> Nested Loop / Hash Join -> Index Scan on ms_mbr_big (nick_nm = 'Moon') -> Index Scan on tr_ord_big (mbr_id = ...)
Plain Text
복사
기억할 이유: 각기 다른 테이블의 조건을 OR로 결합하면 조인 후 필터가 수행되면서 개별 인덱스 접근 경로를 활용하기 어려운 경우가 있다. 조건을 분리하면 각 검색의 선택성을 별도로 활용할 수 있다.
주의·예외: 이 사례의 GOOD은 '항상 더 빠른 SQL'이 아니라 비교 후보이다. UNION에는 중복 제거 비용이 들고 한 주문이 두 조건을 모두 만족할 수 있다. 원문과 같은 결과를 유지하기 위해 양쪽 브랜치 모두 동일한 INNER JOIN을 유지하고, 유일한 ord_no만 선택하는 전제를 사용했다. 실제 출력 컬럼·조인 중복·OUTER JOIN이 있으면 동등성이 달라질 수 있으므로 업무 결과와 성능을 모두 검증해야 한다.
근거: 튜닝 사례 정리 (204건) 0325_14·0819_17·0910_07 및 원본 튜닝 결과서; PostgreSQL 계획은 예상

REMINDER 03-08. NOT IN 서브쿼리의 NULL 함정을 기억하자

MUST · 상황: 픽업 상태 주문이 한 번도 없는 회원을 조회한다.
BAD — 개선 또는 검토가 필요한 SQL
SELECT m.mbr_id FROM ms_mbr_big AS m WHERE m.mbr_id NOT IN ( SELECT o.mbr_id FROM tr_ord_big AS o WHERE o.ord_st = 'PKUP' );
SQL
복사
GOOD — 권장 또는 비교할 SQL
SELECT m.mbr_id FROM ms_mbr_big AS m WHERE NOT EXISTS ( SELECT 1 FROM tr_ord_big AS o WHERE o.mbr_id = m.mbr_id AND o.ord_st = 'PKUP' );
SQL
복사
예상 실행계획 — 실제 측정이 아닌 개념적 예시
BAD (가능한 계획) Seq Scan on ms_mbr_big Filter: (NOT (hashed SubPlan ...)) -> SubPlan/Seq Scan on tr_ord_big GOOD (가능한 계획) Hash Anti Join Hash Cond: (m.mbr_id = o.mbr_id) -> Seq Scan on ms_mbr_big -> Hash -> Seq Scan on tr_ord_big (ord_st = 'PKUP') 또는 Nested Loop Anti Join 등
Plain Text
복사
기억할 이유: NOT IN의 서브쿼리 결과에 NULL이 하나라도 있으면 일반적인 비교에서 UNKNOWN이 발생해 예상과 다르게 아무 행도 반환하지 않을 수 있다. NOT EXISTS는 상관관계가 성립하는 행의 존재만 판단한다.
주의·예외: 실제 tr_ord_big.mbr_id가 NOT NULL로 정의되어 있다면 이 NULL 함정이 나타나지 않을 수 있다. 원본 ERD만으로 NOT NULL 제약을 확인하지 못했다. NOT EXISTS가 항상 NOT IN보다 빠르다는 규칙은 아니다. 실행계획은 Anti Join과 SubPlan 중 어떤 방법을 선택하는지 확인한다.
근거: PostgreSQL NULL 3값 논리에 대한 교육용 추가 항목. 원본 204건의 NOT EXISTS 기록은 '마지막 데이터 검색' 사례로, 이 NULL 함정의 실측 근거는 아님

REMINDER 03-09. 부정 조건을 긍정 조건으로 바꿀 수 있는지 검토하자

CHECK · 상황: 주문 상태가 '대기' 또는 '준비중'인 주문만 찾는다. 주문 상태가 WAIT, PREP, PKUP 세 종류뿐이라는 가상의 업무 규칙을 가정한다.
BAD — 개선 또는 검토가 필요한 SQL
SELECT o.ord_no, o.ord_st FROM tr_ord_big AS o WHERE o.ord_st <> 'PKUP';
SQL
복사
GOOD — 권장 또는 비교할 SQL
SELECT o.ord_no, o.ord_st FROM tr_ord_big AS o WHERE o.ord_st IN ('WAIT', 'PREP');
SQL
복사
예상 실행계획 — 실제 측정이 아닌 개념적 예시
BAD (가능한 계획) Seq Scan on tr_ord_big Filter: (ord_st <> 'PKUP') - 반환 행이 많으면 Seq Scan이 타당할 수 있음 GOOD (ord_st 선두 인덱스, 선택성 유리 시) Bitmap Heap Scan / Index Scan on tr_ord_big Index Cond: (ord_st = ANY ('{WAIT,PREP}'::text[])) 또는 Seq Scan (데이터 분포에 따라)
Plain Text
복사
기억할 이유: 부정 조건은 넓은 범위를 읽어야 하는 경우가 많다. 업무 상태가 명확하게 제한되어 있으면 찾을 대상을 긍정적으로 표현해 선택성을 개선할 여지가 있다.
주의·예외: WAIT/PREP는 실제 상태 코드라고 확인된 값이 아니라 교육을 위한 가정이다. PKUP 이외에 취소·반품·오류 상태가 존재하면 두 쿼리는 같지 않다. 동일 상태 집합과 NULL 정책을 검증하기 전에는 치환하면 안 된다. 부정 조건이 무조건 인덱스를 못 타는 것도 아니다.
근거: MySQL GUIDE p.21 / Oracle GUIDE p.17 (부정 조건보다 긍정 조건 검토); 예상 인덱스 경로는 추가 추론

REMINDER 03-10. 업무상 필수인 조회 조건을 빠뜨리지 말자

MUST · 상황: 회원 M00019의 2023년 3월 주문만 조회하는 화면을 구현한다.
BAD — 개선 또는 검토가 필요한 SQL
-- 전체 회원의 3월 주문을 읽음 SELECT o.ord_no, o.mbr_id, o.ord_dtm FROM tr_ord_big AS o WHERE o.ord_dtm >= TIMESTAMP '2023-03-01' AND o.ord_dtm < TIMESTAMP '2023-04-01';
SQL
복사
GOOD — 권장 또는 비교할 SQL
SELECT o.ord_no, o.mbr_id, o.ord_dtm FROM tr_ord_big AS o WHERE o.mbr_id = 'M00019' AND o.ord_dtm >= TIMESTAMP '2023-03-01' AND o.ord_dtm < TIMESTAMP '2023-04-01';
SQL
복사
예상 실행계획 — 실제 측정이 아닌 개념적 예시
BAD (tr_ord_big_x01(ord_dtm) 사용 시 가능한 계획) Index Scan using tr_ord_big_x01 Index Cond: (ord_dtm >= ... AND ord_dtm < ...) - 해당 기간의 모든 회원 주문이 탐색 대상 GOOD (tr_ord_big_x04(mbr_id, ord_dtm) 사용 시 가능한 계획) Index Scan using tr_ord_big_x04 Index Cond: (mbr_id = 'M00019' AND ord_dtm >= ... AND ord_dtm < ...) - 회원과 기간으로 탐색 범위를 함께 제한
Plain Text
복사
기억할 이유: 데이터 접근량은 '업무적으로 꼭 필요한 조건이 있는가'에 크게 좌우된다. 회원·매장·조직·기간 등 제한해야 할 범위를 빠뜨리면 인덱스가 있어도 읽는 데이터가 지나치게 많아진다.
주의·예외: BAD와 GOOD은 출력해야 하는 업무 범위가 서로 다르다. 업무 요구사항이 특정 회원 조회인 경우에 한해 GOOD이 정확한 SQL이다. 관리자용 전체 주문 조회라면 회원 조건을 임의로 추가하면 안 된다. SQL 코드의 사용자 권한 검증은 별도 필수.
근거: MySQL GUIDE p.25 (시스템 특성에 맞는 필수 조건) / 튜닝 사례 0610_14 및 0330_25 / StartUP Tuning PG PDF p.107, 복합 인덱스 예제

PostgreSQL에서 WHERE 절을 점검하는 방법

실제 실습 DB가 마련되면 같은 검색 요구사항에 대해 BAD와 GOOD의 결과 정확성을 먼저 확인하고, 이어 실행계획을 비교합니다.
-- 실제 SQL을 실행하지 않고 예상 실행계획 확인 EXPLAIN (COSTS OFF) SELECT COUNT(*) AS ord_cnt FROM tr_ord_big AS o WHERE o.ord_dtm >= TIMESTAMP '2023-02-01' AND o.ord_dtm < TIMESTAMP '2023-02-02'; -- 조회 SQL을 실제 실행하여 측정하는 경우 EXPLAIN (ANALYZE, BUFFERS, COSTS OFF) SELECT COUNT(*) AS ord_cnt FROM tr_ord_big AS o WHERE o.ord_dtm >= TIMESTAMP '2023-02-01' AND o.ord_dtm < TIMESTAMP '2023-02-02';
SQL
복사
확인할 항목: Index Cond에 조건이 들어갔는가? Filter로만 남았는가? 읽은 행 수·제외된 행 수·Buffers가 과도하지 않은가? 조인이나 집합 연산으로 변경했다면 결과가 원본과 같은가?
EXPLAIN ANALYZE는 SQL을 실제로 실행합니다. DML은 변경이 발생하므로 별도 주의가 필요합니다.

개발 전 체크리스트

WHERE 컬럼에 TO_CHAR, CAST, LEFT 등 불필요한 변형이 있는가?
TIMESTAMP 일자 검색의 종료 시각과 경계가 정확한가?
문자형 날짜·코드 컬럼을 숫자로 캐스팅하지 않았는가?
정확히 일치하는 검색에 불필요한 LIKE를 사용하지 않았는가?
LIKE 검색에 접두사 조건과 중간 포함 조건을 혼동하지 않았는가?
선택 파라미터를 CASE로 처리해 검색 조건을 감추지 않았는가?
서로 다른 테이블의 OR 조건이 인덱스 검색을 방해하는가?
NOT IN의 NULL 가능성과 NOT EXISTS 대안을 검토했는가?
부정 조건을 바꿀 때 업무 상태 집합의 동등성을 확인했는가?
회원·매장·기간 등 업무상 필수 조회 조건이 포함되어 있는가?

참고 자료 및 적용 범위

제공 자료에서 확인된 내용
•
SQL GUIDE for MySQL.pptx: 3~5페이지(자료형·컬럼 가공·날짜), 15~17페이지(동적 조건·CASE·LIKE), 21페이지(부정 조건), 25페이지(필수 업무 조건)
•
002_SQL GUIDE for ORACLE.pptx: 2~4페이지(자료형·컬럼 가공·날짜), 12~13페이지(CASE·LIKE), 17페이지(부정 조건)
•
StartUPTuning_PG_20250616.pdf: 107페이지(회원·기간 조건), 142페이지(TO_CHAR 날짜 조건), 143페이지(문자 날짜형 변환)
•
StartUPTuning_PG_SQL_Script.sql: SQL-XPERF3-NOGOOD1, SQL-XPERF3-VARCHAR, SQL-CRIX-TR_ORD_BIG_X05, tr_ord_big_x01/x02/x04 인덱스 정의
•
: 사례 0325_14, 0819_17, 0910_07, 0610_14
•
SECTION 01. SQL REMINDER 소개: BIG 계열 테이블 관계·컬럼
교육용 보강 내용(자료의 실측 사례가 아닌 일반적인 PostgreSQL 원리·추론)
•
LIKE 접두사와 text_pattern_ops 조건, NOT IN의 NULL 의미, 매개변수 값에 따른 CASE 최적화, 각 BAD/GOOD 예제의 예상 계획은 교육을 위해 새로 작성했습니다.
•
실제 DDL을 확보하지 못했으므로 PK/FK·NULL 허용 여부, ord_dtm 정확한 타입과 Collation을 임의로 확정하지 않습니다. ord_ymd는 PDF 143페이지 및 실습 SQL의 추가 실습용 문자 날짜 컬럼입니다.
•
기존 SQL GUIDE의 '절대 금지' 표현은 PostgreSQL의 표현식 인덱스·Collation 등 예외를 고려해 일반 원칙 + 예외·검증 형태로 정리했습니다.
🖼️
추후 그림 제작 메모 (이미지 미삽입)
•
그림 A: TO_CHAR(ord_dtm) 조건은 Filter로 넓게 확인, 범위 조건은 Index Cond로 구간 탐색하는 차이를 보여주는 인덱스 리프 그림
•
그림 B: TIMESTAMP 범위 검색의 BETWEEN과 >= 시작 AND < 종료 비교 타임라인
•
그림 C: 두 테이블의 OR 조건을 한 번에 평가하는 방식과 UNION으로 분리하는 방식의 검색 경로 비교
SECTION 03 한 줄 요약: 컬럼을 가공하지 말고, 조건을 정확히 주고, LIKE·OR·부정 조건은 검색 범위와 결과의 동등성을 확인하자.