글을 시작하며

안녕하세요! PassionFruit200 입니다! 
저는 현재 세미나허브의 여러가지 쿼리들을 개발하고 있습니다.

이번 글에서는 애플리케이션단 작성하며 생각했던점들을 글로 남겨보았습니다.

목차

  1. [쿼리 1] 문제상황
    1. 쿼리작성
    2. 탐색방식 리스트업
      1. Nested Loop Join : Seminar Table Full Scan + company_pk_ix
      2. Nested Loop Join : seminar_ix1 ( company_no )  + company_pk_ix
      3. Nested Loop Join : seminar_ix2 ( company_no  + inst_dt ) + company_pk_ix
      4. Nested Loop Join : seminar_ix3 ( inst_dt ) + company_pk_ix
      5. Nested Loop Join : seminar_ix4 ( inst_dt + company_no ) + company_pk_ix 
      6. Sort Merge Join : No Index
      7. Hash Join : No Index
    3. 최종선택
    1.  

[쿼리 1] 문제상황 

세미나 허브 기획팀에서 사용자가 [세미나 목록 버튼]을 누르면, 세미나 허브 목록을 리스트업하여, 사용자에게 보여주려는 기능을 만들려고 합니다.

 

2500만건의 세미나 테이블50만건의 회사 테이블을 활용해 '0초~0.3초' 안에 세미나 제목, 강의 기간, 신청 기간(시작일-종료일), 수강 금액, 신청 가능 인원, 주최 회사명, 신청 상태를 보여주고, 최신 등록순, 가격 내림차순, 가격 오름차순 으로 보여주고, 10개씩 페이지네이션 처리를 통한 목록 조회를 할 수 있도록 처리해달라고 합니다.

1-1. 쿼리 작성

먼저, 쿼리를 작성합니다. 불필요한 데이터 조회(ex. limit 100, 20과 같이)를 줄이기 위해 아래와 같이 2가지로 작성합니다.

- 첫페이지

- 두번째 이후 페이지 쿼리

-- 첫 페이지 조회용 쿼리
select s.NAME as "제목", 
	s.reg_start_date as "신청시작일자",
	s.reg_end_date as "신청종료일자",
	s.start_date as "시작일자", 
	s.end_date as "종료일자",
	s.price as "금액", 
	s.max_capacity as "최대 인원",
	s.available_seats as "여석",
	c.company_no as "회사고유번호",
	c.name as "회사명",
	case 
		when (s.reg_end_date <= now()) then "신청 가능"
		when (s.reg_end_date > now()) then "신청 마감"
	end as '신청가능여부'
from SEMINAR s, company c
where c.company_no  = s.company_no
order by s.inst_dt DESC
limit 0,10;

-- 두번쨰 및 마지막 페이지 조회용 쿼리
select s.NAME as "제목", 
	s.reg_start_date as "신청시작일자",
	s.reg_end_date as "신청종료일자",
	s.start_date as "시작일자", 
	s.end_date as "종료일자",
	s.price as "금액", 
	s.max_capacity as "최대 인원",
	s.available_seats as "여석",
	c.company_no as "회사고유번호",
	c.name as "회사명",
	case 
		when (s.reg_end_date <= now()) then "신청 가능"
		when (s.reg_end_date > now()) then "신청 마감"
	end as '신청가능여부'
from SEMINAR s, company c
where s.inst_dt <= :page_last_inst_dt and not (s.inst_dt = :page_last_inst_dt and seminar_no >= :page_last_seminar_no)
c.company_no  = s.company_no
order by s.inst_dt DESC
limit 0,10;

 

쿼리 실행시 아래와 같이 나와야 합니다.

해당 쿼리가 실제로 데이터베이스에서 어떻게 검색될 수 있을지 탐색방법 목록을 작성했습니다.

1-2. 탐색방식 리스트업

기본 설정 조건 

- Driving Table은 Seminar Table 설정, Driven Table은 Comapny Table 설정합니다.

첫번쨰 이유는, 세미나 목록을 가져오는 것이므로, Seminar와 Company가 한개의 Seminar는 한개의 Company에 의해 공급된다라는 N : 1 공급 관계이므로 N쪽 데이터 입장에서 가져와야합니다. (만약 Company 입장에서 가져오려면 Company 값을 1개로 고정시켜야만 올바르게 가져옵니다.)

두번쨰 이유는, Driving Table은 항상 데이터가 큰 테이블을 기준, Driven Table은 항상 데이터가 작은 테이블을 기준으로 설정해야 탐색 횟수가 줄어듭니다.

 

모든 탐색방식 리스트업

1. Nested Loop Join : Seminar Table Full Scan + Company Table Full Scan

2. Nested Loop Join : seminar_ix1 ( company_no )  

3. Nested Loop Join : seminar_ix2 ( company_no  + inst_dt )

4. Nested Loop Join : seminar_ix3 ( inst_dt )

5. Nested Loop Join : seminar_ix4 ( inst_dt + company_no )

6. Nested Loop Join : company_ix1 ( company_no )

7. Nested Loop Join : company_ix2 ( company_no + name )

8. Nested Loop Join : seminar_ix1( company_no )  + company_ix1 ( company_ no )

9. Nested Loop Join : seminar_ix1( company_no )  + company_ix2 ( company_ no + name )

10. Nested Loop Join : seminar_ix2 ( company_no  + inst_dt )  + company_ix1 ( company_ no )

11. Nested Loop Join : seminar_ix2 ( company_no  + inst_dt )  + company_ix2 ( company_ no + name )

12. Nested Loop Join : seminar_ix3 ( inst_dt )  + company_ix1 ( company_ no )

13. Nested Loop Join : seminar_ix3 ( inst_dt )  + company_ix2 ( company_ no + name)

14. Nested Loop Join : seminar_ix4 ( inst_dt + company_no )  + company_ix1 ( company_ no )

15. Nested Loop Join : seminar_ix4 ( inst_dt + company_no )  + company_ix2 ( company_ no + name)

16. Sort Merge Join : No Index

17. Hash Join : No Index

 

처음에는 위와 같이 리스트업 시켰습니다.

하지만, 여기서 7가지의 방식으로 목록을 줄일 수 있었습니다.

Driven Table의 조인조건인 company_no는 company table의 고유번호, 즉 Primary Key였습니다. 이 의미는, "반드시" PK Clustered Index가 존재한다는 의미입니다. Mysql에서는 한개의 테이블 당 PK Clustered Index 에 의해 반드시 Primary Key Index가 존재합니다.

그렇다면, PK Clustered Index 를 제가 적용한 이유는 무엇일까요?

1. 가장 큰 이유는, company_no의 Cardinality는 1로써, 고유번호임을 알 수 있습니다. 고유번호는 당연히 카디널리티가 1입니다. 1건을 찾기 위해 49만 9999건을 찾는 비효율을 피할 수 있습니다. 

select count(*) as NDV, min(cnt) as Min_Cardinality, max(cnt) as Max_Cardinality, avg(cnt) as Avg_Cardinality 
from ( select company_no, count(*) as cnt
from company c 
where (company_no is not null)
group by company_no ) c ;

2. PK Clustered Index는 모든 컬럼들이 Leaft Node Value에 저장되어 있습니다. 즉 Covering Index로 사용할 수 있습니다. 

(몇몇 분들은 Table Full Scan이 나을 수 있다고 생각할 수 있겠지만, company 테이블의 company_no는 결합조건이기에 그렇지 않습니다.)

 

기존의 모든 탐색방식에서 Driven Table인 Company Table은 반드시 company_pk_ix 를 사용하도록 수정합니다.

1. Nested Loop Join : Seminar Table Full Scan + company_pk_ix

2. Nested Loop Join : seminar_ix1 ( company_no )  + company_pk_ix

3. Nested Loop Join : seminar_ix2 ( company_no  + inst_dt ) + company_pk_ix

4. Nested Loop Join : seminar_ix3 ( inst_dt ) + company_pk_ix

5. Nested Loop Join : seminar_ix4 ( inst_dt + company_no ) + company_pk_ix  

6. Sort Merge Join : No Index

7. Hash Join : No Index

 

다행히, 사전에 불필요한 방법들을 가지치기 할 수 있었습니다.

각 방식의 장단점과 비용을 비교해보겠습니다.

1-2-1. Nested Loop Join : Seminar Table Full Scan + company_pk_ix

시나리오를 작성합니다.

  1. Table Full Scan으로 Seminar Table 1건씩 읽어가며, Company Table PK Clustering Index접근합니다.
  2. 결합조건에 맞는 레코드를 찾고, 결합하고 필요한 컬럼들도 가져옵니다.
  3. 조건에 일치된 레코드들을 Using FileSort 혹은 TempTable 을 통해서 저장하고, Order by inst_dt desc로 정렬합니다.
  4. 상위 10건 데이터를 클라이언트에 반환합니다.

예상 비용 계산식입니다. (B-Tree의 Degree는 2라고 가정. 정렬 함수 또한 QuickSort라고 가정. 통제불가능 변수 무시 )

Expected Time Complexity = Seminar Table Full Scan Record Sequential Access * Company PK Clustering Index( Base = Degree = 2 ) Search +  FileSort Cost(Quick Sort)

                          = O(2.5 * 10^7) * O(log 5*10^5) + O(2.5 * 10^7 * log (2.5 * 10^7) )

 

Expected Space Complexity = 0

 

첫번쨰 페이지 호출 쿼리입니다. 제가 원하는 바로 동작시키기 위해 index hint를 사용한점을 살펴보세요. (Mysql에서는 Full Scan Table Hint가 존재하지 않습니다. )

select s.NAME as "제목", 
	s.reg_start_date as "신청시작일자",
	s.reg_end_date as "신청종료일자",
	s.start_date as "시작일자", 
	s.end_date as "종료일자",
	s.price as "금액", 
	s.max_capacity as "최대 인원",
	s.available_seats as "여석",
	c.company_no as "회사고유번호",
	c.name as "회사명",
	case 
		when (s.reg_end_date > now()) then "신청 가능"
		when (s.reg_end_date <= now()) then "신청 마감"
	end as '신청가능여부'
from SEMINAR s ignore index(primary, seminar_ix1, seminar_ix3, seminar_ix4), company c use index(primary)
where c.company_no = s.company_no 
order by s.inst_dt DESC
limit 0,10;

1m 13s 가 걸립니다. 제 목표인 0~1초 보다 7300% 초과입니다.

실행계획입니다.

예상한대로, 실행계획이 나왔습니다.

혹시 모르니 Tree 형태로도 확인합니다.

시나리오와 일치합니다.

1-2-2. Nested Loop Join : seminar_ix1 ( company_no )  + company_pk_ix

시나리오입니다.

  1. seminar_ix1(company_no) 으로 company가 정렬된 순서대로 읽으며, PK Clustered Index를 통해 인덱스 value값(Column)을 가져옵니다.
  2. 결합조건에 맞는 레코드를 찾고, 필요한 컬럼들도 가져옵니다.
  3. Using FileSort 혹은 TempTable 을 통해서 저장하고, Order by inst_dt desc로 정렬합니다.
  4. Sorted Semiar x Company Table 상위에서 10건을 선택합니다

시나리오에서 보이듯이, 이 방식은 전혀 도움이 안됩니다.

이유는, Driving Table인 Seminar Table의 조인컬럼에 Index를 설정하였기 때문입니다. 이 조인컬럼은 조인 조건외에 조건으로도 사용되지 않았고, 정렬 조건으로도 사용되지 않았습니다. 더더욱이 Driving Table이었기에 의미가 없습니다.

 

1-2-2 방식은 Seminar_ix1 을 만들어서, Space complexity 가 (레코드 수) x (컬럼 크기) = (2.5 * 10^7) * (2^32 bit)를 사용함에도 불구하고, 아래의 TimeComplexity가 1-2-1의 Time Complexity와 Space Complexity 측면 모두에서 효율이 떨어집니다.

 

예상 비용 계산식입니다. (B-Tree의 Degree는 2라고 가정. 정렬 함수 또한 QuickSort라고 가정. 통제불가능 변수 무시 )

Expected Time Complexity = Seminar_ix1 * Seminar PK Clustering Index Search * Company PK Clustering                                                                      Index(Degree = 2) Search +  FileSort Cost(Quick Sort)

                                            = O(2.5 * 10^7) * O(log 2.5 * 10^7) * O(log 5*10^5) + O(2.5 * 10^7 * log (2.5 * 10^7) )

 

Expected Space Complexity = Seminar_ix 1

                                              = O( (4  byte(company_no) + 4 byte (PK)) * 2.5 *10^7)

 

첫번쨰 페이지 호출 쿼리입니다. 제가 원하는 바로 동작시키기 위해 index hint를 사용한점을 살펴보세요.

select s.NAME as "제목", 
	s.reg_start_date as "신청시작일자",
	s.reg_end_date as "신청종료일자",
	s.start_date as "시작일자", 
	s.end_date as "종료일자",
	s.price as "금액", 
	s.max_capacity as "최대 인원",
	s.available_seats as "여석",
	c.company_no as "회사고유번호",
	c.name as "회사명",
	case 
		when (s.reg_end_date > now()) then "신청 가능"
		when (s.reg_end_date <= now()) then "신청 마감"
	end as '신청가능여부'
from SEMINAR s use index(seminar_ix1), company c use index(primary)
where c.company_no = s.company_no 
order by s.inst_dt DESC
limit 0,10;

1m 5s가 걸렸습니다.

실행계획입니다.

예상한대로 이 방식은 전혀 도움 안됩니다.

1-2-3. Nested Loop Join : seminar_ix2 ( company_no  + inst_dt ) + company_pk_ix

인덱스의 정렬 특성상, 1-2-2 와 유사하게 company_no가 선두컬럼에 존재하므로 전혀 소용없습니다.

inst_dt는 company_no에 의해 정렬되어있다는 점을 이해하면 알 수 있습니다.

1-2-4. Nested Loop Join : seminar_ix3 ( inst_dt ) + company_pk_ix

시나리오입니다.

  1. seminar_ix3(inst_dt) 으로 inst_dt가 desc 정렬된 순서대로 읽으며, PK Clustered Index를 통해 인덱스 value값(Column)을 가져옵니다.
  2. 결합조건에 맞는 레코드를 찾고, 필요한 컬럼들도 가져옵니다.
  3. 이미 정렬된 상태이므로, 정렬 작업이 필요없습니다.
  4. TOP N StopKey Algorithm이 작동합니다.

예상 비용 계산식입니다. (B-Tree의 Degree는 2라고 가정. 정렬 함수 또한 QuickSort라고 가정. 통제불가능 변수 무시)

Expected Time Complexity = Seminar_ix3 * Seminar PK Clustering Index Search * Company PK Clustering                                                                     Index(Degree = 2) Search

                                            = O(N) * O(log 2.5 * 10^7) * O(log 5*10^5)

 

Expected Space Complexity = Seminar_ix3

                                              = O( (8  byte(inst_dt) + 4 byte (PK)) * 2.5 *10^7)

select s.NAME as "제목", 
	s.reg_start_date as "신청시작일자",
	s.reg_end_date as "신청종료일자",
	s.start_date as "시작일자", 
	s.end_date as "종료일자",
	s.price as "금액", 
	s.max_capacity as "최대 인원",
	s.available_seats as "여석",
	c.company_no as "회사고유번호",
	c.name as "회사명",
	case 
		when (s.reg_end_date > now()) then "신청 가능"
		when (s.reg_end_date <= now()) then "신청 마감"
	end as '신청가능여부'
from SEMINAR s use index(seminar_ix3), company c use index(primary)
where c.company_no = s.company_no 
order by s.inst_dt DESC
limit 0,10;

 

0.008초가 걸립니다.

실행계획입니다.

 

시나리오와 일치합니다.

1-2-5. Nested Loop Join : seminar_ix4 ( inst_dt + company_no ) + company_pk_ix  

  1. seminar_ix4(inst_dt + company_no) 으로 inst_dt, company_no가 desc 정렬된 순서대로 읽으며, PK Clustered Index를 통해 인덱스 value값(Column)을 가져옵니다.
  2. 결합조건에 맞는 레코드를 찾고, 필요한 컬럼들도 가져옵니다.
  3. 이미 정렬된 상태이므로, 정렬 작업이 필요없습니다. (유의사항 : company_no가 자동으로 desc 정렬되어 나올것임)
  4. TOP N StopKey Algorithm이 작동합니다.
  5.  

예상 비용 계산식입니다. (B-Tree의 Degree는 2라고 가정. 정렬 함수 또한 QuickSort라고 가정. 통제불가능 변수 무시)

Expected Time Complexity = Seminar_ix4 * Seminar PK Clustering Index Search * Company PK Clustering                                                                     Index(Degree = 2) Search

                                            = O(N) * O(log 2.5 * 10^7) * O(log 5*10^5)

 

Expected Space Complexity = Seminar_ix4

                                              = O( (8  byte(inst_dt) + 4 byte (company_no) + 4 byte (PK)) * 2.5 *10^7)

 

select s.NAME as "제목", 
	s.reg_start_date as "신청시작일자",
	s.reg_end_date as "신청종료일자",
	s.start_date as "시작일자", 
	s.end_date as "종료일자",
	s.price as "금액", 
	s.max_capacity as "최대 인원",
	s.available_seats as "여석",
	c.company_no as "회사고유번호",
	c.name as "회사명",
	case 
		when (s.reg_end_date > now()) then "신청 가능"
		when (s.reg_end_date <= now()) then "신청 마감"
	end as '신청가능여부'
from SEMINAR s use index(seminar_ix4), company c use index(primary)
where c.company_no = s.company_no 
order by s.inst_dt DESC
limit 0,10;

 

실행시간은 0.003sec가 소요되었습니다.

시나리오와 일치합니다.

즉, 즉, company_no의 고유번호가 클수록 우선순위가 높습니다. 

1-2-6. Sort Merge Join : No Index

Sort Merge Join은 매번 모든 쿼리를 Sort하는 과정이 수반됩니다. 앞에서 살펴보았듯이, 2500만개 x 50만개 데이터를 Sort하고 반환한다면, 절대 빠를 수 없습니다. 즉, 부분범위처리가 불가합니다.

1-2-7. Hash Join : No Index

이 방식은 수행빈도가 매우 높은 쿼리(OLTP) 환경에서는 좋지 않습니다. Hash 함수가 일회성으로 사용되고 종료되기 떄문입니다.

이 방식이 사용되기 위해서는, NLJ 보다 압도적 효율을 가지고 있어야하는데 이미 1-2-5 시나리오에서 보이듯 해당 방식보다 압도적 속도를 가지는건 어렵습니다. 즉, 부분범위처리가 불가합니다.

1-3.최종선택

최종선택할 방식은 1-2-4. Nested Loop Join : seminar_ix3 ( inst_dt ) + company_pk_ix 방식입니다. 

선택한 측면은 Time Complexity와 Space Complexity 2가지 측면을 기준으로 선택했습니다.

- Time Complexity = O(N) * O(log 2.5 * 10^7) * O(log 5*10^5) 로써, 레코드 개수는 통제불가능한 변수이기에 우리가 통제 가능한 N값으로만 시간복잡도가 결정됩니다. 

만약 O(2.5 * 10^7) * O(log 2.5 * 10^7) * O(log 5*10^5)  과 O(N) * O(log 2.5 * 10^7) * O(log 5*10^5) 의 효율을 비교해보면,O( 2.5 * 10^7 ) / O ( N = 한 페이지당 개수 )만큼 우리가 선정한 방식이 빠릅니다. 심지어 이 방식은 Using FIleSort가 실행된 1-2-1, 1-2-2 의 FileSort 방식과 비교한 값이 아닙니다.

 

- Space Complexity = O( (8  byte(inst_dt) + 4 byte (PK)) * 2.5 *10^7) 로써, 적정한 수준입니다.

 

현재로써는, 하나의 쿼리만 분석했지만 더 많은 검색 쿼리를 추가하며 한개의 테이블에 5개 정도의 인덱스를 구성하며 가장 적합한 인덱스 구성을 찾아나갈 예정입니다.

+ Recent posts