[Mongo] MongoDB Shell 指令操作(Shell CLI)
# 啟動 MongoDB Shell
$ mongosh
# 查看指令
> help # 檢視和 shell 有關的指令
> db.help() # 檢視和 db 有關的指令
> db.[collection].help() # 檢視和 collections 有關的指令
基本觀念
-
NoSQL: not only SQL
-
階層關係:
- database:在 MongoDB server 中可以有多個 database(DB)
- collection: 用來整理組織 document,類似 SQL 中的 table。一個 db 中可以有多個 collections。
- document: 個別的紀錄(record),類似 SQL 中的 row。一個 collections 中可以有多個 documents
- collection: 用來整理組織 document,類似 SQL 中的 table。一個 db 中可以有多個 collections。
- database:在 MongoDB server 中可以有多個 database(DB)
-
MongoDB 將資料儲存成 BSON 的文件,BSON 是一種以二進位(binary)方式來表徵 JSON 的方法
- 在操作 MongoDB 時,我們提供的是 JSON,MongoDB 的 drivers 會把這個 JSON 轉成對應的 BSON 在保存在 MongoDB 中
- BSON 可以包含比 JSON 更多的資料類型
- BSON 的儲存效率更高
-
MongoDB 的原子性(Atomicity)是建立在 Document Level,也就是說,一個 Document 要嘛有被完整保存成功,要嘛失敗完全沒存,不會有只存了「部分 Document」的情況。
當你使用 insertMany()
或 updateMany()
這類方法一次改動多個 documents 時,是可能會發生部分失敗的情況。但要 留意的是,這裡的部分失敗指的是這個「操作」,而不是「document」。如果需要確保整個操作的原子性,需要使用 transaction。
基本指令
> help # 檢視常用指令
> show dbs # 檢視目前所有的 database
> show collections # 檢視當前 db 有的 collections
> db # 顯示當前所在資料庫
> quit # 離開
資料庫相關
db # 顯示當前所在資料庫
show dbs # 顯示現有資料庫(database)
show collections # 顯示當前資料庫內所有的 collections
use [DatabaseName] # 使用此資料庫,如果該資料庫不存在則自動「建立」並使用
##
# 針對 database
##
db.getCollectionNames() # 顯示當前資料庫中的 collection 名稱
db.dropDatabase() # 刪除資料庫
##
# 針對 collection
##
db.myCollection.drop() # 刪除 myCollection 這個 collection
- 在 MongoDB 中,如果該 database 不存在,MongoDB 會在第一次儲存資料到該 database 時建立該 database。
CREATE
##
# Create
# db.collection.insertOne({})
# db.collection.insertMany([{}, {}])
##
db.todos.insertOne({ title: 'Hello Mongo' }); # 建立 document
Ordered Behavior
預設的情況下,MongoDB 使用的是 ordered behavior,在新增資料時如果發生錯誤(例如,duplicate key error):
- 它不會有 rollback 的行為,成功寫進去的資料不會因為其中一個資料寫不進去而被還原
- 預設當
ordered
是true
時,一旦發生錯誤,在錯誤發生後的資料就不會繼續寫入。如果希望它能夠忽略掉這個錯誤,繼續寫入後面的資料,則可以把orderer
設成false
db.hobbies.insertMany(
[
{
_id: 'hiking',
name: 'hiking',
},
{
_id: 'yoga',
name: 'yoga',
},
{
_id: 'cooking',
name: 'cooking',
},
],
{ ordered: false },
);
Write Concern
Write Concern 的目的是確保 MongoDB 對寫操作的確認程度(acknowledgement level)。它允許用戶指定在寫操作被認為成功之前,MongoDB 必須接收到的確認數量。這可以確保數據的耐久性(durability)和一致性(availability),例如在單一伺服器、複寫集(replica sets)或分片集群(shared clusters)中,提供不同程度的數據安全保障。較高的 Write Concern 能增加數據安全性,但可能會降低效能。
如同這張圖所示:
MongoDB 在把資料寫進資料庫時,為了效能,實際上還有一個 Memory 存在;另外,需要的話,還可以寫一個實體的 Journal 檔,如此,一旦資料庫發生錯誤,只要有寫了 Journal 檔,還是可以把沒有寫入到資料庫中的資料復原。
透過 Write Concern 就可以設定這個行為:
{ w: <value>, j: <boolean>, wtimeout: <number> }
w
- Durability:預設的情況下是
w: majority
,表示大部分的 replica set 需要確認完成寫的東西後,才算成功。當資料是不可遺失的話,可以設定更高的 write concerns。 - Performance:如果你的使用情境非常重視寫入的效能,並且能夠容忍可能的資料流失,則可以把 write concern 設成
w: 1
或w: 0
。
- Durability:預設的情況下是
j
:用於指定寫操作在資料寫入日誌(journal)後才會得到確認(acknowledge)。日誌是一種預寫日誌(write-ahead log),用來確保在錯誤發生時數據的完整性- 當
j
是true
時,表示 MongoDB 確保寫操作只有在數據成功寫入日誌(on-disk journal)後才會被確認,但相對的會犧牲效能
- 當
wtimeout
:單位是 ms,目的是避免w
設定的節點無限期等待
當使用情境不同的,例如在 Standalone 或 Replica Sets 時,w
和 j
會有不同的行為,詳細可以參考官方文件的說明。
不等待資料是否寫進 memory 中:
db.hobbies.insertOne('writing', { writeConcern: { w: 1, j: true, wtimeout: 200 } });
// {
// acknowledged: false,
// insertedId: ObjectId('66782454b1ed5d2a0fbe0dce')
// }
READ
keywords: find()
, findOne()
, count()
, distinct()
/**
* Read
* db.collection.find()
* db.collection.findOne()
* db.collection.countDocuments()
* db.collection.estimatedDocumentCount()
* db.collection.distinct()
*/
db.todos.countDocuments(); // todos 中有多少 documents
db.todos.estimatedDocumentCount();
db.todos.find(); // 檢視某一 Collections 內所有的 documents
db.todos.find({ title: 'Create new Node course' }); // 檢視特定 document
db.todos.find({ _id: ObjectId('5c0beffdeb866557d13c0ef8') }); // 檢視特定 document
db.todos.find().pretty(); // 以格式化的方式列出 document
db.todos.find().limit(2); // 指列出前兩筆 document
db.todos.find().skip(2); // 忽略前兩筆資料
db.todos.find().sort({ title: 1 }); // 排序資料,1 是 ascending,-1 是 descending
db.todos.find().toArray();
db.todos.find().forEach();
Basic Query
Query Embedded Documents (Nested Object)
可以在 key 的地方使用 "x.y.z"
,就可以搜尋 embedded documents(nested objects)
db.todos.find({ 'status.description': 'on-time' });
db.movies.find({ 'rating.average': { $gte: 6.7 } });
Query Elements in an Array
如果要找的是 array 中的元素:
// Query 尋找陣列中的資料
// 假設 hobbies 是 array,我們想要找 hobbies 中包含 'sports' 的 document
// hobbies: ['sports', 'video game']
db.todos.find({ hobbies: 'sports' });
上面的 query 會找到在陣列中「包含」有該元素的資料,如果你想要找到是在該陣列中「只有那個元素」,則要使用 hobbies: ["sports"]
。
如果 array 中的元素是 array of object,例如:
# history: [
# { disease: 'headache', treatment: 'aspirin' },
# { disease: 'fever', treatment: 'ibuprofen' }
# ]
如果我們希望找到 history 中有 disease 是 'headache'
的元素,可以使用:
db.patientData.find({ 'history.disease': 'headache' });
當如果你希望找到的是 history
中有完全相同的物件,則是使用:
// 可以用類似的方式來 query
db.patientData.find({ history: { disease: 'headache', treatment: 'aspirin' } });
Array Query Operator
$elemMatch
假設想要找出在 hobbies
array 中,有 { title: 'Sports' }
,且 { frequency: value }
的 value
大於等於 3 的元素。
如果我們使用 $and
並不能得到我們想要的結果:
db.users.find({
// 這個 query 會找到 hobbies 陣列中有 title 為 Sports 且 frequency 大於等於 3 的 documents
// 但不表示這個條件會在同一個 element 中同時被滿足,舉例來說,以下的文件就會被找到
// { hobbies: [{ title: 'Sports', frequency: 2}, { title: 'Yoga', frequency: 3 }] }
$and: [{ 'hobbies.title': 'Sports' }, { 'hobbies.frequency': { $gte: 3 } }],
});
正確的做法是要用 $elemMatch
:
// 這樣才能正確找出 hobbies 陣列中,同時 title 是 Sports frequency 大於等於 3 的 documents
db.users.find({
hobbies: { $elemMatch: { title: 'Sports', frequency: { $gte: 3 } } },
});
Query Operator
Comparison Operators
keywords: $eq
, $gt
, $gte
, $lt
, $lte
, $ne
, $in
, $nin
// $eq: equal(預設沒帶 operator 的話就是這個)
// $gt: greater than
// $gte: greater than or equal to
// $lt: less than
// $lte: less than or equal to
// $ne: not equal
db.todos.find({ priority: { $gt: 2 } });
db.movies.find({ runtime: { $ne: 60 } });
db.movies.find({ runtime: { $in: [] } });
// $in 和 $nin operator
db.todos.find({ status: { $in: ['A', 'D'] } }); // status 的值是「'A' 或 'D'」都符合
db.movies.find({ runtime: { $in: [30, 42] } }); // runtime 的值是「30 或 42」都符合
db.movies.find({ runtime: { $nin: [30, 42] } }); // runtime 的值不是「30 或 42」都符合
Logical Operators
keywords: $or
, $nor
, $and
// $or operator
db.todos.find({
$or: [{ title: 'Create new Node course' }, { title: 'Learn NodeJS' }],
});
db.movies.find({
$or: [{ 'rating.average': { $lt: 5 } }, { 'rating.average': { $gt: 9.3 } }],
});
// $nor operator:表示不是 xxx 也不是 xxx
db.movies.find({
$nor: [{ 'rating.average': { $lt: 5 } }, { 'rating.average': { $gt: 9.3 } }],
});
// $and operator,預設不指定就是 $and
db.movies.find({
$and: [{ 'rating.average': { $gt: 9 } }, { genres: 'Drama' }],
});
// 等同於
db.movies.find({ 'rating.average': { $gt: 9 }, genres: 'Drama' });
之所以會需要 $and
operator,是因為在 JavaScript 中,物件中不能有重複的 key。舉例來說,如果我們想要找出 genres
這個 array 中同時有 Horror
和 Drama
這兩個 elements 的:
// ❌ JavaScript 並不能這樣寫,因為當物件中有相同的 key,後面的會改掉前面的
db.movies.find({ genres: 'Horror', genres: 'Drama' });
// 所以上面的寫法實際上等同於
db.movies.find({ genres: 'Drama' });
// 如果要達到原本的目的,就要用 $and operator
db.movies.find({ $and: [{ genres: 'Horror' }, { genres: 'Drama' }] });
但如果是在 Golang 的 mongo driver 中,不用 $and
即可可以達到原本的目的:
filter := bson.D{{"genres", "Drama"}, {"genres", "Horror"}}
Element Operators
keywords: $type
, $exist
// 找出所有 age 是 null 的資料
db.users.find({ age: { $type: 'null' } });
// 找出所有 age 欄位存在的資料(欄位值是 null 也算存在)
db.users.find({ age: { $exists: true } });
Projection:SELECT fields
針對物件
// Projection(類似 SQL 的 SELECT,只選出有需要用到的欄位,確保效能)
db.todos.find({}, { title: true }); // 會顯示 _id 和 title
db.todos.find({}, { _id: false, title: true }); // 只會顯示 title
db.todos.find({}, { title: false }); // 除了 title 其他都會顯示
db.todos.find({}, { title: true, tag: false }); // 錯誤的語法
針對 Array
// 找出 color 陣列中包含 "black" 或 "white" 的 product
// 在輸出的結果中,只要顯示 color 陣列中第一個符合查詢的元素
db.products.find({ color: { $in: ['black', 'white'] } }, { 'color.$': true });
// 使用 slice 可以指定只顯示陣列中的前幾個元素
// 找出所有 price > 299 的 documents color 陣列中的前兩個元素
db.products.find({ price: { $gt: 299 } }, { color: { $slice: 2 } });
Cursor
當我們使用 find()
時,MongoDB 實際上會回給我們一個 cursor,這是基於效能考量,因為我們並不一定需要所有 find 到的東西,如果一次全部拉出來,都放到記憶體中,會造成很多資源的浪費。
在 Mongo Shell 中,預設會以 batch 的方式,一次顯示 20 筆資料。如果有需要的話,可以透過 DBQuery.shellBatchSize
修改。
// 在 mongosh 中,如果把 find() 回傳的 cursor 保存起來的話
// 它就不會自動 iterate,而要手動 iterate
var cursor = db.users.find();
cursor.hasNext();
cursor.next();
printjson(cursor);
myCursor.objsLeftInBatch();
sort
// 1: ASC
// -1: DESC
db.products.find().sort({ 'spec.ram': -1 });
// 先用 RAM 排序,再用 price 排序
db.products.find().sort({ 'spec.ram': -1, price: 1 });
skip and limit
db.products.find().skip(2).limit(3);
UPDATE
/**
* Update
* db.collection.updateOne({}, {}, {upsert: false})
* db.collection.updateMany({}, {}, {upsert: false})
* 第一個參數的物件稱作 update filter,類似 find,找出符合條件的「documents」
* 第二個參數的物件稱作 update action,透過 $set 可以更新內容
* 第三個參數的物件稱作 options,透過 upsert 可以新增不存在的資料,預設是 false
*/
db.users.updateMany(
{ 'hobbies.title': 'Sports' },
{ $set: { isSporty: true } },
{ upsert: true }
);
Upsert 的意思就是,如果「找不到」就「建立新的」。
Field Update Operator
$set
db.todos.updateOne(
{ _id: ObjectId('5c0beffdeb866557d13c0ef8') },
// $set 會添加新的、或覆蓋原有欄位
{ $set: { tag: 'course', title: 'Learn Node JS' } },
);
$inc:increment
db.users.updateOne(
{ name: 'Manuel' },
{
// 把 age 這個欄位的 value +2
// 如果要 decrement 就用負數,例如 -2
$inc: { age: 2 },
// 可以同時搭配 $set 使用
$set: { isSporty: true },
},
);
$min, $max
db.users.updateOne(
{ name: 'Manuel' },
{
// 如果欄位 age 的值大於 30,則將 age 欄位的值設定為 30
// 如果欄位 age 的值小於等於 30,則不更新
$min: {
age: 30,
},
},
);
$mul
db.users.updateOne(
{ name: 'Manuel' },
{
// 將欄位 age 的值 * 1.1
$mul: {
age: 1.1,
},
},
);
$unset:把欄位移除
如果想要移除某個欄位,需要使用 $unset
operator:
// 如果 age 的欄位是 null,則移除這個欄位
db.users.updateMany({ age: { $type: 'null' } }, { $unset: { age: '' } });
$rename:修改欄位名稱
db.users.updateMany(
{},
// 把所有名為 age 的欄位名稱都改成 totalAge
{ $rename: { age: 'totalAge' } },
);
Array Update Operator
array.$:更新陣列中的 firstMatch
用 $
來指稱 match 到的「第一個」(first match)陣列元素中的位置(index/position)。
舉例來說,我們想要找出 hobbies
陣列中,title
為 Sports
,且 frequency 大於等於 3
的 documents。它的 query 會像這樣:
db.users.find({
hobbies: { $elemMatch: { title: 'Sports', frequency: { $gte: 3 } } },
});
針對被找出來的陣列元素,我們想要「加上一個新的 isHealth
的欄位」,這時候可以這樣寫:
db.users.updateMany(
{
hobbies: { $elemMatch: { title: 'Sports', frequency: { $gte: 3 } } },
},
{
// 在符合條件的 hobbies 陣列元素中,添加一個 isHealth 欄位,並設定為 true
$set: { 'hobbies.$.isHealth': true },
},
);
db.users.updateMany(
{
hobbies: { $elemMatch: { title: 'Sports', frequency: { $gte: 3 } } },
},
{
// 以新的物件,把符合條件的陣列元素取代掉
$set: { 'hobbies.$': { title: 'Running', frequency: 4 } },
},
);
$[]:更新陣列中的所有元素
// 找出所有 age 存在的 documents
// 接著把這些 documents 中 hobbies 陣列裡,元素中的 frequency 都加 1
db.users.updateMany({ age: { $exists: true } }, { $inc: { 'hobbies.$[].frequency': 1 } });