데이터베이스 NoSQL 기본 (mongoDB) - mongoDB 인덱스(INDEX)

8. mongoDB 인덱스(INDEX)

  • https://docs.mongodb.com/manual/indexes/
  • SQL index와 개념적으로 동일
  • 즉, 검색을 더 빠르게 수행하고자 만드는 추가적인 data structure
    • index가 없으면 collection scan (컬렉션의 데이터를 하나하나 조회하는 방식) 으로 검색을 하게 됨
  • Document의 필드들에 index 를 걸면, 데이터의 설정한 키 값을 가지고 document들을 가리키는 포인터 값으로 이뤄진 B-Tree 데이터 구조를 만듬
    • B-Tree : Balanced Binary search Tree, Binary Search (이진 검색) 으로 쿼리 속도를 검색 속도 개선

8.1. 기본 인덱스 _id

  • 모든 MongoDB의 컬렉션은 기본적으로 _id 필드에 인덱스가 존재
  • mongodb는 _id 를 기반으로 기본 인덱스 생성

8.2. Single(단일) 필드 인덱스

  • _id 인덱스 외에도, 사용자가 지정 할 수 있는 단일 필드 인덱스
  • 기본 사용 문법(mongodb CLI): db.COLLECTION.createIndex( { 'field': 1 } ) 또는 db.COLLECTION.createIndex( { 'field': -1 } )
    • 키의 값에는 1, -1 둘중의 하나가 올 수 있음
    • 1 : 순방향(ASCENDING), -1 : 역방향(DESCENDING)

8.3. Compound (복합) 필드 인덱스

  • 두개 이상의 필드를 사용하는 인덱스를 복합 인덱스라고 부름
  • 기본 사용 문법(mongodb CLI): db.COLLECTION.createIndex( { 'field1': 1, 'field2': -1} )
  • 인덱스 방향이 성능에 영향을 미침
    • createIndex({a : 1, b : -1})로 생성한 경우
    • db.x.find({a : 1, b : -1}), db.x.find({a : -1, b : 1}) 의 쿼리만 효과를 봄
    • db.x.find({a : 1, b : 1}), db.x.find({a : -1, b : -1}) 의 쿼리는 효과 없음
  • 복합 인덱스의 경우 필드의 개수가 31개를 넘을 수 없음

8.4. Text 인덱스

  • 텍스트 관련 데이터를 효율적으로 쿼리하기 위한 인덱스
  • 기본 사용 문법(mongodb CLI): db.COLLECTION.createIndex( { 'field' : 'text' } )

pymongo에서는 create_index() 메서드를 제공함

8.5. 코드로 작성해보면서 이해하는 인덱스

import pymongo
username = 'davelee'
password = 'korea123'
connection = pymongo.MongoClient('mongodb://%s:%s@www.funcoding.xyz' % (username, password))
db = connection.test_index_db
# test_index_db 에는 어떤 컬렉션이 있을까?
db.collection_names()
Out[6]:
['articles']
# test data 삽입
db.articles.insert_many(
    [
        { "name": "Java Hut", "description": "Coffee and cakes", "ranking": 1 },
        { "name": "Burger Buns", "description": "Java hamburgers", "ranking": 2 },
        { "name": "Coffee Shop", "description": "Just coffee", "ranking": 3 },
        { "name": "Clothes Clothes Clothes", "description": "Discount clothing", "ranking": 4 },
        { "name": "Java Shopping", "description": "Indonesian goods", "ranking": 5 }
    ]
)
Out[10]:
<pymongo.results.InsertManyResult at 0x1069a1e48>
db.articles.drop()

8.6. Single(단일) 필드 인덱스 생성 with pymongo

# text 인덱스 생성 
db.articles.create_index('ranking')
Out[11]:
'ranking_1'
# text 인덱스 확인
db.articles.index_information()
Out[12]:
{'_id_': {'key': [('_id', 1)], 'ns': 'test_index_db.articles', 'v': 2},
 'ranking_1': {'key': [('ranking', 1)],
  'ns': 'test_index_db.articles',
  'v': 2}}
  • "key" which is a list of (key, direction) pairs specifying the index (as passed to create_index()).
  • _id 는 기본 인덱스로 설정, 1 은 pymongo.ASCENDING
  • subject 는 create_index() 로 만들어진 인덱스
    • 키의 1, -1, 'text' 알아보기
      • pymongo.ASCENDING = 1
      • pymongo.DESCENDING = -1
      • pymongo.TEXT = 'text'

8.7. 특정 인덱스 삭제 with pymongo

# index_information() 에 넣어진 'key'의 값을 통째로 정확하게 적어야 삭제 가능
db.articles.drop_index([('ranking', 1)])
# text 인덱스 다시 생성 
db.articles.create_index([('ranking', pymongo.DESCENDING)])
db.articles.index_information()
Out[14]:
{'_id_': {'key': [('_id', 1)], 'ns': 'test_index_db.articles', 'v': 2},
 'ranking_-1': {'key': [('ranking', -1)],
  'ns': 'test_index_db.articles',
  'v': 2}}

8.8. 전체 인덱스 삭제 with pymongo

# index_information() 에 넣어진 'key'의 값을 통째로 정확하게 적어야 삭제 가능
db.articles.drop_indexes()
db.articles.index_information()
Out[16]:
{'_id_': {'key': [('_id', 1)], 'ns': 'test_index_db.articles', 'v': 2}}

8.9. Text 인덱스 생성 with pymongo

db.articles.create_index([('name', pymongo.TEXT)])
Out[17]:
'name_text'
db.articles.index_information()
Out[18]:
{'_id_': {'key': [('_id', 1)], 'ns': 'test_index_db.articles', 'v': 2},
 'name_text': {'default_language': 'english',
  'key': [('_fts', 'text'), ('_ftsx', 1)],
  'language_override': 'language',
  'ns': 'test_index_db.articles',
  'textIndexVersion': 3,
  'v': 2,
  'weights': SON([('name', 1)])}}
db.articles.drop_indexes()

8.10. Compound (복합) 필드 인덱스 생성 with pymongo

db.articles.create_index([('name', pymongo.TEXT), ('ranking', pymongo.ASCENDING)])
Out[20]:
'name_text_ranking_1'
db.articles.index_information()
Out[21]:
{'_id_': {'key': [('_id', 1)], 'ns': 'test_index_db.articles', 'v': 2},
 'name_text_ranking_1': {'default_language': 'english',
  'key': [('_fts', 'text'), ('_ftsx', 1), ('ranking', 1)],
  'language_override': 'language',
  'ns': 'test_index_db.articles',
  'textIndexVersion': 3,
  'v': 2,
  'weights': SON([('name', 1)])}}
  • name_text_ranking_1 : 복합 인덱스는 복합 컬럼으로 검색할 경우에만 의미가 있고, 검색 형태에 따라 성능 개선의 효과가 있을 수도/없을 수도 있음
    • createIndex({a : 1, b : -1})로 생성한 경우
      • db.x.find({a : 1, b : -1}), db.x.find({a : -1, b : 1}) 의 쿼리만 효과를 봄
      • db.x.find({a : 1, b : 1}), db.x.find({a : -1, b : -1}) 의 쿼리는 효과 없음
db.articles.drop_indexes()

8.11. Text 인덱스와 검색 with pymongo

db.articles.create_index([('name', pymongo.TEXT)])
Out[23]:
'name_text'
result = db.articles.find()
for record in result:
    print(record)
{'_id': ObjectId('5a0c189581f640052dfee6b0'), 'name': 'Java Hut', 'description': 'Coffee and cakes', 'ranking': 1}
{'_id': ObjectId('5a0c189581f640052dfee6b1'), 'name': 'Burger Buns', 'description': 'Java hamburgers', 'ranking': 2}
{'_id': ObjectId('5a0c189581f640052dfee6b2'), 'name': 'Coffee Shop', 'description': 'Just coffee', 'ranking': 3}
{'_id': ObjectId('5a0c189581f640052dfee6b3'), 'name': 'Clothes Clothes Clothes', 'description': 'Discount clothing', 'ranking': 4}
{'_id': ObjectId('5a0c189581f640052dfee6b4'), 'name': 'Java Shopping', 'description': 'Indonesian goods', 'ranking': 5}
- 인덱스 데이터에 대해, 텍스트 검색을 하기 위해, $text operator를 사용할 수 있음 - $text operator는 인덱스 텍스트 데이터를 스페이스와 구두점(. ,등)으로 텍스트를 구분하고, 각 텍스트를 OR 로 검색 - 기본적으로 대소문자 구별하지 않음 - name 컬럼이 pymongo.TEXT 로 인덱싱되어, $text operator 로 검색시 name 컬럼만 검색
# $text operator를 사용한 인덱스 텍스트 데이터 검색 ($text operator 는 $search operator 와 함께 사용됨)
result = db.articles.find({'$text' : {'$search' : 'coffee'}})
for record in result:
    print(record)
{'_id': ObjectId('5a0c189581f640052dfee6b2'), 'name': 'Coffee Shop', 'description': 'Just coffee', 'ranking': 3}
# 띄어쓰기가 있는 경우
result = db.articles.find({'$text' : {'$search' : 'java coffee shop'}})
for record in result:
    print(record)
{'_id': ObjectId('5a0c189581f640052dfee6b0'), 'name': 'Java Hut', 'description': 'Coffee and cakes', 'ranking': 1}
{'_id': ObjectId('5a0c189581f640052dfee6b4'), 'name': 'Java Shopping', 'description': 'Indonesian goods', 'ranking': 5}
{'_id': ObjectId('5a0c189581f640052dfee6b2'), 'name': 'Coffee Shop', 'description': 'Just coffee', 'ranking': 3}
# coffee shop 으로 정확한 검색
result = db.articles.find({'$text': {'$search':"\"coffee shop\"" } } )
for record in result:
    print(record)
{'_id': ObjectId('5a0c189581f640052dfee6b2'), 'name': 'Coffee Shop', 'description': 'Just coffee', 'ranking': 3}
# 대소문자 구별 (실제 name의 컬럼값은 Coffee 이므로, 검색이 안됨)
result = db.articles.find({'$text' : {'$search' : 'coffee', '$caseSensitive' : True}})
for record in result:
    print(record)
# 정규표현식 ($text operator 는 $search operator 와 함께 사용됨)
result = db.articles.find({'name' : {'$regex' : 'Cof.+'}})
for record in result:
    print(record)
{'_id': ObjectId('5a0c189581f640052dfee6b2'), 'name': 'Coffee Shop', 'description': 'Just coffee', 'ranking': 3}
# 정규표현식, 실제 컬럼명과 함께 사용 가능 ($text operator 는 $search operator 와 함께 사용됨)
result = db.articles.find({'name' : {'$regex' : 'Cof.+'}})
for record in result:
    print(record)
{'_id': ObjectId('5a0c189581f640052dfee6b2'), 'name': 'Coffee Shop', 'description': 'Just coffee', 'ranking': 3}