GROUP BY 최적화: Loose Index Scan과 임시 테이블·filesort 판단
MySQL GROUP BY의 Loose Index Scan 조건과 temporary table, filesort 발생 원인을 실행 계획과 운영 관점에서 정리한다.
GROUP BY는 같은 값을 가진 행을 묶는 문법이지만, 실제 비용은 결과 그룹 수보다 그룹을 찾기 위해 읽어야 하는 행 수와 중간 상태를 유지하는 방식에 좌우된다. 적절한 인덱스를 이용하면 MySQL은 각 그룹의 경계만 건너뛰며 읽을 수 있다. 반대로 그룹 키를 인덱스로 처리할 수 없거나 정렬 요구가 추가되면 모든 후보 행을 읽고 내부 임시 테이블에 집계 상태를 저장하거나 filesort를 수행한다.
이 차이는 데이터가 적을 때는 드러나지 않지만, 수천만 행에서 그룹 수가 수백 개뿐인 보고서 쿼리에서는 읽기량과 지연 시간의 자릿수를 바꿀 수 있다. 이 글에서는 MySQL 8.0 이상을 기준으로 Loose Index Scan, 일반 인덱스 스캔, 내부 임시 테이블, filesort의 선택 조건을 구분하고 실행 계획을 운영 지표와 함께 해석한다.
1. GROUP BY 실행의 본질
MySQL이 그룹 집계를 처리하려면 다음 두 문제를 해결해야 한다.
- 같은 그룹에 속하는 행을 어떻게 연속적으로 찾을 것인가?
COUNT(),SUM(),MIN(),MAX()같은 집계 상태를 어디에 보관할 것인가?
입력이 그룹 키 순서로 들어오면 현재 그룹의 상태만 유지하다가 키가 바뀔 때 결과를 내보낼 수 있다. 입력 순서가 보장되지 않으면 그룹별 상태를 해시 또는 정렬 가능한 중간 구조에 저장해야 한다. MySQL 실행 계획에서 Using temporary는 주로 두 번째 방식이 필요하다는 신호다.
flowchart TD
A[WHERE 조건으로 후보 행 결정] --> B{인덱스 순서가 GROUP BY를 지원하는가}
B -->|예| C{그룹 경계만 읽을 수 있는가}
C -->|예| D[Loose Index Scan]
C -->|아니요| E[인덱스 순서대로 행을 읽으며 집계]
B -->|아니요| F[내부 임시 테이블에 그룹 상태 저장]
D --> G{별도 ORDER BY가 필요한가}
E --> G
F --> G
G -->|아니요| H[집계 결과 반환]
G -->|예| I[filesort로 결과 순서 결정]
I --> H
여기서 Using temporary와 Using filesort는 서로 다른 작업이다. 임시 테이블은 그룹 상태나 중간 결과를 저장하는 수단이고, filesort는 결과를 요청된 순서로 배치하는 정렬 알고리즘이다. 한 쿼리에서 둘 다 나타날 수도 있고 하나만 나타날 수도 있다. 또한 filesort라는 이름이 항상 디스크 파일 사용을 뜻하지는 않는다. 메모리에서 완료되는 정렬도 실행 계획에는 Using filesort로 표시된다.
2. Loose Index Scan의 동작 원리
Loose Index Scan은 인덱스의 모든 엔트리를 순차적으로 읽지 않고, 각 그룹에서 필요한 첫 번째 또는 마지막 엔트리로 이동하는 방식이다. 예를 들어 (region, sale_date) 인덱스가 있고 지역별 최소 판매일만 구한다면, 각 region의 첫 인덱스 엔트리만 읽어도 결과를 만들 수 있다.
개념적인 접근 순서는 다음과 같다.
- 인덱스의 첫 번째
region값을 찾는다. - 그 그룹의 첫 번째
sale_date에서MIN()값을 얻는다. - 현재
region범위를 건너뛰고 다음region의 시작점으로 이동한다. - 마지막 그룹까지 반복한다.
행 수가 N, 그룹 수가 G이고 G가 N보다 훨씬 작다면, 전체 인덱스를 훑는 방식보다 읽기량을 크게 줄일 수 있다. 전통 형식 EXPLAIN의 Extra에는 보통 Using index for group-by가 나타난다.
2.1 대표 적용 조건
MySQL 8.0에서 Loose Index Scan을 기대하려면 일반적으로 다음 조건을 함께 만족해야 한다.
- 단일 테이블을 대상으로 한다.
GROUP BY컬럼이 사용할 BTREE 인덱스의 가장 왼쪽 선두 컬럼들(leftmost prefix)을 이룬다.- 집계 함수가 없다면
DISTINCT와 유사한 그룹 경계 탐색이 가능해야 한다. - 집계 함수가 있다면 대표적으로
MIN()또는MAX()를 사용하며, 대상 컬럼이 인덱스에서 그룹 키 바로 다음 위치에 있어야 한다. - 같은 쿼리의
MIN()과MAX()는 같은 인덱스 컬럼을 대상으로 해야 한다. - 인덱스의 나머지 컬럼에 조건이 있다면 그룹 경계 탐색을 깨뜨리지 않는 상수 동등 조건 등 문서화된 제약을 만족해야 한다.
- 인덱스 컬럼의 prefix index만으로는 전체 값을 정확히 비교할 수 있으리라고 가정하지 않는다.
SUM()이나 COUNT()가 있다는 이유만으로 Loose Index Scan이 되는 것은 아니다. 이 함수들은 보통 그룹 내 모든 행을 확인해야 하므로, 인덱스 순서를 활용하더라도 각 인덱스 엔트리를 읽는 일반 집계가 필요하다. 실행 계획에서 단순히 Using index가 보이는 것과 Using index for group-by가 보이는 것은 구분해야 한다.
3. 재현용 테이블과 데이터
다음 예제는 MySQL 8.0 검증용 소규모 데이터다. (region, sale_date) 인덱스는 지역별 최소·최대 날짜를 그룹 경계에서 찾도록 설계했다. channel에는 별도 인덱스를 두지 않아 임시 테이블 방식과 비교한다.
DROP TABLE IF EXISTS group_sales;
CREATE TABLE group_sales (
sale_id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
region VARCHAR(20) NOT NULL,
sale_date DATE NOT NULL,
channel VARCHAR(20) NOT NULL,
amount DECIMAL(10,2) NOT NULL,
PRIMARY KEY (sale_id),
KEY ix_region_date (region, sale_date)
) ENGINE=InnoDB;
INSERT INTO group_sales (region, sale_date, channel, amount) VALUES
('부산', '2026-07-01', '매장', 12000.00),
('부산', '2026-07-03', '온라인', 18000.00),
('부산', '2026-07-08', '온라인', 25000.00),
('서울', '2026-07-02', '매장', 15000.00),
('서울', '2026-07-05', '온라인', 32000.00),
('서울', '2026-07-09', '매장', 21000.00),
('제주', '2026-07-04', '온라인', 17000.00),
('제주', '2026-07-10', '매장', 23000.00);
ANALYZE TABLE group_sales;
실행 결과(MySQL 8.0.x):
mysql> DROP TABLE IF EXISTS group_sales;
Query OK, 0 rows affected (0.00 sec)
mysql> CREATE TABLE group_sales (
-> sale_id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
-> region VARCHAR(20) NOT NULL,
-> sale_date DATE NOT NULL,
-> channel VARCHAR(20) NOT NULL,
-> amount DECIMAL(10,2) NOT NULL,
-> PRIMARY KEY (sale_id),
-> KEY ix_region_date (region, sale_date)
-> ) ENGINE=InnoDB;
Query OK, 0 rows affected (0.00 sec)
mysql> INSERT INTO group_sales (region, sale_date, channel, amount) VALUES
-> ('부산', '2026-07-01', '매장', 12000.00),
-> ('부산', '2026-07-03', '온라인', 18000.00),
-> ('부산', '2026-07-08', '온라인', 25000.00),
-> ('서울', '2026-07-02', '매장', 15000.00),
-> ('서울', '2026-07-05', '온라인', 32000.00),
-> ('서울', '2026-07-09', '매장', 21000.00),
-> ('제주', '2026-07-04', '온라인', 17000.00),
-> ('제주', '2026-07-10', '매장', 23000.00);
Query OK, 8 rows affected (0.01 sec)
Records: 8 Duplicates: 0 Warnings: 0
mysql> ANALYZE TABLE group_sales;
+-----------------------------+---------+----------+----------+
| Table | Op | Msg_type | Msg_text |
+-----------------------------+---------+----------+----------+
| mysql_tech_note.group_sales | analyze | status | OK |
+-----------------------------+---------+----------+----------+
1 row in set (0.00 sec)
이 준비 SQL은 임시 검증 데이터베이스에서 실행한다. 운영 환경에서는 기존 테이블을 삭제하는 예제로 사용하지 말아야 한다.
4. Loose Index Scan 실행 계획 확인
지역별 최초·최종 판매일은 인덱스에서 각 지역 그룹의 양 끝만 확인하면 된다.
EXPLAIN
SELECT region, MIN(sale_date) AS first_sale_date, MAX(sale_date) AS last_sale_date
FROM group_sales
GROUP BY region;
실행 결과(MySQL 8.0.x):
mysql> EXPLAIN
-> SELECT region, MIN(sale_date) AS first_sale_date, MAX(sale_date) AS last_sale_date
-> FROM group_sales
-> GROUP BY region;
+----+-------------+-------------+------------+-------+----------------+----------------+---------+------+------+----------+--------------------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------------+------------+-------+----------------+----------------+---------+------+------+----------+--------------------------+
| 1 | SIMPLE | group_sales | NULL | range | ix_region_date | ix_region_date | 82 | NULL | 4 | 100.00 | Using index for group-by |
+----+-------------+-------------+------------+-------+----------------+----------------+---------+------+------+----------+--------------------------+
1 row in set (0.00 sec)
핵심은 key가 ix_region_date이고 Extra가 Using index for group-by인지 확인하는 것이다. 테스트 데이터의 rows 추정치는 통계 샘플링과 버전에 따라 달라질 수 있으므로 고정값 자체보다 접근 방식과 Extra를 우선 해석한다.
실제 결과도 그룹별 경계를 정확히 반환한다.
SELECT region, MIN(sale_date) AS first_sale_date, MAX(sale_date) AS last_sale_date
FROM group_sales
GROUP BY region
ORDER BY region;
실행 결과(MySQL 8.0.x):
mysql> SELECT region, MIN(sale_date) AS first_sale_date, MAX(sale_date) AS last_sale_date
-> FROM group_sales
-> GROUP BY region
-> ORDER BY region;
+--------+-----------------+----------------+
| region | first_sale_date | last_sale_date |
+--------+-----------------+----------------+
| 부산 | 2026-07-01 | 2026-07-08 |
| 제주 | 2026-07-04 | 2026-07-10 |
| 서울 | 2026-07-02 | 2026-07-09 |
+--------+-----------------+----------------+
3 rows in set (0.00 sec)
Loose Index Scan의 장점은 결과 행이 적다는 사실이 아니라, 결과를 만들기 위해 방문하는 인덱스 엔트리가 그룹 수에 가까워질 수 있다는 점이다. 다만 WHERE sale_date >= ...처럼 선두 그룹 키를 건너뛰고 후속 컬럼에 범위 조건을 주면 항상 같은 최적화가 유지된다고 단정해서는 안 된다. 실제 인덱스 정의와 실행 계획을 함께 확인해야 한다.
5. 인덱스가 없는 그룹 키와 내부 임시 테이블
이번에는 인덱스가 없는 channel로 그룹화한다. 스토리지 엔진에서 읽은 순서만으로는 같은 채널의 행이 연속한다는 보장이 없으므로 서버 계층은 그룹별 카운터를 중간 구조에 유지해야 한다.
EXPLAIN
SELECT channel, COUNT(*) AS order_count, SUM(amount) AS total_amount
FROM group_sales
GROUP BY channel;
실행 결과(MySQL 8.0.x):
mysql> EXPLAIN
-> SELECT channel, COUNT(*) AS order_count, SUM(amount) AS total_amount
-> FROM group_sales
-> GROUP BY channel;
+----+-------------+-------------+------------+------+---------------+------+---------+------+------+----------+-----------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------------+------------+------+---------------+------+---------+------+------+----------+-----------------+
| 1 | SIMPLE | group_sales | NULL | ALL | NULL | NULL | NULL | NULL | 8 | 100.00 | Using temporary |
+----+-------------+-------------+------------+------+---------------+------+---------+------+------+----------+-----------------+
1 row in set (0.00 sec)
이 실행 계획에서는 일반적으로 type=ALL과 Using temporary를 확인할 수 있다. COUNT(*)와 SUM(amount)는 각 그룹의 모든 행을 반영해야 하므로 Loose Index Scan의 대표적인 MIN()·MAX() 최적화와도 성격이 다르다.
MySQL 8.0의 내부 임시 테이블은 기본적으로 TempTable 엔진을 사용할 수 있으며, 가능한 범위에서는 메모리를 이용하다가 데이터 크기와 설정에 따라 디스크 기반 저장으로 전환될 수 있다. 운영자는 Using temporary만 보고 즉시 장애라고 판단해서는 안 된다. 다음 요소를 함께 봐야 한다.
- 입력 행 수와 필터링 후 행 수
- 그룹 수와 그룹 키의 폭
- 집계 결과에 포함되는 가변 길이 컬럼의 크기
- 동시 실행 수
tmp_table_size와 TempTable 관련 메모리·디스크 설정Created_tmp_tables,Created_tmp_disk_tables의 증가율- 쿼리 지연 시간과 임시 저장 공간 I/O
상태 카운터는 서버 시작 이후 누적값이므로 한 번의 스냅샷만으로 특정 쿼리를 지목할 수 없다. 짧은 구간의 증가량을 비교하거나 Performance Schema의 statement digest 통계와 함께 해석하는 편이 안전하다.
SHOW GLOBAL STATUS WHERE Variable_name IN
('Created_tmp_tables', 'Created_tmp_disk_tables');
실행 결과(MySQL 8.0.x):
mysql> SHOW GLOBAL STATUS WHERE Variable_name IN
-> ('Created_tmp_tables', 'Created_tmp_disk_tables');
+-------------------------+-------+
| Variable_name | Value |
+-------------------------+-------+
| Created_tmp_disk_tables | 0 |
| Created_tmp_tables | 1 |
+-------------------------+-------+
2 rows in set (0.00 sec)
이 값은 검증 컨테이너의 짧은 생명주기에서 나온 수치와 운영 서버의 수치를 직접 비교할 대상이 아니다. 운영에서는 같은 시간 창의 쿼리 처리량과 함께 비율 및 증가 속도를 관찰한다.
6. GROUP BY와 filesort의 관계
MySQL 8.0의 GROUP BY는 결과 정렬을 암묵적으로 보장하지 않는다. 정렬된 결과가 필요하면 반드시 ORDER BY를 명시해야 한다. 요청한 순서가 그룹화에 사용한 인덱스 순서와 다르면 집계가 끝난 결과에 별도 filesort가 필요할 수 있다.
다음 쿼리는 지역별 건수를 계산한 뒤 건수가 큰 그룹부터 정렬한다. COUNT(*) 결과는 테이블에 저장된 인덱스 컬럼이 아니므로 인덱스 순서만으로 ORDER BY order_count DESC를 충족할 수 없다.
EXPLAIN
SELECT region, COUNT(*) AS order_count
FROM group_sales
GROUP BY region
ORDER BY order_count DESC, region;
실행 결과(MySQL 8.0.x):
mysql> EXPLAIN
-> SELECT region, COUNT(*) AS order_count
-> FROM group_sales
-> GROUP BY region
-> ORDER BY order_count DESC, region;
+----+-------------+-------------+------------+-------+----------------+----------------+---------+------+------+----------+----------------------------------------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------------+------------+-------+----------------+----------------+---------+------+------+----------+----------------------------------------------+
| 1 | SIMPLE | group_sales | NULL | index | ix_region_date | ix_region_date | 85 | NULL | 8 | 100.00 | Using index; Using temporary; Using filesort |
+----+-------------+-------------+------------+-------+----------------+----------------+---------+------+------+----------+----------------------------------------------+
1 row in set (0.00 sec)
Extra에서 Using temporary와 Using filesort가 함께 나타날 수 있다. 전자는 그룹별 카운터 유지, 후자는 집계 결과의 재정렬을 뜻한다. 정렬 대상은 원본 전체 행이 아니라 집계된 그룹 결과일 수 있으므로, 같은 Using filesort라도 비용은 입력 행 수와 그룹 수에 따라 크게 다르다.
다음 두 오해를 피해야 한다.
Using filesort가 보이면 반드시 디스크 정렬이다. 그렇지 않다. filesort는 인덱스 순서로 정렬을 해결하지 못했다는 뜻이며 메모리에서 끝날 수 있다.GROUP BY컬럼에 인덱스를 만들면 모든 집계가 빨라진다. 인덱스가 스캔 순서를 제공할 수는 있지만SUM()과COUNT()가 요구하는 모든 행 읽기를 제거하지 못할 수 있다. 필터 조건, projection, 인덱스 폭, 랜덤 I/O 비용까지 고려해야 한다.
7. 인덱스 설계 판단 기준
7.1 WHERE, GROUP BY, 집계 컬럼의 순서를 함께 본다
인덱스는 GROUP BY만 보고 설계하지 않는다. 예를 들어 특정 기간의 지역별 합계를 반복 조회한다면 (sale_date, region)은 기간 범위 탐색에는 유리하지만, 범위 조건 뒤의 region 순서를 그룹화에 온전히 활용하기 어려울 수 있다. 반대로 (region, sale_date)는 지역별 경계 탐색에는 적합하지만 전체 지역에 대한 날짜 범위 필터가 비효율적일 수 있다.
따라서 다음 질문에 답해야 한다.
- 가장 강한 선택도를 가진 조건은 무엇인가?
- 조회 범위는 일부 지역인가, 일부 기간인가?
- 집계는
MIN()·MAX()인가, 모든 행을 읽어야 하는SUM()·COUNT()인가? - 결과 그룹 수는 원본 행 수에 비해 얼마나 작은가?
- 인덱스를 넓혀 covering 효과를 얻을 가치가 쓰기 비용보다 큰가?
7.2 ORDER BY까지 같은 인덱스로 해결 가능한지 확인한다
GROUP BY region ORDER BY region처럼 그룹 키와 정렬 키가 호환되면 추가 정렬을 피할 가능성이 높다. 그러나 ORDER BY SUM(amount) DESC처럼 계산된 집계값으로 정렬하면 일반적으로 결과 집합에 대한 filesort가 필요하다. 이 경우 억지로 복잡한 인덱스를 추가하기보다, 그룹 수를 줄이는 필터와 적절한 사전 집계 전략이 더 효과적일 수 있다.
7.3 사전 집계는 읽기와 쓰기의 교환이다
대규모 이력 테이블에서 동일한 일·지역·상품 집계를 반복한다면 요약 테이블이나 비동기 집계 파이프라인을 고려할 수 있다. 다만 사전 집계는 다음 비용을 만든다.
- 원본과 요약 데이터의 일관성 관리
- 지연 도착·수정 데이터 재처리
- 집계 차원 변경 시 스키마와 배치 재설계
- 중복 집계 및 재시도에 대한 멱등성 보장
단발성 느린 쿼리를 이유로 즉시 요약 테이블을 만드는 대신, digest별 실행 빈도와 총 DB 시간을 확인한 뒤 결정해야 한다.
8. 운영 진단 절차
GROUP BY가 느릴 때는 다음 순서로 범위를 좁힌다.
EXPLAIN에서 접근 테이블 순서,type,key,rows,Extra를 확인한다.- 가능하면 비운영 환경 또는 안전한 읽기 쿼리에
EXPLAIN ANALYZE를 적용해 추정 행과 실제 행을 비교한다. Using index for group-by,Using temporary,Using filesort를 각각 독립된 신호로 해석한다.- WHERE 필터 적용 전후 행 수와 최종 그룹 수를 비교한다.
- 내부 임시 테이블 누적 카운터의 구간 증가량을 확인한다.
- Performance Schema digest에서 호출 횟수, 총 지연 시간, 검사 행 수, 임시 테이블 관련 통계를 확인한다.
- 후보 인덱스는 쓰기 부하와 버퍼 풀 점유까지 포함해 검증한다.
EXPLAIN ANALYZE는 쿼리를 실제로 실행한다. 대규모 스캔이나 잠금 영향을 일으킬 수 있는 문장을 운영 서버에서 무심코 실행하지 말아야 한다. 먼저 일반 EXPLAIN, 복제본 또는 스테이징 환경, 제한된 조건으로 검증하는 순서를 권장한다.
9. Aurora MySQL에서의 해석
Aurora MySQL도 MySQL 호환 옵티마이저와 실행 계획을 사용하므로 Using index for group-by, Using temporary, Using filesort의 기본 해석은 같다. 그러나 운영 환경에서는 다음 차이를 함께 고려해야 한다.
- Aurora의 분산 스토리지가 로컬 InnoDB 파일 시스템과 같은 I/O 특성을 가진다고 가정하지 않는다.
- 임시 객체와 정렬 작업은 인스턴스의 로컬 메모리 및 임시 저장 공간 압력과 연관될 수 있으므로 인스턴스 클래스별 한계를 확인한다.
- 파라미터는 DB cluster parameter group과 DB parameter group의 적용 범위를 구분한다.
- Performance Insights 또는 Database Insights의 상위 SQL, 대기 이벤트, 호출 빈도를 실행 계획과 함께 본다.
- Reader 인스턴스로 분석 쿼리를 분리해도 복제 지연, 캐시 상태, 인스턴스 크기 차이 때문에 Writer와 동일한 성능을 보장하지 않는다.
Aurora에서는 스토리지 계층이 다르다는 이유만으로 비효율적인 그룹 집계가 사라지지 않는다. 읽는 행 수, 중간 결과 크기, CPU 정렬 비용, 동시성 압력은 여전히 쿼리와 인덱스 설계의 문제다.
10. 실패 양상과 주의 사항
통계가 달라 실행 계획이 바뀐다
소규모 검증 데이터에서는 인덱스 최적화가 명확해도 운영 데이터의 분포, 그룹 수, 조건 선택도에 따라 옵티마이저가 다른 계획을 선택할 수 있다. ANALYZE TABLE, Histogram, 지속 통계 상태를 확인하되 통계 갱신 자체가 운영 부하와 계획 변화를 일으킬 수 있음을 고려한다.
넓은 covering index를 무조건 추가한다
집계 쿼리 하나를 위해 많은 컬럼을 포함한 인덱스를 추가하면 버퍼 풀 점유, INSERT/UPDATE 비용, redo와 복제량이 증가한다. 읽기 절감 효과는 실제 호출 빈도와 총 DB 시간으로 입증해야 한다.
ORDER BY NULL을 오래된 습관으로 사용한다
과거 버전에서는 GROUP BY의 암묵 정렬을 억제하려고 ORDER BY NULL을 사용하기도 했다. MySQL 8.0에서는 GROUP BY가 암묵 정렬을 보장하지 않으므로 이를 최신 기본 튜닝 규칙처럼 적용하지 않는다. 필요한 정렬만 명시적으로 작성한다.
임시 테이블 카운터 하나로 원인을 단정한다
Created_tmp_disk_tables는 전역 누적 카운터이며 여러 세션과 쿼리의 영향을 받는다. 트래픽이 증가하면 비율이 같아도 절대값은 증가한다. 시간 구간, 처리량, digest별 통계, 지연 시간, 임시 공간 사용량을 함께 비교한다.
11. DBA 점검표
-
GROUP BY -
Using index와Using index for group-by -
MIN()·MAX() -
COUNT()·SUM() -
Using temporary와Using filesort -
ORDER BY - 추정
rows
12. 결론
GROUP BY 최적화의 핵심은 문법을 바꾸는 요령이 아니라, 입력이 어떤 순서로 공급되고 그룹 상태를 어디에 유지하며 결과를 다시 정렬해야 하는지 이해하는 데 있다. Using index for group-by는 그룹 경계를 건너뛰는 매우 효율적인 접근을 뜻할 수 있지만 적용 조건은 제한적이다. Using temporary와 Using filesort는 각각 중간 집계와 정렬 필요성을 나타내며, 표시 자체보다 처리 행 수·그룹 수·실행 빈도·메모리와 I/O 압력을 함께 해석해야 한다.
다음 단계에서는 DISTINCT가 GROUP BY와 유사한 최적화 경로를 사용하는 경우와, 중복 제거가 조인 순서 및 임시 테이블 비용에 미치는 영향을 살펴볼 수 있다.