상세 페이지
마지막 값을 가져오는 것과 마지막 행 한 건을 가져오는 것은 다르다. 최신의 기준, 동률, 그룹별 한 건 여부를 먼저 결정하자.
난이도: 초급 후반~중급 초반 · 설명 방식: BAD SQL → GOOD SQL → 핵심 규칙. 실행계획 전문 용어는 후반 SECTION 11에서 다룬다.
학습 항목 이동: 기존 09-01 「GROUP BY에 불필요한 컬럼을 추가하지 말자」는 SECTION 07. GROUP BY와 집계 작성 규칙의 07-01로 이동했다. SECTION 09는 이제 최신 데이터 검색에 집중한다.
REMINDER 09-02. MAX 내부의 컬럼 가공을 피하자
상황: 회원별 최근 주문일자를 YYYYMMDD 문자열로 출력한다. 실제 일반 테이블 startdbpg.tr_ord에서 주문이 2,150건 있는 회원 M0035를 선택했다. 기존의 M00100은 주문이 0건이어서 인덱스 비교 실습에 적합하지 않다.
BAD — 모든 주문일시를 문자열로 변환한 후 MAX
SELECT MAX(TO_CHAR(o.ord_dtm, 'YYYYMMDD')) AS last_ord_ymd
FROM startdbpg.tr_ord o
WHERE o.mbr_id = 'M0035';
SQL
복사
MAX의 대상이 원본 TIMESTAMP가 아니라 TO_CHAR의 결과 문자열이다. 따라서 원본 ord_dtm의 B-tree 정렬 순서를 이용해 최신 1건만 읽는 최댓값 최적화를 적용하기 어려워진다.
GOOD — 원본 컬럼으로 MAX를 먼저 수행하고 최종 결과만 변환
SELECT TO_CHAR(MAX(o.ord_dtm), 'YYYYMMDD') AS last_ord_ymd
FROM startdbpg.tr_ord o
WHERE o.mbr_id = 'M0035';
SQL
복사
MAX(ord_dtm)는 원본 날짜·시각 컬럼의 최댓값을 찾는다. 조건과 정렬을 지원하는 인덱스가 있으면 PostgreSQL이 LIMIT 1 기반의 최댓값 검색으로 최적화할 수 있다.
실제 인덱스 구성 및 생성
기존 인덱스에는 tr_ord_fk01(mbr_id)와 tr_ord_x51(ord_dtm)가 각각 존재하지만, (mbr_id, ord_dtm) 복합 인덱스는 없었다. 실제 스키마에서 50번대 x51, x52, x53 이름 사용을 확인한 후 다음 인덱스를 실제로 생성했다.
CREATE INDEX IF NOT EXISTS tr_ord_x54
ON startdbpg.tr_ord (mbr_id, ord_dtm);
SQL
복사
•
생성 결과: startdbpg.tr_ord_x54 생성 성공
•
실제 정의: CREATE INDEX tr_ord_x54 ON startdbpg.tr_ord USING btree (mbr_id, ord_dtm)
•
인덱스 크기: 22MB (조회 시점)
•
데이터 입력·수정 시 인덱스 유지 비용과 저장 공간이 추가되므로 실습 목적과 운영 목적의 인덱스 추가는 구분해야 한다.
실제 데이터 / 결과 동등성
•
회원 M0035: 총 2,150건; 마지막 주문일시 2025-01-25 07:16:00
•
BAD 결과: 20250125
•
GOOD 결과: 20250125
•
추가 검증 M0039: 138건, BAD·GOOD 모두 20250119
•
주문이 없는 M00100: BAD·GOOD 모두 NULL
인덱스 생성 전 실행계획 — 실제 실측
항목 | BAD | GOOD |
스캔 방식 | tr_ord_fk01(mbr_id) Index Scan | tr_ord_x51(ord_dtm) Index Scan Backward + 회원 Filter |
스캔에서 읽은 행 수 | 2,150 | 1 (필터 제거 75행) |
Shared Buffer Hit | 2,060 | 7 |
실행시간 (각 1회) | 12.570ms | 2.357ms |
기존 날짜 단일 인덱스에서도 GOOD은 MAX(ord_dtm) 최적화가 일어나 날짜 인덱스를 역방향으로 읽고 회원 조건에 일치하는 최신 주문 1건을 찾았다. 다만 회원 조건이 인덱스 선두 키에 없어서 다른 회원 주문 75행을 필터링했다.
인덱스 생성 후 실행계획 — 실제 실측 (EXPLAIN (ANALYZE, BUFFERS, COSTS OFF))
항목 | BAD: MAX(TO_CHAR(...)) | GOOD: TO_CHAR(MAX(...)) |
사용 인덱스 | tr_ord_x54 | tr_ord_x54 |
스캔 방식 | Index Only Scan | Index Only Scan Backward |
인덱스 스캔 반환 행 수 | 2,150 | 1 |
Heap Fetches | 0 | 0 |
Buffers: shared hit | 12 | 4 |
실행시간 (재실행 각 1회) | 2.387ms | 0.447ms |
생성 직후의 캐시 워밍·동시 부하 영향을 줄이기 위해 인덱스 접근 후 재실행한 측정값이다. 다른 반복 실행에서는 BAD 2.771ms, GOOD 0.655ms도 관찰됐다. 실제 시간은 변동하므로 배율은 고정되지 않는다.
BAD — 인덱스 생성 후 실제 실행계획
Aggregate (actual time=2.244..2.245 rows=1 loops=1)
Buffers: shared hit=12
-> Index Only Scan using tr_ord_x54 on tr_ord o
(actual time=0.072..0.538 rows=2150 loops=1)
Index Cond: (mbr_id = 'M0035'::text)
Heap Fetches: 0
Buffers: shared hit=12
Planning Time: 0.746 ms
Execution Time: 2.387 ms
Plain Text
복사
GOOD — 인덱스 생성 후 실제 실행계획
Result (actual time=0.207..0.208 rows=1 loops=1)
Buffers: shared hit=4
InitPlan 1
-> Limit (actual time=0.106..0.107 rows=1 loops=1)
Buffers: shared hit=4
-> Index Only Scan Backward using tr_ord_x54 on tr_ord o
(actual time=0.105..0.106 rows=1 loops=1)
Index Cond: ((mbr_id = 'M0035'::text) AND (ord_dtm IS NOT NULL))
Heap Fetches: 0
Buffers: shared hit=4
Planning Time: 0.995 ms
Execution Time: 0.447 ms
Plain Text
복사
실행계획 읽기: BAD는 복합 인덱스를 사용하더라도 해당 회원의 2,150행을 읽고 각 ord_dtm을 TO_CHAR로 변환해 최대 문자열을 집계했다. GOOD은 mbr_id 동등 조건으로 인덱스 구간을 지정하고 ord_dtm을 역방향으로 읽어 첫 1건에서 멈췄다. 중요한 차이는 인덱스 사용 여부 자체가 아니라 읽어야 하는 인덱스 행 수다.
핵심: MAX/MIN의 대상 컬럼을 불필요하게 가공하지 말자. 최댓값을 찾을 때는 원본 컬럼을 사용하고, 문자 형식 등 출력용 변환은 집계가 끝난 후 적용하자. 해당 검색 조건과 집계 컬럼을 지원하는 인덱스가 있으면 최소 접근으로 끝날 수 있다.
주의할 예외
•
이 사례에서는 TIMESTAMP를 YYYYMMDD 문자열로 표현하는 최신 일자가 목적이므로 두 쿼리의 결과가 동일했다. 다른 문자열 형식이나 비단조 함수·타임존 변환·자료형 처리에서는 MAX(f(x)) = f(MAX(x))가 성립한다고 단정하면 안 된다.
•
ord_dtm IS NULL 데이터가 없는 실제 표본에서 검증했다. 주문 0건이면 두 방식 모두 집계 결과 NULL이다.
•
Index Only Scan에서 Heap Fetches = 0인 것은 현재 visibility map 등 접근 조건에 따른 결과이며 언제나 보장되는 것은 아니다.
•
복합 인덱스가 없어도 다른 인덱스를 통해 최댓값 최적화가 가능할 수 있다. 인덱스는 추가 공간·쓰기 비용이 있으므로 필요성을 실제 SQL 빈도·실행계획으로 판단한다.
REMINDER 09-03. 마지막 한 행은 동률까지 고려해 선택하자
상황: 회원의 가장 최근 주문 한 건에 대한 주문번호·주문일시·주문금액을 조회한다. 실제 일반 테이블 startdbpg.tr_ord의 회원 M0045를 선정했다. 이 회원은 주문 140건이 있고, 가장 최근 주문일시 2025-01-17 07:02:00이 같은 주문이 2건 존재한다.
BAD — MAX 주문일시와 같은 행을 모두 조회
SELECT o.ord_no, o.ord_dtm, o.ord_amt
FROM startdbpg.tr_ord o
WHERE o.mbr_id = 'M0045'
AND o.ord_dtm = (
SELECT MAX(x.ord_dtm)
FROM startdbpg.tr_ord x
WHERE x.mbr_id = o.mbr_id
)
ORDER BY o.ord_no DESC;
SQL
복사
MAX(ord_dtm)는 가장 최근 시각만 결정한다. 그 시각에 주문이 2건이면 2행 모두 반환하므로, 한 건만 필요한 업무 요건을 충족하지 못한다.
GOOD — 최신 일시로 정렬한 후 주문번호로 동률을 해소
SELECT o.ord_no, o.ord_dtm, o.ord_amt
FROM startdbpg.tr_ord o
WHERE o.mbr_id = 'M0045'
ORDER BY o.ord_dtm DESC, o.ord_no DESC
LIMIT 1;
SQL
복사
ord_dtm DESC로 최신 시각부터 조회하고, 같은 시각의 주문 중 ord_no가 큰 주문 1건을 선택한다. ord_no는 PK라서 정렬 순서가 유일하게 결정된다. 요청에 따라 NULLS LAST는 사용하지 않았다. 현재 실습 데이터에는 ord_dtm IS NULL인 행이 0건이다.
실제 결과 비교
구분 | 주문번호(ord_no) | 주문일시(ord_dtm) | 주문금액(ord_amt) |
BAD 첫 행 | 725809 | 2025-01-17 07:02:00 | 11,300 |
BAD 두 번째 행 | 725781 | 2025-01-17 07:02:00 | 4,500 |
GOOD 한 행 | 725809 | 2025-01-17 07:02:00 | 11,300 |
실제 실행계획 검증 — EXPLAIN (ANALYZE, BUFFERS, COSTS OFF)
항목 | BAD | GOOD |
결과 행 수 | 2 | 1 |
주요 방식 | 상관 MAX SubPlan 반복 + Index Scan | Index Scan Backward → Incremental Sort → Limit |
SubPlan 실행 횟수 | 140회 | 없음 |
Buffers: shared hit | 518 | 14 |
실행시간 (각 1회) | 2.517ms | 0.223ms |
BAD 실제 실행계획 (핵심 발췌)
Sort (actual time=2.385..2.386 rows=2 loops=1)
Sort Key: o.ord_no DESC
Buffers: shared hit=518
-> Index Scan using tr_ord_fk01 on tr_ord o
(actual time=2.340..2.353 rows=2 loops=1)
Index Cond: ((mbr_id)::text = 'M0045'::text)
Filter: (ord_dtm = (SubPlan 2))
Rows Removed by Filter: 138
SubPlan 2
-> Result (actual time=0.011..0.011 rows=1 loops=140)
InitPlan 1
-> Limit (actual time=0.010..0.011 rows=1 loops=140)
-> Index Only Scan Backward using tr_ord_x54 on tr_ord x
(actual time=0.010..0.010 rows=1 loops=140)
Heap Fetches: 0
Planning Time: 1.039 ms
Execution Time: 2.517 ms
Plain Text
복사
GOOD 실제 실행계획 (핵심 발췌)
Limit (actual time=0.135..0.136 rows=1 loops=1)
Buffers: shared hit=14
-> Incremental Sort (actual time=0.134..0.135 rows=1 loops=1)
Sort Key: ord_dtm DESC, ord_no DESC
Presorted Key: ord_dtm
Full-sort Groups: 1 Sort Method: quicksort
Peak Memory: 25kB
-> Index Scan Backward using tr_ord_x54 on tr_ord o
(actual time=0.063..0.076 rows=3 loops=1)
Index Cond: ((mbr_id)::text = 'M0045'::text)
Planning Time: 0.850 ms
Execution Time: 0.223 ms
Plain Text
복사
실행계획 해석: 09-02에서 실제 생성한 복합 인덱스 tr_ord_x54(mbr_id, ord_dtm)를 GOOD에서 사용했다. 인덱스를 역방향으로 조회하되, ord_no가 인덱스 키에 없으므로 같은 주문일시 안에서는 Incremental Sort로 동률을 정렬한다. 실제 스캔은 3행, 결과는 1행이었다. BAD에서는 상관 MAX가 140회 반복됐다. 실행시간은 각각 1회 실측값이며 데이터·캐시·계획에 따라 달라진다.
핵심: 가장 최근 시각의 MAX만으로 가장 최근 주문 한 건이 결정되는 것은 아니다. 한 건이 필요하면 업무상 동률 해소 기준(예: 주문번호)을 포함한 ORDER BY ... LIMIT 1을 사용하자.
주의할 예외
•
최신 주문일시의 동률 주문을 모두 반환해야 하는 업무라면 BAD의 여러 행은 오류가 아니며, LIMIT 1로 줄이면 결과가 달라진다.
•
주문번호 최댓값과 주문일시 최댓값은 같은 의미가 아니다. 먼저 업무상 최신의 기준을 정의한다.
•
이 예제의 ord_dtm은 실제 데이터에서 NULL이 없어서 NULLS LAST를 생략했다. PostgreSQL에서는 DESC 정렬의 기본 NULL 위치가 FIRST이므로, 향후 NULL이 허용·유입되면 별도 처리 여부를 결정해야 한다.
•
적절한 인덱스가 없을 때는 Sort 비용이 증가할 수 있다. 위 성능은 기존 tr_ord_x54 인덱스가 있는 현재 환경에서의 실측이다.
REMINDER 09-04. 상관 MAX를 반복하지 말고 먼저 집계하자
상황: 2023년 1월 5일 주문에서 회원별 가장 큰 주문번호를 가진 주문 한 건과 회원 닉네임을 구한다. 실제 일반 테이블 startdbpg.tr_ord, startdbpg.ms_mbr를 사용했다. 해당 날짜 주문은 590건, 회원은 380명이고, 정상 결과는 회원별 1행씩 380건이다.
BAD — 날짜를 먼저 제한해도 주문마다 상관 MAX를 반복
SELECT o.mbr_id, m.nick_nm,
o.ord_no, o.ord_dtm, o.ord_amt
FROM startdbpg.tr_ord o
JOIN startdbpg.ms_mbr m
ON m.mbr_id = o.mbr_id
WHERE o.ord_dtm >= TIMESTAMP '2023-01-05'
AND o.ord_dtm < TIMESTAMP '2023-01-06'
AND o.ord_no = (
SELECT MAX(x.ord_no)
FROM startdbpg.tr_ord x
WHERE x.ord_dtm >= TIMESTAMP '2023-01-05'
AND x.ord_dtm < TIMESTAMP '2023-01-06'
AND x.mbr_id = o.mbr_id
)
ORDER BY o.mbr_id;
SQL
복사
바깥쪽 주문을 하루로 제한했는데도, WHERE의 상관 서브쿼리가 후보 주문 590행 각각에 대해 MAX(ord_no)를 재계산했다. 같은 회원의 주문이 여러 건이면 그 회원의 최대 주문번호를 중복 계산하게 된다.
GOOD — 해당 날짜 주문을 회원별로 한 번 집계한 후 필요한 행과 JOIN
WITH last_no AS (
SELECT x.mbr_id,
MAX(x.ord_no) AS ord_no
FROM startdbpg.tr_ord x
WHERE x.ord_dtm >= TIMESTAMP '2023-01-05'
AND x.ord_dtm < TIMESTAMP '2023-01-06'
GROUP BY x.mbr_id
)
SELECT o.mbr_id, m.nick_nm,
o.ord_no, o.ord_dtm, o.ord_amt
FROM last_no l
JOIN startdbpg.tr_ord o
ON o.ord_no = l.ord_no
JOIN startdbpg.ms_mbr m
ON m.mbr_id = o.mbr_id
ORDER BY o.mbr_id;
SQL
복사
날짜 범위의 590건을 한 번 읽고 회원별 MAX(ord_no)를 380건으로 선집계한다. 이후 주문 PK(ord_no)와 회원 PK(mbr_id)로 상세 정보에 접근한다.
실제 데이터 / 결과 검증
•
2023-01-05 주문 590건, 서로 다른 회원 380명.
•
BAD와 GOOD 모두 380건을 반환했다.
•
출력 컬럼 전체에 대해 EXCEPT ALL을 양방향으로 실행한 결과 차이 0건이다.
•
원문 BAD에는 바깥쪽 날짜 조건이 없었다. 이번에는 BAD에도 같은 날짜 조건을 추가해 후보 범위를 같게 만들고, 상관 집계 반복과 선집계의 차이에 집중했다. 원래 BAD가 더 넓은 범위를 검사할 수 있다는 사실과 별개의 비교다.
실제 실행계획·성능 비교 (EXPLAIN (ANALYZE, BUFFERS, COSTS OFF))
항목 | BAD: 행별 상관 MAX | GOOD: 회원별 선집계 |
대상 날짜 주문 | 590건 | 590건 |
MAX(ord_no) 처리 | SubPlan 590회 실행 | HashAggregate 1회 |
집계 결과 회원 | 개별 주문의 조건 검사 후 380건 | 380개 그룹 |
최종 반환 행 | 380건 | 380건 |
Buffers: shared hit | 3,856 | 2,676 |
실행시간 (재측정 각 1회) | 9.703ms | 5.578ms |
BAD — 실제 실행계획 (핵심 노드 발췌)
Sort (actual time=9.467..9.503 rows=380 loops=1)
Sort Key: o.mbr_id
Buffers: shared hit=3856
-> Nested Loop (actual time=0.268..8.845 rows=380 loops=1)
-> Index Scan using tr_ord_x51 on tr_ord o
(actual time=0.078..6.652 rows=380 loops=1)
Index Cond: (ord_dtm >= '2023-01-05' AND ord_dtm < '2023-01-06')
Filter: (ord_no = (SubPlan 1))
Rows Removed by Filter: 210
SubPlan 1
-> Aggregate (actual time=0.010..0.010 rows=1 loops=590)
-> Index Scan using tr_ord_x54 on tr_ord x
Index Cond: (mbr_id = o.mbr_id AND ord_dtm 범위)
-> Index Scan using ms_mbr_pk on ms_mbr m
Planning Time: 1.918 ms
Execution Time: 9.703 ms
Plain Text
복사
위 Index Cond는 비교 기간을 읽기 쉽게 줄여 쓴 발췌 표현이다. 원본 계획에는 PostgreSQL 자료형 변환·날짜 상수 등이 상세하게 출력된다.
GOOD — 실제 실행계획 (핵심 노드 발췌)
Sort (actual time=5.280..5.337 rows=380 loops=1)
Sort Key: o.mbr_id
Buffers: shared hit=2676
-> Nested Loop (actual time=0.696..4.081 rows=380 loops=1)
-> Nested Loop (actual time=0.674..1.966 rows=380 loops=1)
-> HashAggregate (actual time=0.634..0.763 rows=380 loops=1)
Group Key: x.mbr_id
Batches: 1 Memory Usage: 73kB
-> Index Scan using tr_ord_x51 on tr_ord x
(actual time=0.038..0.268 rows=590 loops=1)
-> Index Scan using tr_ord_pk on tr_ord o
Index Cond: (ord_no = max(x.ord_no))
-> Index Scan using ms_mbr_pk on ms_mbr m
Planning Time: 2.841 ms
Execution Time: 5.578 ms
Plain Text
복사
실행계획 읽기: BAD는 tr_ord_x54(mbr_id, ord_dtm) 복합 인덱스가 있어도 SubPlan loops=590으로 같은 목적의 MAX 조회를 반복했다. GOOD은 tr_ord_x51(ord_dtm)로 기간 주문 590건을 읽은 다음 HashAggregate로 380개 그룹을 한 번에 만들었다. 이 사례는 인덱스 추가가 아니라 SQL 작성 방식의 변화에 의한 반복 작업 감소를 보여준다. 실행시간은 캐시·부하에 따라 달라지며 다른 재측정에서도 BAD 9.910ms, GOOD 4.870ms가 관찰됐다.
핵심: 여러 행을 처리하면서 같은 그룹의 MAX를 상관 서브쿼리로 반복 계산한다면, 필요한 범위를 먼저 읽고 그룹별로 한 번 집계한 후 JOIN하자.
주의할 예외
•
여기서 '마지막'은 그날 회원별 ord_no 최댓값이다. 가장 최근 주문일시와 같은 뜻이라는 보장은 없다.
•
ord_no가 PK이므로 최대 주문번호에 해당하는 주문 행은 하나뿐이다. 집계에 여러 값을 함께 출력하거나 1:N 관계에 JOIN할 때는 반환 행 수를 다시 확인해야 한다.
•
PostgreSQL 옵티마이저가 상관 서브쿼리를 효율적으로 처리하거나, 외부 후보가 아주 적은 경우에는 선집계가 더 빠르다고 단정할 수 없다.
•
09-05는 같은 업무를 ROW_NUMBER()로 해결하는 다른 패턴이다. 어느 방식이 빠른지는 별도 실행계획 비교가 필요하다.
REMINDER 09-05. ROW_NUMBER로 그룹별 마지막 한 건을 선택할 수 있다
검토 상태: 추가 테스트 필요 (보류)
•
후속 검증: 일반 테이블 startdbpg.tr_ord에서 NOT EXISTS·ROW_NUMBER()·GROUP BY + MAX를 업무 조건별로 추가 비교한다. 회원별/매장별, 날짜 범위, Hash Anti Join·Merge Anti Join·Nested Loop Anti Join 선택에 따른 성능 차이를 확인한다.
•
현재까지 관찰: 매장별 주간 조회에서는 NOT EXISTS가 경쟁력 있었으나, 회원별 조회와 단일 매장 장기간 조회에서는 ROW_NUMBER()가 유리했다. 새로 만든 tr_ord_x55(shop_id, ord_dtm, ord_no)가 실제 계획에 쓰이는지 및 쓰기 비용 대비 실효성도 검토해야 한다.
•
주의: 아래 tr_ord_big의 기존 BAD/GOOD은 미확정 초안으로 유지한다. ROW_NUMBER()가 항상 빠르다는 의미는 아니며, NOT EXISTS로 대체할지는 추가 테스트 후 결정한다.
상황: 09-04와 같은 날짜의 회원별 최대 주문번호 한 건이 필요하다.
BAD — 기존 또는 비교 대상
SELECT o.mbr_id, o.ord_no, o.ord_dtm, o.ord_amt
FROM tr_ord_big AS o
WHERE o.ord_no = (
SELECT MAX(x.ord_no)
FROM tr_ord_big AS x
WHERE x.mbr_id = o.mbr_id
AND x.ord_dtm >= TIMESTAMP '2023-01-05'
AND x.ord_dtm < TIMESTAMP '2023-01-06'
);
SQL
복사
GOOD — 권장 또는 대안
SELECT r.mbr_id, r.ord_no, r.ord_dtm, r.ord_amt
FROM (
SELECT o.mbr_id, o.ord_no, o.ord_dtm, o.ord_amt,
ROW_NUMBER() OVER (
PARTITION BY o.mbr_id
ORDER BY o.ord_no DESC
) AS rn
FROM tr_ord_big AS o
WHERE o.ord_dtm >= TIMESTAMP '2023-01-05'
AND o.ord_dtm < TIMESTAMP '2023-01-06'
) AS r
WHERE r.rn = 1;
SQL
복사
핵심: 날짜 조건을 먼저 적용한 후 PARTITION BY mbr_id로 묶고, 회원별 ord_no DESC 첫 행을 선택한다.
주의할 예외: ROW_NUMBER는 SQL상 회원별 한 건을 만드는 방법일 뿐, 적합한 정렬 인덱스가 없다면 많은 후보 행의 Sort가 발생할 수 있다. MAX+선집계 방식과 성능 비교가 필요하다. 반환 그룹에 여러 건을 허용한다면 RANK/동률 처리 등 다른 요구사항이다.
SQL 작성 후 체크리스트
MAX로 값 하나가 필요한가, 최신 주문 한 행이 필요한가?
주문번호의 최댓값을 최신 시각과 혼동하지 않는가?
동률 시 PK 등 유일한 기준을 지정했는가?
그룹별 마지막 한 건 조회에서 그룹 기준과 정렬 기준을 명확히 했는가?
한 줄 요약: 마지막 값을 가져오는 것과 마지막 행 한 건을 가져오는 것은 다르다. 최신의 기준, 동률, 그룹별 한 건 여부를 먼저 결정하자.

