MongoDB Indexing

MongoDB Indexing

index : 提升query 性能,但是会有额外的cost 因为对于写操作需要维护index。 因此对于read-intensive 的应用,index 会有很大帮助。

Data structure

B-Tree: search, sequential access, inserts, and deletes to be performed in logarithmic time.

Another interesting property of a B-tree is that it is self-balancing - 高度小

Screen Shot 2020-11-05 at 10.45.48 AM

Single field Index

最常用的index 类型

db.books.createIndex( { price: 1 } ) ascending

db.books.createIndex( { price: -1 } ) descending

该索引使用: exact match or range query

Compound Index

db.books.createIndex({"name": 1, "isbn": 1})

Sorting in compound index

compound index 多个字段 的 ascending 和 descending 应当一致

prefix indexing

顺序重要- 左匹配

db.books.createIndex({"name": 1, "isbn": 1}) 如果按照name 或者 name+isbn 查询都可以从index 获益, 但是如果查询isdn 则无法使用该idnex。

常用的index 种类

unique index

类似于RMDBs的unique , 防止index filed 存在duplicate value.

db.users.createIndex({username: 1}, {unique: true})

// 如果违反报如下错误
E11000 duplicate key error index:
gardening.users.$username_1 dup key: { : "kbanker" }

TTL

Time to live indexes are used to automatically delete documents after an expiration time.

createIndex( { "created_at_date": 1 }, { expireAfterSeconds: 86400 } )

The created_at_date field values have to be either a date or an array of dates (the earliest one will be used)

sparse index

Indexes are dense by default.

使用场景: 如果某个field不是在所有doc中都存在。

multikey index

对于内容为数组的filed

{
name: "Wheelbarrow",
tags: ["tools", "gardening", "soil"]
}

db.colcreateIndex({"tags":1})

db.col.find({tags:"tools"})

hashed index

index 字段首先通过hash function做hash

db.recipes.createIndex({recipe_name: 'hashed'})

hashed index is ideal for equality matches but cannot work with range queries.

Hashed indexes are used internally by MongoDB for hashed-based sharding : the entries in a hashed index are evenly distributed.

Eg: If your shard index is based on an increasing value, such as a MongoDB OIDs,[7] then new documents created will only be inserted to a single shard—unless the index is hashed.

跟shariding 有关,防止doc 都聚集在同一个shard ,对于heavy write 可以确保doc 均匀分布在不同sharding


Index management

best scenario: index 在空库添加,这样index 可以incremental build。

exception : 对于数据迁移或者是对已经存在数据的db或collection 创建index, index build 是比较expensive的操作(sorting + index creation ),对于大数据量的index build 可能会花很久 而且index build 很难被kill 掉。

check index: getIndexes() or getIndexSpecs()

eg paymentdb. session

db.session.createIndex({ created: 1 }, { expireAfterSeconds: 1800 });

db.session.getIndexes();
[
{
"v" : 2,
"key" : {
"_id" : 1
},
"name" : "_id_",
"ns" : "paymentdb.session"
},
{
"v" : 2,
"key" : {
"created" : 1
},
"name" : "created_1",
"ns" : "paymentdb.session",
"expireAfterSeconds" : 1800
}
]
db.lock.createIndex( { "expireAt": 1 }, { expireAfterSeconds: 3600 } )
db.lock.createIndex( { "name": 1 }, { unique: true } )

db.lock.getIndexes();
[
{
"v" : 2,
"key" : {
"_id" : 1
},
"name" : "_id_",
"ns" : "paymentdb.lock"
},
{
"v" : 2,
"key" : {
"expireAt" : 1
},
"name" : "expireAt_1",
"ns" : "paymentdb.lock",
"expireAfterSeconds" : 3600
},
{
"v" : 2,
"unique" : true,
"key" : {
"name" : 1
},
"name" : "name_1",
"ns" : "paymentdb.lock"
}
]

options:

  • background index
  • offline index

background index

createIndex( { name: 1 }, { background: true } )

index 可以通过mongoshell 或者driver 来管理, 默认的index build 是foreground, 会blocking 其他操作

Although the index build will still take a write lock, the job will yield to allow other readers and writers to access the database. 更好的做法选在 wirite load 比较小的时候 去做 backgroud index build

offline build index

  • Stop one secondary from the replica set
  • Restart it as a standalone server in a different port
  • Build the index from the shell as a standalone index
  • Restart the secondary in the replica set
  • Allow the secondary to catch up with the primary

query optiomization

主要是先定位slow queries,然后查看原因,最后是action

Check Appendix for sample data

1. 定位slow query

Using the db.setProfilingLevel(2) most verbose level; it directs the profiler to log every read and write.

db.setProfilingLevel(1, 50), 记录query时间超过特定阈值(50ms)

可以从 system.profile (capped collection) 查找

db.system.profile.find({millis: {$gt: 150}})

db.system.profile.find().sort({$natural: -1}).limit(1).pretty()\


{
"op" : "query",
"ns" : "stocks.values",
"command" : {
"find" : "values",
"filter" : {
"stock_symbol" : "GOOG"
},
"limit" : 1,
"singleBatch" : false,
"sort" : {
"date" : -1
},
"lsid" : {
"id" : UUID("977c3ec4-8307-4026-ab68-d5c28ff5744f")
},
"$db" : "stocks"
},
"keysExamined" : 0,
"docsExamined" : 4308303,
"hasSortStage" : true,
"cursorExhausted" : true,
"numYield" : 33721,
"nreturned" : 1,
"locks" : {
"Global" : {
"acquireCount" : {
"r" : NumberLong(33722)
}
},
"Database" : {
"acquireCount" : {
"r" : NumberLong(33722)
}
},
"Collection" : {
"acquireCount" : {
"r" : NumberLong(33722)
}
}
},
"responseLength" : 279,
"protocol" : "op_msg",
"millis" : 3206,
"planSummary" : "COLLSCAN",
"execStats" : {
"stage" : "SORT",
"nReturned" : 1,
"executionTimeMillisEstimate" : 2627,
"works" : 4308308,
"advanced" : 1,
"needTime" : 4308306,
"needYield" : 0,
"saveState" : 33721,
"restoreState" : 33721,
"isEOF" : 1,
"invalidates" : 0,
"sortPattern" : {
"date" : -1
},
"memUsage" : 182,
"memLimit" : 33554432,
"limitAmount" : 1,
"inputStage" : {
"stage" : "SORT_KEY_GENERATOR",
"nReturned" : 894,
"executionTimeMillisEstimate" : 2562,
"works" : 4308306,
"advanced" : 894,
"needTime" : 4307411,
"needYield" : 0,
"saveState" : 33721,
"restoreState" : 33721,
"isEOF" : 1,
"invalidates" : 0,
"inputStage" : {
"stage" : "COLLSCAN",
"filter" : {
"stock_symbol" : {
"$eq" : "GOOG"
}
},
"nReturned" : 894,
"executionTimeMillisEstimate" : 2467,
"works" : 4308305,
"advanced" : 894,
"needTime" : 4307410,
"needYield" : 0,
"saveState" : 33721,
"restoreState" : 33721,
"isEOF" : 1,
"invalidates" : 0,
"direction" : "forward",
"docsExamined" : 4308303
}
}
},
"ts" : ISODate("2020-11-05T12:21:48.197Z"),
"client" : "127.0.0.1",
"appName" : "MongoDB Shell",
"allUsers" : [ ],
"user" : ""
}

Key fields:

  • docsExamined: The number of documents in the collection that MongoDB scanned in order to carry out the operation.
  • nreturned: The number of documents returned by the operation.

Ref: https://docs.mongodb.com/manual/reference/database-profiler/

2. examing slow queries

db.values.find({"stock_symbol": "GOOG"}).sort({date: -1}).limit(1).explain("executionStats")

{
"queryPlanner" : {
"plannerVersion" : 1,
"namespace" : "stocks.values",
"indexFilterSet" : false,
"parsedQuery" : {
"stock_symbol" : {
"$eq" : "GOOG"
}
},
"winningPlan" : {
"stage" : "SORT",
"sortPattern" : {
"date" : -1
},
"limitAmount" : 1,
"inputStage" : {
"stage" : "SORT_KEY_GENERATOR",
"inputStage" : {
"stage" : "COLLSCAN",
"filter" : {
"stock_symbol" : {
"$eq" : "GOOG"
}
},
"direction" : "forward"
}
}
},
"rejectedPlans" : [ ]
},
"executionStats" : {
"executionSuccess" : true,
"nReturned" : 1,
"executionTimeMillis" : 7897,
"totalKeysExamined" : 0,
"totalDocsExamined" : 4308303,
"executionStages" : {
"stage" : "SORT",
"nReturned" : 1,
"executionTimeMillisEstimate" : 6555,
"works" : 4308308,
"advanced" : 1,
"needTime" : 4308306,
"needYield" : 0,
"saveState" : 33763,
"restoreState" : 33763,
"isEOF" : 1,
"invalidates" : 0,
"sortPattern" : {
"date" : -1
},
"memUsage" : 182,
"memLimit" : 33554432,
"limitAmount" : 1,
"inputStage" : {
"stage" : "SORT_KEY_GENERATOR",
"nReturned" : 894,
"executionTimeMillisEstimate" : 6445,
"works" : 4308306,
"advanced" : 894,
"needTime" : 4307411,
"needYield" : 0,
"saveState" : 33763,
"restoreState" : 33763,
"isEOF" : 1,
"invalidates" : 0,
"inputStage" : {
"stage" : "COLLSCAN",
"filter" : {
"stock_symbol" : {
"$eq" : "GOOG"
}
},
"nReturned" : 894,
"executionTimeMillisEstimate" : 6343,
"works" : 4308305,
"advanced" : 894,
"needTime" : 4307410,
"needYield" : 0,
"saveState" : 33763,
"restoreState" : 33763,
"isEOF" : 1,
"invalidates" : 0,
"direction" : "forward",
"docsExamined" : 4308303
}
}
}
},
"serverInfo" : {
"host" : "seashells-mbp.cn.ibm.com",
"port" : 27017,
"version" : "4.0.5",
"gitVersion" : "3739429dd92b92d1b0ab120911a23d50bf03c412"
},
"ok" : 1
}

最主要的就是returned 和 examined 应该是相近的数字。

3. actions

最简单的场景是添加index,或者调整index,调整data model etc...

db.values.createIndex({close: 1})

======
2020-11-05T22:17:08.172+0800 I INDEX [conn5] build index on: stocks.values properties: { v: 2, key: { close: 1.0 }, name: "close_1", ns: "stocks.values" }
2020-11-05T22:17:08.172+0800 I INDEX [conn5] building index using bulk method; build may temporarily use up to 500 megabytes of RAM
2020-11-05T22:17:11.000+0800 I - [conn5] Index Build: 1049100/4308303 24%
2020-11-05T22:17:14.000+0800 I - [conn5] Index Build: 2862800/4308303 66%
2020-11-05T22:17:20.932+0800 I INDEX [conn5] build index done. scanned 4308303 total records. 12 secs
======

db.values.find({}).sort({close: -1}).limit(1).explain("executionStats")

{
"queryPlanner" : {
"plannerVersion" : 1,
"namespace" : "stocks.values",
"indexFilterSet" : false,
"parsedQuery" : {

},
"winningPlan" : {
"stage" : "LIMIT",
"limitAmount" : 1,
"inputStage" : {
"stage" : "FETCH",
"inputStage" : {
"stage" : "IXSCAN",
"keyPattern" : {
"close" : 1
},
"indexName" : "close_1",
"isMultiKey" : false,
"multiKeyPaths" : {
"close" : [ ]
},
"isUnique" : false,
"isSparse" : false,
"isPartial" : false,
"indexVersion" : 2,
"direction" : "backward",
"indexBounds" : {
"close" : [
"[MaxKey, MinKey]"
]
}
}
}
},
"rejectedPlans" : [ ]
},
"executionStats" : {
"executionSuccess" : true,
"nReturned" : 1,
"executionTimeMillis" : 3,
"totalKeysExamined" : 1,
"totalDocsExamined" : 1,
"executionStages" : {
"stage" : "LIMIT",
"nReturned" : 1,
"executionTimeMillisEstimate" : 0,
"works" : 2,
"advanced" : 1,
"needTime" : 0,
"needYield" : 0,
"saveState" : 0,
"restoreState" : 0,
"isEOF" : 1,
"invalidates" : 0,
"limitAmount" : 1,
"inputStage" : {
"stage" : "FETCH",
"nReturned" : 1,
"executionTimeMillisEstimate" : 0,
"works" : 1,
"advanced" : 1,
"needTime" : 0,
"needYield" : 0,
"saveState" : 0,
"restoreState" : 0,
"isEOF" : 0,
"invalidates" : 0,
"docsExamined" : 1,
"alreadyHasObj" : 0,
"inputStage" : {
"stage" : "IXSCAN",
"nReturned" : 1,
"executionTimeMillisEstimate" : 0,
"works" : 1,
"advanced" : 1,
"needTime" : 0,
"needYield" : 0,
"saveState" : 0,
"restoreState" : 0,
"isEOF" : 0,
"invalidates" : 0,
"keyPattern" : {
"close" : 1
},
"indexName" : "close_1",
"isMultiKey" : false,
"multiKeyPaths" : {
"close" : [ ]
},
"isUnique" : false,
"isSparse" : false,
"isPartial" : false,
"indexVersion" : 2,
"direction" : "backward",
"indexBounds" : {
"close" : [
"[MaxKey, MinKey]"
]
},
"keysExamined" : 1,
"seeks" : 1,
"dupsTested" : 0,
"dupsDropped" : 0,
"seenInvalidated" : 0
}
}
}
},
"serverInfo" : {
"host" : "seashells-mbp.cn.ibm.com",
"port" : 27017,
"version" : "4.0.5",
"gitVersion" : "3739429dd92b92d1b0ab120911a23d50bf03c412"
},
"ok" : 1
}

> db.values.getIndexes();
[
{
"v" : 2,
"key" : {
"_id" : 1
},
"name" : "_id_",
"ns" : "stocks.values"
},
{
"v" : 2,
"key" : {
"close" : 1
},
"name" : "close_1",
"ns" : "stocks.values"
}
]

query optimizer

Query optimizer is the piece of software that determines which index, if any, will most efficiently serve a given query.

Optimizer chooses the index that requires scanning the least number of index entries.

When the query is first run, the optimizer creates a query plan for each index that might efficiently satisfy the query. The optimizer then runs each plan in parallel. Usually, the plan that finishes with the lowest value for nscanned is declared the winner; but in rare occasions, the optimizer may select the full collection scan as the winning plan for a given query. The optimizer then halts any long-running plans and saves the winner for future use.


Appendix

启动mongo, Reset and restart MongoDB

rm -rf ~/Env/mongodb-osx-x86_64-4.0.5/data/db/ && mkdir -p ~/Env/mongodb-osx-x86_64-4.0.5/data/db && mongod --dbpath ~/Env/mongodb-osx-x86_64-4.0.5/data/db

mongorestore

mongorestore -d stocks stocks

Index

db.values.getIndexes();
[
{
"v" : 2,
"key" : {
"_id" : 1
},
"name" : "_id_",
"ns" : "stocks.values"
}
]

Sample

> db.values.count();
4308303

> db.values.find()
{ "_id" : ObjectId("4d094f58c96767d7a0099d49"), "exchange" : "NASDAQ", "stock_symbol" : "AACC", "date" : "2008-03-07", "open" : 8.4, "high" : 8.75, "low" : 8.08, "close" : 8.55, "volume" : 275800, "adj close" : 8.55 }
{ "_id" : ObjectId("4d094f58c96767d7a0099d4a"), "exchange" : "NASDAQ", "stock_symbol" : "AACC", "date" : "2008-03-06", "open" : 9.03, "high" : 9.03, "low" : 8.41, "close" : 8.56, "volume" : 353600, "adj close" : 8.56 }
{ "_id" : ObjectId("4d094f58c96767d7a0099d4b"), "exchange" : "NASDAQ", "stock_symbol" : "AACC", "date" : "2008-03-05", "open" : 9.12, "high" : 9.17, "low" : 8.85, "close" : 9.12, "volume" : 156200, "adj close" : 9.12 }
{ "_id" : ObjectId("4d094f58c96767d7a0099d4c"), "exchange" : "NASDAQ", "stock_symbol" : "AACC", "date" : "2008-03-04", "open" : 9.05, "high" : 9.14, "low" : 8.73, "close" : 9.09, "volume" : 420700, "adj close" : 9.09 }
{ "_id" : ObjectId("4d094f58c96767d7a0099d4d"), "exchange" : "NASDAQ", "stock_symbol" : "AACC", "date" : "2008-03-03", "open" : 9.68, "high" : 9.69, "low" : 8.98, "close" : 9.15, "volume" : 407200, "adj close" : 9.15 }
{ "_id" : ObjectId("4d094f58c96767d7a0099d4e"), "exchange" : "NASDAQ", "stock_symbol" : "AACC", "date" : "2008-02-29", "open" : 9.52, "high" : 9.76, "low" : 9.25, "close" : 9.75, "volume" : 269400, "adj close" : 9.75 }
{ "_id" : ObjectId("4d094f58c96767d7a0099d4f"), "exchange" : "NASDAQ", "stock_symbol" : "AACC", "date" : "2008-02-28", "open" : 9.7, "high" : 10.1, "low" : 9.67, "close" : 9.7, "volume" : 150200, "adj close" : 9.7 }
{ "_id" : ObjectId("4d094f58c96767d7a0099d50"), "exchange" : "NASDAQ", "stock_symbol" : "AACC", "date" : "2008-02-27", "open" : 9.8, "high" : 10.25, "low" : 9.58, "close" : 9.76, "volume" : 190700, "adj close" : 9.76 }
{ "_id" : ObjectId("4d094f58c96767d7a0099d51"), "exchange" : "NASDAQ", "stock_symbol" : "AACC", "date" : "2008-02-26", "open" : 9.4, "high" : 9.94, "low" : 9.25, "close" : 9.91, "volume" : 205100, "adj close" : 9.91 }
{ "_id" : ObjectId("4d094f58c96767d7a0099d52"), "exchange" : "NASDAQ", "stock_symbol" : "AACC", "date" : "2008-02-25", "open" : 9.84, "high" : 10, "low" : 9.37, "close" : 9.79, "volume" : 352200, "adj close" : 9.79 }
{ "_id" : ObjectId("4d094f58c96767d7a0099d53"), "exchange" : "NASDAQ", "stock_symbol" : "AACC", "date" : "2008-02-22", "open" : 10.19, "high" : 10.19, "low" : 9.75, "close" : 10, "volume" : 305400, "adj close" : 10 }
{ "_id" : ObjectId("4d094f58c96767d7a0099d54"), "exchange" : "NASDAQ", "stock_symbol" : "AACC", "date" : "2008-02-21", "open" : 10.42, "high" : 10.53, "low" : 10.25, "close" : 10.42, "volume" : 126100, "adj close" : 10.42 }
{ "_id" : ObjectId("4d094f58c96767d7a0099d55"), "exchange" : "NASDAQ", "stock_symbol" : "AACC", "date" : "2008-02-20", "open" : 9.91, "high" : 10.49, "low" : 9.91, "close" : 10.33, "volume" : 99700, "adj close" : 10.33 }
{ "_id" : ObjectId("4d094f58c96767d7a0099d56"), "exchange" : "NASDAQ", "stock_symbol" : "AACC", "date" : "2008-02-19", "open" : 10.31, "high" : 10.4, "low" : 9.94, "close" : 10.01, "volume" : 170100, "adj close" : 10.01 }
{ "_id" : ObjectId("4d094f58c96767d7a0099d57"), "exchange" : "NASDAQ", "stock_symbol" : "AACC", "date" : "2008-02-15", "open" : 10.08, "high" : 10.29, "low" : 9.93, "close" : 10.15, "volume" : 259700, "adj close" : 10.15 }