0%

mongoDB 基礎2


Geospatial Data

Geo json架構

  • 必須是一個stucture結構
  • cordinate的第一個屬性是經度,第二個為緯度
1
2
//type與coordinate是一定要有的
db.places.insertOne({name: "Califorina Academy", location: {type: "Point", coordinates: [-122.4724356, 37.7672544]}})

使用find找出與現在位置接近的點資料

  • $near: 是mongodb提供給geospatial data的一種方法,必須配合geospatial index
1
2
3
4
5
db.places.createIndex({location: "2dsphere"}) //2dsphere是特別給geo資料用的index型態
//以下query會找到上面所加的點,$geometry代表後面接的是一個geo json結構的資料
db.places.find({location: {$near: {$geometry: {type: "Point", coordinates: [-122.47114, 37.771104]}}})
//為了讓query變的更有意義,可以加一些參數。$maxDistance代表離自己最遠幾公尺內,#minDistance代表至少要離自己幾公尺
db.places.find({location: {$near: {$geometry: {type: "Point", coordinates: [-122.47114, 37.771104]}, $maxDistance: 500, $minDistance: 10}})

找出一個面積內所包含的點資料

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//首先再加入更多的點資料,包含上面的資料前三個資料在Golden Gate Park裡面,最後一個在外面
db.places.insertOne({name: "Conservatory of Flowers", location: {type: "Point", coordinates: [-122.4615748, 37.7701756]}})
db.places.insertOne({name: "Golden Gate Tennis Park", location: {type: "Point", coordinates: [-122.4593702, 37.7705046]}})
db.places.insertOne({name: "Nopa", location: {type: "Point", coordinates: [-122.4389058, 37.7747415]}})
//接著利用googlemap抓出圍住公園的四個點
const p1 = [-122.4547, 37.77473]
const p2 = [-122.45303, 37.76641]
const p3 = [-122.51026, 37.76411]
const p4 = [-122.51088, 37.77131]
//$geoWithin特別用來搜尋elements是否在一個特定的面之內。coordinates內是一個array再包一個array後,裡面再包住含經緯度的array
//$geoWithin不要求index(有的話可以提升query速度),但是有用到$near就需要有index
//起始點p1要再重複一次代表一個完整的Polygon已經結束
//以下的query將會找出有在公園內的點(也就是除了Nova以外的點)
db.places.find({location: {$geoWithin: {$geometry: {type: "Polygon", coordinates: [[p1, p2, p3, p4, p1]]}}}})

確認一個點資料(user)是否在一個特定的範圍內

1
2
3
4
5
6
7
//首先把剛剛公園的area資料加入資料庫
db.areas.insertOne({name: "Golden Gate Park", area: {type: "Polygon", coordinates: [[p1, p2, p3, p4, p1]]}})
//在下query之前必須先加入index
db.areas.createIndex({area: "2dsphere"})
//$geoIntersect可以找出與特定area有相交的點或面。以下query會回傳Golden Gate Park,如果該點與多個面有相交,那就會回傳所有的面
//不只可以query點對面,也可以面對面
db.areas.find({area: {$geoIntersects: {$geometry: {type: "Point", coordinates: [-122.49089, 37.76992]}}}})

尋找存在特定方圓內的所有點

1
2
3
//$certerSphere可以指定一個點和一個距離半徑,此query會把距離此點的方圓半徑內的點找出來
//距離要自行從公尺或英里轉換成radians(弧度),可以參考官方文件,以下範例是用公里轉換
db.places.find({location: {$geoWithin: {$centerSphere: [[-122.46203, 37.77286], 1/6378.1]}}})

Aggregation Pipeline Stages

  • 使用aggregate方法我們可以將多個query條件組合在一起
  • 前一個條件的輸出將會變成後面條件的輸入,可以使用的method可以參考官網

  • $group: 此方法可以根據documents的某一個field進行分群,裡面可以定義需要的field。group將多個document合而為一
  • $project: 此方法可以選擇需要哪些field被傳到下一個pipeline,可以是已經存在的field或是自己定義新的field。project是一對一的關係
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
//以下query會先過濾出gender是female的document,再根據group創造出新的輸出
//aggregate外面是一個陣列,每個stage為一個structure
db.persons.aggregate([
{ $match: { gender: 'female' } },
{ $group: { _id: { state: "$location.state" }, totalPersons: { $sum: 1 } } }
]).pretty();
//回傳如下
{ "_id" : { "state" : "berkshire" }, "totalPersons" : 1 }
{ "_id" : { "state" : "indre-et-loire" }, "totalPersons" : 1 }
{ "_id" : { "state" : "loiret" }, "totalPersons" : 1 }
//....省略
//加入sort,可以針對自己產生的field進行排序
db.persons.aggregate([
{ $match: { gender: 'female' } },
{ $group: { _id: { state: "$location.state" }, totalPersons: { $sum: 1 } } },
{ $sort: {totalPersons: -1}}
]).pretty();
//回傳如下
{ "_id" : { "state" : "midtjylland" }, "totalPersons" : 33 }
{ "_id" : { "state" : "nordjylland" }, "totalPersons" : 27 }
{ "_id" : { "state" : "syddanmark" }, "totalPersons" : 24 }
//....省略

db.persons.aggregate([
{
$project: { _id: 0, gender: 1,
fullName: {
//$concat可以連接字串,需要用一個array將要連接的字串包起來
//$toUpper可以將字母小寫轉成大寫
$concat: [{ $toUpper: "$name.first" }, " ", { $toUpper: "$name.last" }]
} } }
]).pretty()
//回傳如下
{ "gender" : "male", "fullName" : "VICTOR PEDERSEN" }
{ "gender" : "male", "fullName" : "CARL JACOBS" }
{ "gender" : "male", "fullName" : "ZACHARY LO" }
//如果只需要轉換名字的第一個字母為大小,可以使用更複雜的操作
//把$name.first拆成第一個字母和剩下的字母,只針對第一個字母做大小寫轉換,再把剩下的字串連在一起
//$substrCP可以拆解字串,第一個參數為拆解的對象,第二個參數為從哪個index開始,第三個參數為需要幾個element
//$strLenCP幫助我們得知字串的長度
db.persons.aggregate([
{
$project: {
_id: 0,
gender: 1,
fullName: {
$concat: [
{
$toUpper: {$substrCP: ['$name.first', 0, 1]}},
{
//使用$subtract減1是因為要扣掉第一個字母的長度
$substrCP: ['$name.first', 1, {$subtract: [{ $strLenCP: '$name.first'}, 1]}]
},
" ",
{ $toUpper: "$name.last" }
]
}
}
}
]).pretty()
//回傳如下
{ "gender" : "male", "fullName" : "Victor PEDERSEN" }
{ "gender" : "male", "fullName" : "Carl JACOBS" }
//甚至可以結合兩個$project,後面的project將會使用前面的project的結果當作輸入
db.persons.aggregate([
{
$project:{
_id: 0,
name: 1,//在下一個project會進行上個例子一樣的操作
email: 1,
location:{
type: "Point",
coordinates:[
//convert可以轉換資料型態
{$convert: {input: '$location.coordinates.longitude', to: "double", onError: 0.0, onNull: 0.0}},
{$convert: {input: '$location.coordinates.latitude', to: "double", onError: 0.0, onNull: 0.0}}
]
}
}},{
$project: {
gender: 1,
email: 1,
location: 1,
fullName: { $concat: [{ $toUpper: {$substrCP: ['$name.first', 0, 1]}},
{
$substrCP: ['$name.first', 1, {$subtract: [{ $strLenCP: '$name.first'}, 1]}]
},
" ",{ $toUpper: "$name.last" }] }
}
}
]).pretty()
//回傳如下
{
"location" : {
"type" : "Point",
"coordinates" : [
130.0105,
88.1818
]
},
"email" : "gonca.alnıaçık@example.com",
"fullName" : "Gonca ALNıAçıK"
}
  • $unwind: unwind方法可以接受一個array,並且根據array element的數量將document分割成多個,範例如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
//friends範例裡的資料
{
"_id" : ObjectId("5d64e4156116a09c69665b28"),
"name" : "Max",
"hobbies" : [
"Sports",
"Cooking"
],
"age" : 29,
"examScores" : [
{
"difficulty" : 4,
"score" : 57.9
},
{
"difficulty" : 6,
"score" : 62.1
},
{
"difficulty" : 3,
"score" : 88.5
}
]
}
//使用unwind方法
db.friends.aggregate([{$unwine: "$hobbies"}])
//會變成
{
"_id" : ObjectId("5d64e4156116a09c69665b28"),
"name" : "Max",
"hobbies" : "Sports",
"age" : 29,
"examScores" : [
{
"difficulty" : 4,
"score" : 57.9
},
{
"difficulty" : 6,
"score" : 62.1
},
{
"difficulty" : 3,
"score" : 88.5
}
]
}
{
"_id" : ObjectId("5d64e4156116a09c69665b28"),
"name" : "Max",
"hobbies" : "Cooking",
"age" : 29,
"examScores" : [
{
"difficulty" : 4,
"score" : 57.9
},
{
"difficulty" : 6,
"score" : 62.1
},
{
"difficulty" : 3,
"score" : 88.5
}
]
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
//$push可以將incoming data傳到array裡,所以allHobbies將會是一個array裡面再包多個array(因為hobbies是array)
db.friends.aggregate([
{$group: {_id: {age: "$age"}, allHobbies: {$push: "$hobbies"}}}
])
//結合unwind就可以避免allHobbies裡面是array包array
db.friends.aggregate([
{$unwind: "$hobbies"},
{$group: {_id: {age: "$age"}, allHobbies: {$push: "$hobbies"}}}
])
//但是上面的方法allHobbies可能會有重複的element,可以將push改成addToSet改善這個問題
//$addToSet不會把重複的資料push進array裡
db.friends.aggregate([
{$unwind: "$hobbies"},
{$group: {_id: {age: "$age"}, allHobbies: {$addToSet: "$hobbies"}}}
])

//如果只要擷取array的部分element到新定義的field,可以使用$slice
//$slice接受一個array做為參數,第一個element為要擷取的對象array
//如果$slice接受的array參數只有兩個element,第二個element為從index0開始需要幾個element(正數),或從最後一個element開始需要幾個element(負數)
//如果$slice接受的array參數有三個element,第二個為從index幾開始,第三個為需要幾個element
db.friends.aggregate([
{$project: {_id: 0, examScore: {$slice: ["$examScores", 1]}}}//只會擷取第一個element作為examScore的參數
])

//可以結合project,只顯示array中分數大於特定值得element
db.friends.aggregate([
{$project: {
_id: 0,
//這裡的sc要加兩個$是因為如果只有一個$,mongodb會以為是原始資料的sc field而不是我們自己定義的sr field
scores: {$filter: {input: "$examScores", as: "sc", cond: {$gt: ["$$sc.score", 60]}}}
}}
])
//回傳
{
"scores" : [
{
"difficulty" : 6,
"score" : 62.1
},
{
"difficulty" : 3,
"score" : 88.5
}
]
}
//如果只需要顯示一個人所得到的最高成績,可以用以下方法
db.friends.aggregate([
{$unwind: "examScores"},//先根據成績分開document,這樣一個document就會只擁有一個成績
{$project: {_id: 1, name: 1, age:1 ,score: "$examScores.score"}},//examScores是一個structure,我們只需要score的部分
{$sort: {score: -1}},//成績由大排到小
//$first可以在一個group中使用第一個element的參數當作整個group的某一個field的參數
{$group: {_id: "$_id", name: {$first: "$name"}, maxScore: {$max: "$score"}}},
{$sort: {maxScore: -1}}
])
//回傳如下
{ "_id" : ObjectId("5d64e4156116a09c69665b28"), "name" : "Max", "maxScore" : 88.5 }
{ "_id" : ObjectId("5d64e4156116a09c69665b2a"), "name" : "Maria", "maxScore" : 75.1 }
{ "_id" : ObjectId("5d64e4156116a09c69665b29"), "name" : "Manu", "maxScore" : 74.3 }
  • $bucket: bucket可以讓我們對輸入的資料進行分群,並產生我們需要的field
  • $bucketAuto: 相較於bucket,bucketAuto可以自動幫我們產生group的區間
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
db.persons.aggregate([
{$bucket: {
groupBy: "$dob.age",//定義針對哪個field做分群
boundaries: [0, 18, 30, 50, 80, 120],//分群的區間
output: {
numPerson: {$sum: 1},
averageAge: {$avg: "$dob.age"}
}
}}
])
//回傳如下
{ "_id" : 18, "numPerson" : 868, "averageAge" : 25.101382488479263 }//18-29
{ "_id" : 30, "numPerson" : 1828, "averageAge" : 39.4917943107221 }//30-19
{ "_id" : 50, "numPerson" : 2304, "averageAge" : 61.46440972222222 }//50-79
//使用bucketAuto
db.persons.aggregate([
{$bucketAuto: {
groupBy: "$dob.age",
buckets: 5,//mongodb會自動產生5個區間
output: {
numPerson: {$sum: 1},
averageAge: {$avg: "$dob.age"}
}
}}
])
//回傳如下
"_id" : {
"min" : 21,
"max" : 32
},
"numPerson" : 1042,
"averageAge" : 25.99616122840691
}

"_id" : {
"min" : 32,
"max" : 43
},
"numPerson" : 1010,
"averageAge" : 36.97722772277228
}

"_id" : {
"min" : 43,
"max" : 54
},
"numPerson" : 1033,
"averageAge" : 47.98838334946757
}

"_id" : {
"min" : 54,
"max" : 65
},
"numPerson" : 1064,
"averageAge" : 58.99342105263158
}

"_id" : {
"min" : 65,
"max" : 74
},
"numPerson" : 851,
"averageAge" : 69.11515863689776
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//假設我們要找出最老的5個人
db.persons.aggregate([
{ $project: {_id: 0, name: {$concat: ["$name.first"," ", "$name.lst"]}, birthdate: {$toDate: "$dob.date"}}},
{ $sort: {birthdate: 1}},
{ $limit: 5}
])
//回傳如下
{ "name" : null, "birthdate" : ISODate("1944-09-07T15:52:50Z") }
{ "name" : null, "birthdate" : ISODate("1944-09-12T07:49:20Z") }
{ "name" : null, "birthdate" : ISODate("1944-09-13T14:58:41Z") }
{ "name" : null, "birthdate" : ISODate("1944-09-16T16:03:28Z") }
{ "name" : null, "birthdate" : ISODate("1944-09-17T15:04:13Z") }
//如果要再取得下5筆資料,可以加上skip
db.persons.aggregate([
{ $project: {_id: 0, name: {$concat: ["$name.first"," ", "$name.lst"]}, birthdate: {$toDate: "$dob.date"}}},
{ $sort: {birthdate: 1}},
{ $skip: 5},
{ $limit: 5}
])
  • $out: 將out放在aggregation的最後可以將整個結果存到指定的collection
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
db.persons.aggregate([
{
$project: {
_id: 0, name: 1, email: 1, birthdate: { $toDate: '$dob.date' },
age: "$dob.age",
location: {
type: 'Point',
coordinates: [
{
$convert: {
input: '$location.coordinates.longitude',
to: 'double',
onError: 0.0,
onNull: 0.0
}
},
{
$convert: {
input: '$location.coordinates.latitude',
to: 'double',
onError: 0.0,
onNull: 0.0
}}]}}
},
{
$project: {
gender: 1, email: 1,location: 1, birthdate: 1, age: 1,
fullName: {
$concat: [
{ $toUpper: { $substrCP: ['$name.first', 0, 1] } },
{
$substrCP: [
'$name.first',
1,
{ $subtract: [{ $strLenCP: '$name.first' }, 1] }
]
},
' ',
{ $toUpper: { $substrCP: ['$name.last', 0, 1] } },
{
$substrCP: [
'$name.last',
1,
{ $subtract: [{ $strLenCP: '$name.last' }, 1] }
]}]}}
},
{ $out: "transformedPersons" } //該collection可以是現存的或不存在的
]).pretty();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
//aggregation也可以操作geo資料
//首先必須加入geo index
db.transformPersons.createIndex({location: "2dsphere"})
//$geoNear可以找出離某點最近的其他點,但要注意的是必須在aggregation的第一個stage
db.transformedPersons.aggregate([
{
$geoNear: {
near: {
type: 'Point',
coordinates: [-18.4, -42.8]
},
maxDistance: 1000000,
num: 10,
query: { age: { $gt: 30 } },
distanceField: "distance"//mongodb會幫我們算離該點的距離,所以要指定一個field存距離,名字可以隨意決定
}
}
]).pretty();
//回傳如下(只擷取一個)
"_id" : ObjectId("5d654a7c5b563b45f720b2cd"),
"location" : {
"type" : "Point",
"coordinates" : [
-18.5996,
-42.6128
]
},
"email" : "elijah.lewis@example.com",
"birthdate" : ISODate("1986-03-29T06:40:18Z"),
"age" : 32,
"fullName" : "Elijah Lewis",
"distance" : 26473.52536319881
}

Numeric Data

MongoDB的四種數字型態

  • Intergers (int32): 純整數
  • Longs (int64): 純整數
  • Doubles (64bit): 包含小數。不論有沒有小數,如果沒有特別指定就預設為該型態(因為mongo shell是基於javascript環境)
  • High precisions doubles (128bit): 包含小數
Intergers (int32) Longs (int64) Doubles (64bit) High precisions doubles (128bit)
純整數 純整數 包含小數 包含小數
-2,147,483,648 到 2,147,483,647 -9,223,372,036,854,775,808 到 9,223,372,036,854,775,807 小數不保證精準 小數高精準(34位小數內)
  • 如果在其他language driver,預設可能不是doubles(64bit),例如python的環境預設為(int32)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//age:29會以doubles(64bit)的方式被儲存,實際上在小數點後好幾位並不能保證全是0
db.persons.insertOne({age: 29})
//如果我們要以int(int32)去儲存他,可以這樣做
db.persons.insertOne({age: NumberInt(29)})
db.persons.insertOne({age: NumberInt("29")})//結果與上面一樣
//如果使用int32儲存超過範圍的數字,不會有error但錯誤的內容會被儲存
db.conpanies.insertMany([
{valuation: NumberInt("5000000000")},
{valuation: NumberInt("2147483647")},
{valuation: NumberInt("2147483648")}
])
//使用find察看結果
{ "_id" : ObjectId("5d6602d58a9830cd604283c2"), "valuation" : 705032704 }
{ "_id" : ObjectId("5d6602d58a9830cd604283c3"), "valuation" : 2147483647 }//只有他在範圍內
{ "_id" : ObjectId("5d6602d58a9830cd604283c4"), "valuation" : -2147483648 }
//以下結果會被正常儲存,因為預設的double(64bit)的範圍更大
db.conpanies.insertOne({valuation: 2147483648})
//要注意的是如果要儲存為long,如果傳入的數字不是字串且超過double(64bit)就必須使用字串的方式,因為數字在傳入NumberLong之前還是double
//以下結果會error
db.conpanies.insertOne({valuation: NumberLong(9223372036854775807)})
//以下結果會正常儲存
db.conpanies.insertOne({valuation: NumberLong("9223372036854775807")})
  • 雖然將數字都存這字串就不會有超過大小的問題,但是就不能使用$inc等運算子來做數學運算
1
2
3
4
5
6
db.accounts.insertOne({name: "Max", amount: NumberInt("10")})
//要注意這邊加完後的結果會變成以double的型態儲存,因為int(int32)加上double會變成double的方式儲存
db.accounts.updateMany({}, {$inc: {amount: 10}})
//所以必須以下面方式做加減
db.accounts.updateMany({}, {$inc: {amount: NumberInt("10")}})
//使用find查看結果,amount雖然沒有特別顯示NumberInt(因為預設的顯示方式),但實際上是以NumberInt儲存
  • double不精準的證明
1
2
3
4
5
6
7
8
9
10
11
db.test.insertOne({a: 0.3, b: 0.1})
//如果把a - b會發現不是0.2
db.test.aggregate([{$project: {result: {$subtract: ["$a", "$b"]}}}])
{ "_id" : ObjectId("5d6609c98a9830cd604283c5"), "result" : 0.19999999999999998 }

//使用NumberDecimal可以解決該問題
db.test.insertOne({a: NumberDecimal("0.3"), b: NumberDecimal("0.1")})
//可以得到正確的結果
db.test.aggregate([{$project: {result: {$subtract: ["$a", "$b"]}}}])
{ "_id" : ObjectId("5d660b9e8a9830cd604283c6"), "result" : NumberDecimal("0.2") }
//如果要針對特殊形態做運算,最好都使用目標的型態(雙方一樣的型態),且為了確保轉換正確,使用字串傳入的方式為佳

參考資料

Float vs Double vs Decimal連結
Modelling Number/ Monetary Data in MongoDB連結