Elasticsearch reindex시 alias를 사용하여 무중단으로 진행하기 & big index 리인덱싱 시 비동기 처리 방법

Elasticsearch reindex를 진행할 때, 단순하게 새로운 인덱스를 만들고 reindex api를 진행하고 기존 인덱스를 지우고 새로 만들어서 다시 reindex를 해줬다. (이전글: https://wedul.site/611?category=680504)

하지만 그것은 해당 인덱스의 document의 수가 적어서 금방 진행이 되었었고 만약 document수가 10만가지만 넘어도 생각보다 오래걸려서 서비스의 흐름이 끊어지게 된다는걸 인지하지 못했다. 같은 회사 동료분께서 해당 부분에 대해서 말씀해주셨고, 그 분이 가이드 해주신대로 진행해서 reindex를 무중단하게 진행하는 방법을 찾아봤다.

 

Alias를 이용하여 reindex하기


기존 index wedul의 매핑구조이다.

PUT wedul 
{
  "mappings": {
    "dynamic": false,
    "properties": {
      "name": {
        "type": "text"
      }
    }
  }
}

해당 인덱스의 데이터는 현재 다음과 같이 들어있는 것을 볼 수 있다. 여기서 age는 매핑이 안되어있어서 검색에 잡을 수 없기에 이를 reindex를 통해 매핑 정보를 업데이트해주자.

wedul 인덱스에 들어있는 데이터(왼), age로 검색이 안됨 (우)

그럼 reindex를 위해 새로운 인덱스 wedul_v1을 만들어보자.

reindex를 진행할 새로운 index, wedul_v1

그리고 wedul_v1으로 reindex를 실행해준다. 이때 주의사항이 있는데 document양이 10만 이상이 넘어가게 되면 작업이 오래걸리기에 kibana에서 504 gateway timeout이 발생하고 작업이 중단된다. 그래서 해당 작업을 비동기로 실행시키는 옵션인 wait_for_completion=false를 함께 설정해주고 진행해야한다.

POST _reindex?wait_for_completion=false
{
  "source": {
    "index": "wedul"
  },
  "dest": {
    "index": "wedul_v1"
  }
}

그럼 위에 이미지처럼 task 프로세스 번호가 나오고 이 프로세스에 시작시간 상태 취소 가능여부 등등을 GET _task 명령어를 통해 볼 수 있다. 여기서 프로세스가 종료되면 reindex가 다 된것이다.

그 다음 wedul_v1에 wedul이라는 alias를 지정해줘야한다. 

POST _aliases
{
  "actions": [
    {
      "add": {
        "index": "wedul_v1",
        "alias": "wedul"
      }
    }
  ]
}

alias를 지정하기 전에 기존 인덱스 wedul을 지워줘야한다. DELETE wedul 명령어를 날려서 기존 인덱스를 지우고 위의 alias 명령어를 실행시킨다. 

그럼 정상적으로 alias를 통해 무중단 reindex를 실행되었다. 정상적으로 실행 되었는지 age에 대한 query를 날려보자.

ㅋㅋ 정상적으로 실행되었다.

앞으로 이런 방식으로 진행해야겠다.

 

출처 : https://discuss.elastic.co/t/reindex-big-index/83047

 

Reindex big index

I would like to reindex a very big index. When I run reindex API with elasticsearchjs client I will receive the requestTimeout error, or Gateway timeout error. It's ok because the reindex process is still running in Elastic server. However, what I want to

discuss.elastic.co

https://www.elastic.co/kr/blog/changing-mapping-with-zero-downtime

댓글()

Elasticsearch에서 Dictionary 변경 시 analyzer와 인덱싱된 Document 갱신 방법

Elasticsearch에서 Dictionary를 사용하여 analyzer를 만들고 그를 사용해서 index에 Document를 인덱싱할 수 있다. 근데 Dictionary가 변경되면 analyzer를 변경하고 indexing된 document를 갱신하려면 어떻게 해야하는지 정리해보자.

Background 지식


Analyzer는 character filter, tokenizer, token filter 순서대로 적용한다. 기본적으로 anaylyzer는 indexing time과 search time에 적용된다. index time 분석 대상은 source data(원본 데이터)이고 search time 분석 대상은 query string이다. 그러므로 사전을 변경하는 것은 indexing, serching 두개 모두 영항을 준다.

사전 업데이트 방법


엘라스틱서치에서 analyzer는 index가 close/open될 때 사전을 읽는다. 그리고 일반적으로 로딩된 이후로는 다시 사전을 읽어 들이지 않는다. 그러므로 수정된 사전을 업데이트 하기 위해서는 dictionary file을 가지고 있는 node를 재시작하거나 index를 _close, _open해야한다.

예를들어 위메프라고 형태소를 나눴을 때, 위메프라는 명사를 알지 못해 다음과 같이 쪼개진다.

GET nori_sample/_analyze
{
"analyzer": "my_analyzer",
"text" : "위메프"
}

anaylze api 결과

그럼 명사라는걸 알려주기 위해서 사전에 위메프를 추가해보자.

그리고 다시 검색을 해보자.
하지만 결과는 처음과 같다. 위에 말한 것 처럼 반영해주기 위해서는 node를 재시작하거나 index를 닫았다가 열어야한다.

다시 검색한 검색결과는 똑같다.

그럼 index를 _close했다가 _open해보자.

POST nori_sample/_close
POST nori_sample/_open


그리고 결과를 다시 확인하면 잘 구분된걸 확인할 수 있다.

정상적으로 변경된 Dictionary가 반영된걸 볼 수있다.

하지만 이 방식으로 사전 업데이트는 이미 인덱싱된 document에는 적용되지는 않는다. 왜냐하면 document는 사전이 업데이트 되기전에 analyzer를 사용해서 인뎅싱 되기 때문이다. 그래서 사전이 업데이트 되었다고해서 사전이 적용되어서 검색결과가 변경되어 나오지는 않는다.

그럼 어떻게 변경된 사전정보를 이미 존재하는 indices에 적용할 수 있을까?

엘라스틱서치에서는 인덱스된 document가 업데이트 되었을 때, document는 제거되고 다시 생성된다. 이때 우리가 업데이트한 사전정보를 이용해서 document가 다시 인덱싱된다. 그렇기 때문에 update by query api를 사용하여 인덱스에 모든 정보를 업데이트해야한다.

update by query 사용방법은 다음과 같다.

update by query 사용방법

 https://www.elastic.co/guide/en/elasticsearch/reference/7.0/docs-update-by-query.html

 

 

출처 : https://www.elastic.co/kr/blog/dictionary-update-behavior-for-elasticsearch-cjk-language-analyzers

댓글()
  1. 최중한 2019.09.04 16:53 댓글주소  수정/삭제  댓글쓰기

    안녕하세요 글 잘봤습니다:)
    궁금한 것이 있는데요, 이 작업을 할 때 리소스를 많이 써야 해서 검색 서비스에 영향을 줄 수도 있다고 하는데
    이걸 회피하는 방법은 어떤 것이 있는 지 궁금합니다.

Elasticsearch에서 reindex를 이용해서 매핑정보 변경하기

Elasticsearch에서 index를 구성하다보면 매핑정보를 추가하거나 수정하고 싶을때가 있다.  내가 아는 내에서는 한번 생성된 index의 매핑정보를 변경하는건 어렵다. 그래서 reindex를 통해 index의 매핑정보를 변경해줘야한다.

우선 wedul_mapping이라는 인덱스가 있다고 해보자.
매핑 정보는 다음과 같다.

PUT wedul_mapping
{
  "mappings": {
    "_doc": {
      "dynamic": "false",
      "properties": {
        "id": {
          "type": "integer"
        },
        "name": {
          "type": "text",
          "fields": {
            "keyword": {
              "type": "keyword"
            }
          }
        }
      }
    }
  }
}

이때 name에서 keyword필드를 제거하고 age라는 새로운 Integer타입의 필드를 매핑하고 싶은 경우에 wedul_mapping_dump라는 새로운 임시 인덱스를 생성한다.

PUT wedul_mapping_dump
{
  "mappings": {
    "_doc": {
      "dynamic": "false",
      "properties": {
        "id": {
          "type": "integer"
        },
        "name": {
          "type": "text"
        },
        "age": {
          "type": "integer"
        }
      }
    }
  }
}

그리고 기존 wedul_mapping인데스에서 wedul_mapping_dump 인덱스로 reindex를 실행한다.

POST _reindex
{
  "source": {
    "index": "wedul_mapping"
  },
  "dest": {
    "index": "wedul_mapping_dump"
  }
}

 

그럼 데이터가 모두 변경된 인덱스 wedul_mapping_dump로 복사되면서 매핑정보가 변경된것을 알 수 있다. 그리고 이름이 같은 wedul_mapping인덱스에 다시 옮기려면 wedul_mapping인덱스를 제거하고 변경된 매핑정보로 새로 생성한뒤 다시한번 reindex를 해주면된다. 데이터가 많은 실 환경에서는 reindex 작업의 비용이 크기 때문에 한번 매핑정보를 설정할 때 잘해주는것이 좋을 것 같다.

댓글()

Elasticsearch에서 refresh 정리

Elasticsearch에서 document를 업데이트하고 바로 해당 정보를 조회하려고 했다.

하지만 조회가 되지 않았다. 분명이 업데이트가 종료된 것을 확인 했는데 왜 그런지 의문이 들었다.


그래서 찾아봤는데 document가 업데이트가 되고나서 인덱스에서 실제로 조회가 될 수있는 상태가 되기위해서는 일정시간이 필요한 것 같다.

자세히는 모르지만 다시 인덱싱을 걸기 때문에 그러는건 아닌가 생각된다.


그래서 이런경우에 업데이트가 종료 되었다고 알리는 시간을 검색이 가능하게 변경된 시간까지 포함해서 알려주도록 하는 옵션이 존재한다.


그렇게 되면 업데이트가 되고 검색이 가능한줄 알고 프로그램을 작성하다가 버그가 발생하는 비율을 줄일 수 있다.


일반적인 bulkInsert나 update, create같은 명령에는 refresh: wait_for를 사용하고 update by query 등에 명령어에서는 waitForCompletion=true 옵션을 부여하여 검색을 사용한다.


예시

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
client.bulk({
refresh: wait_for,
  body: [
    // action description
    { index:  { _index: 'myindex', _type: 'mytype', _id: 1 } },
     // the document to index
    { title: 'foo' },
    // action description
    { update: { _index: 'myindex', _type: 'mytype', _id: 2 } },
    // the document to update
    { doc: { title: 'foo' } },
    // action description
    { delete: { _index: 'myindex', _type: 'mytype', _id: 3 } },
    // no document needed for this delete
  ]
}, function (err, resp) {
  // ...
});
cs

api 문서를 보면 조금 더 정확한 내용을 확인할 수 있다.


참고

https://stackoverflow.com/questions/40676324/elasticsearch-updates-are-not-immediate-how-do-you-wait-for-elasticsearch-to-fi

https://www.elastic.co/guide/en/elasticsearch/client/javascript-api/current/api-reference.html



주의

하지만 이렇게 프로그램을 작성할 경우에는 예상하는 바와 같이 엄청난 시간이 소요된다.

실제로 이번에 프로젝트를 진행할 때 해당 옵션없이 업데이트를 할 때 5분걸렸던 양이 1시간이 되어도 되지 않았다. 이럴 경우에는 하나하나 다 update 명령을 사용하지말고 bulk로 작업하는 것을 추천한다. 훨씬빠르다.



댓글()

Elasticsearch에서 Paging시 max_result_window 초과시 조회가 안되는 이슈

엘라스틱 서치에서 데이터를 paging 하여 조회할때 from과 size를 사용한다. 

from은 시작 지점을 이야기하고 size는 그 시작 지점으로 부터 몇 개의 데이터를 보여주어야 하는 건지 설정할 때 사용 되는 값이다. 그래서 계산 방법은 다음과 같다.

from : (page - 1) * size 

size : size


그럼  만약 3개씩 보여주는 페이지에서 2번째 페이지를 보여주기 위해서는 from은 3, size는 3으로 설정하면 된다.

1
2
3
4
5
6
7
8
9
GET wedul/_search
{
  "from": 3, 
  "size": 3, 
  "query": {
    "match_all": {}
  }
}
 
cs


그럼 만약 wedul 페이지를 접근하다가 다음과 같이 Document의 숫자가 10000을 넘어가게 되면 어떻게 될까? 쿼리를 사용해서 조회를 해보자.

1
2
3
4
5
6
7
8
9
GET wedul/_search
{
  "from": 9999, 
  "size": 3, 
  "query": {
    "match_all": {}
  }
}
 
cs


정상적인 결과가 나오지 않고 query_phase_execution_exception에러가 발생하고 다음과 같은 에러문구를 출력한다.

1
Result window is too large, from + size must be less than or equal to: [10000] but was [10002]. See the scroll api for a more efficient way to request large data sets. This limit can be set by changing the [index.max_result_window] index level setting.
cs


이유가 몰까? 이유를 잘 몰라서 엘라스틱 서치 공식홈페이지 문서를 뒤져봤다. 

엘라스틱서치에서 인덱스를 생성할 때 기본적으로 하나의 노드에 5 섯개의 shard가 생성되고 그 shard는 데이터를 나누어서 저장한다. 물론 그 데이터를 복제하고 있는 primary shard도 1로 설정되는데 이는 primary shard가 한개라는 뜻이 아니라 각 shard 마다 복제 shard가 한 개씩 존재한다는 의미이다, 

다시 본점으로 돌아가서 노드에 분리되어있는 파티션 shard들에 데이터가 분산되어 들어간다. 그래서 만약 5섯개의 shard가 있는 노드에 위치한 index에서 1 ~ 10 개의 데이터를 찾는다면 각 shard에서 10개의 데이터를 찾고 모아서 정렬작업을 한 후 50개의 데이터에서 1 ~ 10까지의 데이터를  반환한다.

그럼 10000개의 데이터라면? 각 shard에서 10000개의 데이터를 가져와서 모으고 그것을 정렬할 것이다. 그래서 총 50000개 이상의 데이터를 모아야 하고 그것을 정렬해야하기 때문에 성능적인 문제를 야기할 수 있다. 그래서 엘라스틱서치의 기본 검색 제한 document의 값은 10000이고 이 설정값 이름은 max_result_window이다.

이 값은 아래의 쿼리를 사용해서 원하는 대로 50000까지 설정할 수 있다. 하지만 근본적으로 10000을 넘게 조회하게되면 많은 리소스 사용으로 성능문제를 야기할 수 있기 때문에 함부로 설정값을 바꿀것이아니라 검색을 10000개가 한번에 되지 않도록 검색조건을 잘 분할해서 지정해야한다.

1
2
3
4
PUT your_index_name/_settings
  "max_result_window" : 500000 
}
cs


참조 

https://www.elastic.co/guide/en/elasticsearch/guide/current/pagination.html

https://www.elastic.co/guide/en/elasticsearch/guide/current/_fetch_phase.html

댓글()

elasticsearch multi type 기능 제거 이슈

elasticsearch는 인덱스 하나의 여러 type을 제공했다. 예를 들면 twitter라는 인덱스에 user와 tweet 두개의 타입을 가질 수도 있고 그 안에 Document들이 저장된다.

각각의 타입에 들어있는 properties에는 서로 다른 타입의 것과 독립적으로 보이기 때문에 각 타입에 같은 이름의 properties를 사용할 수 있다. 

예를 들면 user type 필드에도 user_name을 만들고, tweet 필드에도 user_name을 만들 수있다.

그리고 각 type에 들어있는 document에는 _id 속성이 있는데 이는 각 타입별로 만들어지기 때문에 서로 다른 타입에 _id는 같은 값을 가질 수도 있다.

위의 내용까지는 얼추 알던내용인데 왜 7.0부터는 이 편한 multi type을 제거하려고 하는걸까? 우리는 공부할 때 엘라스틱 서치와 RDBMS와의 관계를 다음과 같이 정의 했었다.

하지만 어쩌면 이는 잘못된 설명이었다. 왜냐하면 Type과 매칭되는 Table은 각 table끼리 완전 개별적인 관계이기 때문에 각 테이블에 이름이 같은 컬럼이 있어도 상관이 없다. 하지만 사실 elasticsearch에서 서로다른 type간에 같은 이름이 존재하는 것은 이와 다른다.

왜냐하면 서로 다른 mapping type에 field는 내부적으로 같은 Lucene 필드를 사용한다. 다시 말하자면 위에서 예로들었던 user 타입의 user_name 필드는 tweet 유형의 user_name 필드와 정확히 동일한 필드에 저장되며 두 user_name 필드는 두 유형 모두에서 동일한 매핑 (정의)을 가져야한다.

즉 그렇기 때문에 서로 다른 타입의 동일한 이름을 가진 필드는 서로 관련이 있다는 뜻이다. 이런 문제로 필드를 지우거나 문서를 압축하거나 하는 등등에서 문제가 발생할 수 있다고 한다. 그래서 이를 제거하기로 하였고 6버전부터 차츰 기능을 빼고 있으면 7버전부터는 완전하게 제거한다고 한다.



대안

이렇게 multi type이 제거되고 index하나에 one type만 제공된다면 어떻게 기존의 방식을 처리하라는 뜻인가?

간단하게 하나의 인덱스에 하나의 타입만 사용해서 이 문제를 만들지 말자는 것 같다. 만약 위의 예에서 user와 tweet 타입을 별도의 인덱스를 사용해서 지정하는 것이다. 그러면 위와 같은 multi type 문제가 발생하지 않고 완전 개별적인 공간이라서 Data의 밀집도가 높아져서 루신에서 압축과 검색기능이 더 효율적으로 동작할거라는 설명이다.

나도 아직 엘라스틱 서치를 사용해본지 일주일밖에 되지 않아서 자세히는 모르지만 대강 무슨뜻인지는 알 것 같다. 자세한 내용은 아래 링크를 들어가서 확인해보면 더 쉽게 이해할 수 있다.



참고자료

https://www.elastic.co/guide/en/elasticsearch/reference/current/removal-of-types.html



댓글()

Docker Container에 Elasticsearch와 데이터 시각화 kibana 설치 및 연동

회사에서 사용하는 Elasticsearch 공부를 위해서 docker에 설치해보고 시각화에 도움주는 Kibana도 같이 설치해보자.

우선 Elasticsearch에 대한 기본 정보는 API 문서에서 확인할 수 있다.
https://www.elastic.co/guide/kr/elasticsearch/reference/current/gs-index-query.html


Elasticsearch 설치

해당 이미지에는 xpack도 포함되어있다. xpack은 보안, 알림, 모니터링, 보고, 그래프 기능을 설치하기 편리한 단일 패키지로 번들 구성한 Elastic Stack 확장 프로그램이다.


우선 이미지를 내려받는다.

1
docker pull docker.elastic.co/elasticsearch/elasticsearch-platinum:6.0.0
cs

그리고 내려받은 이미지를 이용하여 Elasticsearch를 conatiner에 올려서 실행시킨다.

1
docker run --9200:9200 -9300:9300 -"discovery.type=single-node" -"transport.host=127.0.0.1" ---name elastic docker.elastic.co/elasticsearch/elasticsearch-platinum:6.0.0 && sleep 20
cs

그리고 xpack 설치를 진행하기 위해서 우선 해당 컨테이너 bash 쉘을 실행시킨다.

1
2
// bash shell 열기
docker exec -it elastic /bin/bash
cs

그리고 xpack을 설치한다.

1
bin/elasticsearch-plugin install x-pack
cs


마지막으로 Elasticsearch에서 자동 색인 생성을 비활성화 해준경우에 xpack에서 다음 색인을 생성할 수 있도록 elasticsearch.yml에 설정해준다.

1
action.auto_create_index: .security,.monitoring*,.watches,.triggered_watches,.watcher-history*
cs


그러고 http://localhost:9200에 들어가면 정상적으로 설치된것을 확인할 수 있다. (계정입력하는 화면이 나오면 elastic / changeme 정보를 이용해서 사용한다.

Kibana 설치

Docker 이미지 다운

1
docker pull docker.elastic.co/kibana/kibana:6.0.0
cs

container에 이미지 올리기

1
docker run -d --rm --link dazzling_mayer:elastic-url -e "ELASTICSEARCH_URL=http://elastic-url:9200" -p 5601:5601 --name kibana docker.elastic.co/kibana/kibana:6.0.0 && sleep 20
cs


Index 추가


처음 접속하면 elasticsearch에서 사용하는 index의 이름을 입력하라고 한다. 패턴을 입력하면 되는데 만약 날짜마다 인덱스가 생성되는 구조면 밑에 TimeFilter를 설정을 같이해주고 그게 아니라면 customer* 이런식으로만 지정해줘도 된다.


댓글()

Mysql 실행계획 설명

데이터베이스/mysql|2018. 10. 3. 23:43

프로그램의 성능을 높히기 위해서는 DB튜닝이 필요하다. 
Mysql에서 튜닝을 하기 위해서 제공하는 쿼리의 실행 계획에 대해 정리해보자.



Mysql의 데이터 처리 방식

우선 Mysql의 데이터 처리방식에 대해 정리해보자.

- Mysql은 단일 코어로 데이터를 처리하기 때문에 멀티코어로 scale out을 진행하는 것 보다 cpu 자체의 성능을 높히는 scale up을 하는 것이 더 효율적이다. 
- Oracle과 달리 mysql은 nested loop join 알고리즘만 사용한다.  
- Nested Loop Join은 선행 테이블의 검색 결과 값 하나하나 테이블 B와 조인하는 방식이다. 그래서 데이터 양이 적을 때는 상관이 없으나 데이터가 많은 테이블끼리 조인할 시 성능에 문제가 있을 수 있다. 그래서 내부적으로 join buffer를 사용하여 드라이빙 테이블에서 조인에 사용될 데이터를 찾아 join buffer에 채우고 조인 버퍼에서 조인 대상 B 테이블의 데이터를 스캔하면서 풀, 인덱스 스캔, 인덱스 범위 스캔등을 사용하여 테이블에 데이터와 조인한다. 



Mysql 쿼리 성능 진단 (for 최적화)
성능 진단을 위해서 사용하는 방법은 Explain을 사용하는 것이다.  

Explain을 사용해서 쿼리 실행계획을 살펴보면 하단에 그림과 같이 출력된다.


각 필드에 대한 설명은 다음과 같다. 
ID : Select 아이디 
Select_type : 참조 타입 
Table : 참조하는 테이블 
Type : 조인 타입 
Possible_keys : 데이터를 조회할 때 DB에서 사용할 수 있는 인덱스 리스트 
Key : 실제로 사용할 인덱스 
Key_len : 실제로 사용할 인덱스 길이 
Ref : Key 안의 인덱스와 비교하는 컬럼(상수) 
Rows : 쿼리 실행 시 조사하는 행 수 
Extra : 추가 정보 

이 필드중에 Select_type, type, Extra에 대해서만 잘 확인하면 좋은 쿼리를 작성할 수 있다.


Select_type 종류

구분
설명
예시
SIMPLE
UNION이나 서브쿼리가 없는 단순 SELECT를 의미한다. 
SELECT * FROM USER;
PRIMARY 
서브쿼리가 있을 때 가장 바깥쪽에 있는 SELECT 
SELECT * FROM (SELECT * FROM USER) t; 
DERIVED 
FROM절 안의 서브쿼리 
SELECT * FROM (SELECT * FROM USER) t; 
DEPENDENT SUBQUERY 
외부 쿼리와 상호 연관된 서브쿼리 
SELECT * FROM user u1 WHERE EXISTS ( 
    SELECT * FROM user u2 WHERE u1.user_id = u2.user_id 
);


Type
Type에는 system, const, ref... 등등 많이 있지만 성능상 문제가 되는 부분은 index, all이 두가지 타입이 문제다.
구분
설명
index 
인덱스를 처음부터 끝까지 찾아서 검색하는 경우로, 일반적으로 인덱스 풀스캔이라고 지칭 
all 
테이블 풀스캔으로 모든 부분을 스캔하는 것


Extra
쿼리 실행에 대한 추가적인 정보를 보여준다. 
하단의 대표적인 설명인 4가지중에서 특히 FileSort와 Using Temporary의 경우에는 쿼리 튜닝이 필요한 상태
구분
설명
Using Index 
인덱스를 이용해서 데이터를 추출
Using Where 
Where 조건으로 데이터를 추출.  (Type에서 All과 Index와 마찬가지로 성능에 문제) 
Using Filesort 
데이터의 정렬이 필요한 경우로써 데이터 양이 많을수록 성능에 직접적인 영향을 끼친다. 
Using Temporary 
내부적으로 Temporary Table을 사용하는 경우


Join 최적화 포인트
- Nested Loop 조인으로 되어있기 때문에 기준 테이블에서 조회되는 데이터양에 따라 연관 테이블의 데이터양이 결정되기 때문에 기준 테이블(왼쪽)의 데이터양을 줄이는 것이 관건. 
- Outer Join은 지양한다. 꼭 필요한 경우만 사용한다. 
- join시 조합 경우의 수를 줄이기 위해 복합 컬럼 index를 사용.


댓글()