Notice
Recent Posts
Recent Comments
Link
반응형
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | ||||||
2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 | 24 | 25 | 26 | 27 | 28 |
Tags
- 깃허브
- socket
- db버퍼캐시
- 인스턴스
- 인덱스 튜닝
- 컬렉션프레임워크
- 멀티쓰레드
- 클라이언트
- 친절한 SQL 튜닝
- Oracle
- DBA
- 자바
- 백준
- 카카오코딩테스트
- 서버
- 생성자
- DB
- 클래스
- 친절한 sql튜닝
- JavaScript
- 코딩
- SQL
- 메소드
- SQLP
- 오라클
- springboot
- Swing
- 상속
- java
- Spring
Archives
- Today
- Total
프리 정보 컨텐츠
친절한 SQL 튜닝 - 4장 조인 튜닝 본문
반응형
4장 조인 튜닝
4.1.1 NL 조인 기본 메커니즘
SELECT E.사원명, C.고객명, C.전화번호
FROM 사원 E, 고객 C
WHERE E.입사일자 >= '19960101'
AND C.관리사원번호 = E.사원번호
- 위 테이블에서 1996년 1월 1일 이후 입사한 사원이 관리하는 고객 데이터를 추출하는 데이터를 만들어 보자.
- 가장 쉽게 생각하는 방법은 사원 테이블로부터 1996년 1월 1일 이후 입사한 사원을 찾은 후, 고객 테이블에서 사원번호가 일치하는 레코드를 찾는 것 이것이 Nested Loop 조인이 사용하는 알고리즘이다.
- 아래 수행 구조를 통해 쉽게 이해할 수 있다.
<C ,JAVA>
for(i=0; i<100; i++){
for(j=0; j<100; j++){
...
}
}
<PL/SQL>
for outer in 1..100 loop
for inner in 1..100 loop
dbms_output.put_line(outer || ' : ' || inner);
end loop;
end loop;
begin
for outer in(select 사원번호, 사원명 from 사원 where 입사일자 >= '19960101')
loop
for inner in (select 고객명, 전화번호 from 고객
where 관리사원번호 = outer.사원번호)
loop
dbms_output.put_line(
outer.사원명 || ' : ' || inner.고객명 || ' : ' || inner.전화번호);
end loop
end loop;
end;
- NL 조인은 위 중첩 루프문과 같은 수행 구조를 사용한다.
- Outer와 Inner 양쪽 테이블 모두 인덱스를 이용하며 Outer쪽 테이블은 사이즈가 크지 않으면 인덱스를 이용하지 않을 수도 있다.
- 반면 Inner 쪽 테이블은 인덱스를 사용해야 한다.
- 관리사원번호로 고객 데이터를 검색할 때 Outer 루프에서 읽은 건수만큼 Table Full Scan을 반복하기 때문이다.
- 결국 NL조인은 인덱스를 이용한 조인 방식이라고 할 수 있다.
4.1.2 NL 조인 실행계획 제어
- NL 조인을 제어할 때는 아래와 같이 use_nl 힌트를 사용한다.
select /*+ordered use_nl(c)*/
e. 사원명, c.고객명, c.암호화된_전화번호
from 사원 e, 고객 c
where e.입사일자 >= '19960101'
and c.관리사원번호 = e.사원번호
- 위의 힌트는 사원 테이블(Driving Table)을 기준으로 고객 테이블(Inner Table)과 NL 방식으로 조인하라는 뜻이다.
- 세 개 이상 테이블을 조인할 때는 힌트를 아래처럼 사용한다.
select /*+ordered use_nl(B) use_nl(C) use_hash(D)*/
from A, B, C, D
where ...
- A->B->C->D 순으로 조인하되 B와 조인할 때 그리고 C와 조인할 때는 NL방식으로 D와 조인할 때는 해시 방식으로 조인하라는 뜻이다.
- ordered 대신 아래와 같이 leading 힌트를 사용하면 FROM 절을 바꾸지 않고도 마음껏 순서를 제어할 수 있다.
select /*+ leading(C,A,D,B) use_nl(A) use_nl(D) use_hash(B)*/
from A,B,C,D
where ...
4.1.5 NL 조인 특징 요약
- NL 조인의 첫 번째 특징
- 랜덤 액세스 위주의 조인 방식
- 레코드 하나를 읽으려고 블록을 통쨰로 읽는 랜덤 액세스 방식은 메모리 버퍼에서 빠르게 읽더라고 비효율이 존재한다.
- 인덱스 구성이 아무리 완벽해도 대량 데이터 조인할 때 NL 조인이 불리한 이유이다.
- NL 조인의 두 번쨰 특징
- 조인을 한 레코드씩 순차적으로 진행한다.
- 대량 데이터 처리 시 첫 번재 특징으로 불리하지만 두 번째 특징 때문에 아무리 큰 테이블이어도 빠른 응답 속도를 낼 수 있다.
4.2 소트 머지 조인
- 조인 컬럼에 인덱스가 없을 때, 대량 데이터 조인이어서 인덱스가 효과적이지 않을 때, 옵티마이저는 NL 조인 대신 소트 머지 조인이나 해시 조인을 선택한다.
- 해시 조인의 등장으로 소트 머지 조인 쓰임이 적지만 해시 조인을 사용할 수 없는 상황에서 대량 데이터를 조인하고자 할 때 여전히 유용하다.
4.2.1 SGA vs PGA
- System Glolbal Area(SGA) 공유 메모리 영역인 SGA에 캐시된 데이터는 여러 프로세스가 공유 가능하다.
- 동시에 직렬화하여 액세스 하기 위한 Lock 메커니즘으로 래치가 존재한다.
- 오라클 서버 프로세스는 SGA에 공유된 데이터를 읽고 각 오라클 서버 프로세스에 할당된 메모리 영역을 PGA라고 부른다.
- PGA는 독립적인 메모리 공간으로 래치 메커니즘이 불필요하므로 같은 양의 데이터를 읽더라도 SGA 버퍼캐시에서 읽을 때보다 훨씬 빠르다.
4.2.2 소트 머지 조인 기본 메커니즘
- 소트 단계 : 양쪽 집합을 조인 컬럼 기준으로 정렬한다.
- 머지 단계 : 정렬한 양쪽 집합을 서로 머지한다.
- 소트 머지 조인은 아래와 같은 use_merge 힌트로 유도한다.
SELECT /*+ ordered use_merge(c)*/
E.사원번호, E.사원명, E.입사일자, C.고객번호, C고객명, C.전화번호, C.최종주문금액
FROM 사원 E, 고객 C
WHERE C.관리사원번호 = E.사원번호
AND E.입사일자 >= '19960101'
AND E.부서코드 = 'Z123'
AND C.최종주문금액 >= 20000
- 위 SQL 수행 과정은 아래와 같은 과정으로 풀어서 설명 가능하다.
- 조건에 해당하는 사원 데이터를 읽어 조인컬럼인 사원번호 순으로 정렬하여 PGA 영역에 할당된 Sort Area에 저장한다. 만약에 PGA에 담을 수 없을 정도로 크면 Temp 테이블 스페이스에 저장한다.
- 조건에 해당하는 고객 데이터를 읽어 조인컬럼인 관리사원번호 순으로 정렬한다. 마찬가지로 PGA 영역에 할당.
- PGA에 저장한 사원 데이터와 고객 데이터를 조인한다. (아래 SQL 문 참고)
BEGIN
FOR OUTER IN (SELECT * FROM PGA_정렬된_사원_데이터)
LOOP -- OUTER 루프
FOR INNER IN (SELECT * FROM PGA_정렬된_고객_데이터
WHERE 관리사원번호 = OUTER.사원번호)
LOOP -- INNER 루프
DBMS_OUTPUT.PUT_LINE(...);
END LOOP;
END LOOP;
END;
- 위에 SQL에서 볼 수 있듯이 정렬한 데이터를 NL조인과 같이 사용한다는 것을 볼 수 있다.
- Sort Area에 저장한 데이터 자체가 인덱스 역할을 하므로 인덱스가 없어도 사용할 수 있는 대용량에 적합한 것이 소트 머지이다.
4.2.3 대량 데이터를 조인할 때 소트 머지 조인이 빠른 이유
- NL 조인은 인덱스를 이용한 조인 방식으로 액세스하는 모든 블록을 랜덤 액세스 방식으로 DB 버퍼캐시를 경유해서 읽는다.
- 즉, 읽는 모든 블록에 래치 획득 및 캐시버퍼 체인 스캔 과정을 거치며 버퍼 캐시에 없으면 디스크에서 읽어 들인다.
- 인덱스를 이용하기 때문에 인덱스 손익분기점 한계를 그대로 드러내어 대량 데이터 조인에 NL조인이 불리한 이유이다.
- 반면, 소트 머지 조인은 양쪽 테이블로부터 조인 대상 집합을 일괄적으로 읽어 PGA에 저장한 후 조인한다.
- PGA는 프로세스만을 위한 독립적인 메모리 공간이므로 데이터를 읽을 때 래치 획득 과정이 없어
소트 머지 조인이 대량 데이터 조인에 유리한 이유이다.
4.2.4 소트 머지 조인의 주용도
- 랜덤 액세스 위주의 NL조인이 대량 데이터 처리에 한계를 보일 떄 소트 머지 조인이 인기 있었으나 더 빠른 해시조인의 등장으로 소트 머지 조인의 쓰임새는 예전만 못하다.
- 하지만 해시 조인은 조인 조건식이 등치(=) 조건이 아닐 떄 사용할 수 없다는 단점이 존재한다.
- 그러므로 소트 머지 조인은 아래와 같은 상황에 주로 사용된다.
- 조인 조건식이 등치(=) 조건이 나닌 대량 데이터 조인
- 조인 조건식이 아예 없는 조인
4.2.5 소트 머지 조인 이해하기
SELECT /*+ ordered use_merge(c)*/
E.사원번호, E.사원명, E.입사일자, C.고객번호, C고객명, C.전화번호, C.최종주문금액
FROM 사원 E, 고객 C
WHERE C.관리사원번호 = E.사원번호
AND E.입사일자 >= '19960101'
AND E.부서코드 = 'Z123'
AND C.최종주문금액 >= 20000
- ordered는 FROM 절에 기술한 순서대로 조인하라고 옵티마이저에 지시하는 힌트이다.
- ordered와 use_merge(c) 힌트를 같이 사용했으므로 양쪽 테이블을 조인 컬럼 순으로 각각 정렬한 후 '정렬된 사원' 기준으로 '정렬된 고객'과 조인하라는 뜻으로 해석하면 된다.
- 소트 머지의 시행계획은 아래와 같다.
Execution Plan
----------------------------------------------------
SELECT STATEMENT Optimizer=ALL_ROWS
MERGE JOIN
SORT(JOIN)
TABLE ACCESS (BY INDEX ROWID) OF '사원' (TABLE)
INDEX (RANGE SCAN) OF '사원_X1' (INDEX)
SORT(JOIN)
TABLE ACCESS (BY INDEX ROWID) OF '고객' (TABLE)
INDEX (RANGE SCAN) OF '고객_X1' (INDEX)
4.2.6 소트 머지 조인 특징 요약
- 소트 머지 조인은 조인을 위해 실시간으로 인덱스를 생성하는 것과 같다.
- 양쪽 집합을 정렬한 다음에 NL조인과 같은 방식으로 사용하지만, PGA영역에 저장한 데이터를 이용하기 때문에 빠르다.
- NL 조인은 조인 컬럼에 대한 인덱스 유무에 크게 영향을 받지만, 소트 머지 조인은 영향을 크게 받지 않는다.
- 따라서
조인 컬럼에 인덱스가 없는 상황에서 대용량 테이블을 각각 읽어 조인 대상 집합을 줄일 수 있을 때 아주 유리하다.
- 스캔 위주의 액세스 방식을 사용한다는 점도 중요한 특징이지만, 모든 처리가 스캔 방식으로 이루어지지는 않고 이는 해시 조인도 마찬가지이다.
4.3 해시 조인
- 소트 머지 조인과 해시 조인은 조인 과정에 인덱스를 이용하지 않아 대량 데이터 조인할 때 NL조인보다 훨씬 빠르다.
- 소트 머지 조인은 양쪽 테이블을 정렬하는 부담이 있지만, 해시 조인은 그런 부담도 없다.
4.3.1 해시 조인 기본 메커니즘
- Build 단계 : 작은 쪽 테이블을 읽어 해시 테이블을 생성한다.
- Probe 단계 : 큰 쪽 테이블을 읽어 해시 테이블을 탐색하며 조인한다.
SELECT /*+ ordered use_hash(c)*/
E.사원번호, E.사원명, E.입사일자, C.고객번호, C고객명, C.전화번호, C.최종주문금액
FROM 사원 E, 고객 C
WHERE C.관리사원번호 = E.사원번호
AND E.입사일자 >= '19960101'
AND E.부서코드 = 'Z123'
AND C.최종주문금액 >= 20000
- 소트 머지 조인에서 사용했던 SQL에서 처럼 해시 조인은 use_hash 힌트로 유도한다.
- Build 단계 : 아래 조건에 해당하는 사원 데이터를 읽어 조인 컬럼인 사원번호를 해시 테이블 키 값으로 사용한다. 즉, 사원번호를 해시 함수에 입력해서 반환된 값으로 해시 체인을 찾고, 그 해시 체인에 데이터를 연결한다. 해시 테이블은 PGA 영역에 할당된 Hash Area에 저장한다.
- Probe 단계 : 고객 데이터에 해당하는 조건식으로 고객 데이터를 하나씩 읽어 해시 테이블을 탐색한다. 즉, 관리사원번호를 해시 함수에 입력해서 반환된 값으로 해시 체인을 찾고, 그 해시 체인을 스캔해서 값이 같은 사원번호를 찾는다.
BEGIN
FOR OUTER IN (SELECT 고객번호, 고객명...
FROM 고객
WHERE 최종주문금액 >= 20000)
LOOP -- OUTER 루프
FOR INNER IN (SELECT 사원번호, 사원명, 입사일자
FROM PGA_정렬된_사원 해시맵
WHERE 사원번호 = OUTER.관리사원번호)
LOOP -- INNER 루프
DBMS_OUTPUT.PUT_LINE(...);
END LOOP;
END LOOP;
END;
- Probe 단계에서 조인하는 과정을 PL/SQL 코드로 표현하면 위와 같다.
4.3.2 해시 조인이 빠른 이유
- Hash Area에 생성한 해시 테이블을 이용한다는 점만 다를 뿐 해시 조인도 프로세싱 자체는 NL 조인과 같다.
- 해시 조인도 소트 머지 조인과 같이 해시 테이블을 PGA 영역에 할당하기 때문이다.
- NL 조인은 Outer 테이블 레코드마다 Inner 쪽 테이블 레코드를 읽기 위해 래치 획득 및 캐시버퍼 체인 스캔 과정을 반복하지만, 해시 조인은 래치 획득 과정 없이 PGA에서 빠르게 데이터를 탐색하고 조인한다.
- 즉 정리하자면 해시 조인은, NL 조인처럼 조인 과정에서 발생하는 랜덤 액세스 부하가 없고, 소트 머지 조인처럼 양쪽 집합을 미리 정렬하는 부하도 없다.
- 해시 테이블을 생성하는 비용이 수반되지만, 둘 중 작은 집합을 Build Input 으로 선택하므로 대게는 부담이 크지 않다.
4.3.3 대용량 Build Input 처리
- 대용량 테이블 두 개를 조인할 때 인메모리 해시조인이 아니라 불가능한 상황에서는
분할, 정복
방식을 통해 조인한다.
- 파티션 단계 : 조인하는 양쪽 집합의 조인 컬럼에 해시 함수를 적용하고, 반환된 해시 값에 따라 동적으로 파티셔닝한다. 독립적으로 처리할 수 있는 여러 개의 작은 서브 집합으로 분할함으로써 파티션 짝을 생성한다.
- 조인 단계 : 파티션 단계를 완료하면, 각 파티션 짝에 대해 하나씩 조인을 수행한다. 이 때 Build Input과 Probe Input은 독립적으로 각 파티션별로 작은 쪽을 선택하여 해시 테이블을 선택한다.
4.3.5 조인 메서드 선택 기준
- 소량 데이터 조인 -> NL 조인
- 대량 데이터 조인 -> 해시 조인
- 대량 데이터 조인 but 해시 조인으로 처리 불가능(등치 조건 =이 아닐 때, 조인 조건식이 없을 경우) -> 소트 머지 조인
- 여기서 소량과 대량의 기준은 데이터의 양이 아니라 랜덤 액세스가 많아 성능이 나오지 않는 경우도 대량 데이터 조인에 해당한다.
수행 빈도가 높은 쿼리는 해시조인 보다는 NL 조인을 선택
하라고 한다. 왜냐하면 NL 조인은 인덱스를 통해 쿼리의 성능을 영구적으로 유지하며 재사용하는 반면에 해시 테이블은 단 하나의 쿼리를 위해 소멸하는 자료구조이기 떄문이다.- 즉 결론적으로 해시 조인은 아래 세 가지 조건을 만족하는 SQL문에 주로 사용한다.
1. 수행 빈도가 낮음.
2. 쿼리 수행 시간이 오래 걸림
3. 대량 데이터 조인하는 경우
4.4 서브쿼리 조인
- 옵티마이저는 Cost를 평가하고 조인에 대한 실행계획을 생성하기에 앞서 사용자가 사용하는 SQL을 최적하에 유리한 형태로 변환하는 작업부터 진행한다.
필터 오퍼레이션
- 아래 쿼리는 서브 쿼리를 필터 방식으로 처리할 때의 실행계획이다. no_unnest 힌트를 사용하면 서브 쿼리를 풀어내지 말고 수행하라고 옵티마이저에 지시하는 힌트이다.
select c.고객번호, c.고객명
from 고객 c
where c.가입일시 >= trunc(add_months(sysdate, -1), 'mm')
and exists(
select /*+ no_unnest */
from 거래
where 고객번호 = c.고객번호
and 거래일시 >= trunc(sysdate, 'mm') )
)
Execution Plan
------------------------------------------------------
0 SELECT STATEMENT OPTIMIZER = ALL_ROWS
1 0 FILTER
2 1 TABLE ACCESS(BY INDEX ROWID) OF '고객' (TABLE)
3 2 INDEX (RANGE SCAN) OF '고객_X01' (INDEX)
4 1 INDEX (RANGE SCAN) OF '거래_X01' (INDEX)
- 필터 오퍼레이션은 기본적으로 NL조인과 루틴이 같으나 차이점이 존재하다면 메인쿼리의 한 로우가 서브쿼리의 한 로우와 조인에 성공하는 순간 멈추고, 메인쿼리의 다음 로우를 계속 처리한다는 점이다.
4.4.4 스칼라 서브쿼리 조인
- 아래와 같은 GET_DNAME 함수가 있다.
CREATE OR REPLACE FUNCTION GET_DNAME(P_DEPTNO NUMBER) RETURN VARCHAR2
IS
L_DNAME DEPT.DNMAE%TYPE;
BEGIN
SELECT DNAME INTO L_DNAME FROM DEPT WHERE DEPTNO = P_DEPTNO;
RETURN L_DNAME;
EXCEPTION
WHEN OTHERS THEN
RETURN NULL;
END;
- GET_DNAME 함수를 사용하는 아래 쿼리를 실행하면 함수 안에 있는 SELECT 쿼리를 메인쿼리 건 수 만큼 재귀적으로 반복 실행한다.
SELECT EMPNO, ENAME, SAL, HIREDATE, GET_DNAME(E.DEPTNO) AS DNAME
FROM EMP E
WHERE SAL >= 2000
- 위에 코드는 아래 처럼 풀어서 사용가능하다.
SELECT EMPNO, ENAME, SAL, HIREDATE,
,(SELECT D.NAME FROM DEPT D WHERE D.DEPTNO = E.DEPTNO) AS DNAME
FROM EMP E
WHERE SAL >= 2000
- 이는 아래 OUTER 조인문처럼 하나의 문장으로 이해하라는 뜻이며, 아래 처럼 NL 조인 방식으로 실행된다.
SELECT EMPNO, ENAME, SAL, HIREDATE, D.DNAME
FROM EMP E LEFT OUTER JOIN DEPT D ON E.DEPTNO = D.DEPTNO
WHERE E.SAL >= 2000
스칼라 서브쿼리 캐싱 효과
- 스칼라 서브쿼리로 조인하면 오라클은 조인 횟수를 최소화하려고 입력 값과 출력 값을 내부 캐시에 저장해 둔다.
- 조인할 때마다 캐시에서 입력 값을 찾아보고 찾으면 저장된 출력 값을 반환한다.
- 캐시에서 찾지 못할 때만 조인을 수행하며, 결과는 버리지 않고 캐시에 저장해준다.
- 스칼라 서브쿼리의 입력 값은, 그 안에서 참조하는 메인 쿼리의 컬럼 값이다.
SELECT EMPNO, ENAME, SAL, HIREDATE
,(SELECT D.NAME -- 출력 값 : D.NAME
FROM DEPT D
WHERE D.DEPTNO = E.DEPTNO -- 입력 값 : E.EMPNO
) AS DNAME
FROM EMP E
WHERE SAL >= 2000
- 캐싱은 쿼리 단위로 이루어지며, 쿼리를 시작할 때 PGA 메모리에 공간을 할당하고, 쿼리를 수행하면서 공간을 채워나가고, 쿼리를 마치는 순간 공간을 반환한다.
- 아래의 예제는 많이 활용 되는 튜닝 기법에 해당한다.
SELECT EMPNO, ENAME, SAL, HIREDATE
,(SELECT GET_DNAME(E.DEPTNO) FROM DUAL) AS DNAME
FROM EMP E
WHERE SAL >= 2000
- 위와 같이 SELECT-LIST에 사용한 함수는 메인 쿼리 결과 건 수 만큼 반복 수행되는데, 스칼라 서브쿼리를 덧씌우면 호출 횟수를 최소화할 수 있다.
- 하지만 캐시 공간은 늘 부족하고, 스칼라 서브쿼리 캐싱 효과는 입력 값의 종류가 소수여서 해시 충돌 가능성이 작을 때 효과가 있다.
- 즉, 캐시를 매번 확인하는 비용 때문에 오히려 성능이 더 나빠지고 CPU 사용률만 높게 만들어 메모리도 더 사용한다.
- 아래 쿼리를 예로 들어서 보자.
SELECT 거래번호, 고객번호, 영업조직ID, 거래구분코드,
, (SELECT 거래구분명 FROM 거래구분 WHERE 거래구분코드 = T.거래구분코드) 거래구분명
FROM 거래 T
WHERE 거래일자 >= TO_CHAR(add_months(SYSDATE, -3), 'YYYYMMDD') -- 50,000건
- 거래구분코드로 20개 값이 존재할 때 20개면 캐시에 모두 저장하고도 남아 메인 쿼리에서 50,000개를 읽는 동안 거래구분코드별 조인 액세스는 최초 한 번씩만 발생하여 이후에는 모두 캐시에서 데이터를 찾아 조인 성능을 높이는 데 큰 도움이 된다.
- 위 쿼리에서 스칼라 서브쿼리가 성능에 도움이 되려면, 최근 3개월간 수백 명 이내 일부 고객만 거래를 발생시켰어야 한다.
- 또한, 메인 쿼리 집합이 매우 작으면 캐싱은 쿼리 단위로 이루어지므로 캐시 재사용성이 낮다.
SELECT C.고객번호, C.고객명
, (SELECT ROUND(AVG(거래금액), 2) 평균거래금액
FROM 거래
WHERE 거래일시 >= TRUNC(SYSDATE, 'MM')
AND 고객번호 = C.고객번호)
FROM 고객 C
WHERE C.가입일시 >= TRUNC(add_months(SYSDATE, -1), 'MM')
Execution Plan
------------------------------------------------------------
0 SELECT STATEMENT OPTIMIZER=ALL_ROWS
1 0 SORT
2 1 TABLE ACCESS (BY INDEX ROWID BATCHED) OF '거래'
3 2 INDEX (RANGE SCAN) OF '거래_X02'
4 0 TABLE ACCESS (FULL) OF '고객'
5 4 INDEX (RANGE SCAN) OF '고객_X01'
실행순서
- 고객_X01 인덱스 범위 스캔 , 가입일시 >= C.가입일시 >= TRUNC(add_months(SYSDATE, -1), 'MM') 를 만족하는 고객 위치 정보를 찾습니다.
- 찾은 고객 정보를 기반으로 고객번호와 고객명을 메모리에 로드합니다.
- 거래_X02 인덱스 범위 스캔 , 거래일시 >= TRUNC(SYSDATE, 'MM') 거래 데이터를 조회합니다.
- 조회한 데이터를 기반으로 거래금액을 메모리에 로드합니다.
- 각 고객별로 가져온 거래 데이터를 정렬 및 그룹화하여 거래금액의 평균을 계산하고 , ROUND합니다.
- 최종적으로 결과를 수집하여 사용자에게 반환합니다.
반응형
Comments