前言
關係型數據庫的使用有一套數學理論支撐,數據庫表結構優化其實質就是優化範式。通常在關係型數據庫中使用的第三範式。然而,在非關係型數據庫中,使用第二範式更能夠發揮非關係型數據庫中的優勢。
第二範式需要滿足兩點:一,屬性不重複,二,所有的屬性依賴於主屬性。因此第二範式允許將一次查詢的所有屬性都存在同一張表中。
第三範式需要滿足兩點:一,滿足第二範式,二,屬性不依賴於其它非主屬性,非主屬性之間必須相互獨立,不存在其他的函數關係。例如,學校包含班級,班級包含學生,學生所學課程。如果學生的ID是主屬性,其餘屬性爲非主屬性,那麼該關係中存在着大量的非主屬性之間的依賴,因此在第三範式中,不允許將學校信息,班級信息,學校及課程信息存在一張表裏,必須拆分爲獨立的表,並建立表的關聯關係。
如上圖所示,第三範式的表結構所存儲的數據會比第二範式少很多,避免了冗餘屬性值的存儲。在第三範式的表結構中,更容易對數據進行修改;在查詢方面,符合第三範式的查詢方式更是有嚴密的數學規範。
然而,第二範式更像是第三範式的查詢結果。
無論是關係型數據庫還是非關係型數據庫,在實際應用中,我們都是希望通過第三範式建立數據表,更合理的存儲數據;又希望能夠在查詢時,擁有第二範式的結構,一次性查詢總比聯合查詢快得多。
非關係型Mongo數據庫
在實際應用場景中,大多數業務都具有強關係,因此使用非關係型數據庫也不可避免的使用第三範式建立數據結構。但是在使用聯合查詢時,對其性能還是有很多地方需要考究的。
其中,非關係型數據MongoDB提供了聯合查詢的方式$lookup,而$lookup使用的是類似SQL嵌套連接的查詢。(雖然,對副表的外部建立索引能提高 lookup 性能;但是,lookup 對副表的任何字段的操作(Match, Group 等等)都不會使用索引,當不論是主表或者副表的數據量大時,其連接查詢非常的慢,遠不如關係型數據庫,期待MongoDB 官網的繼續優化吧)
// 方式一:簡單的左連接查詢,連接字段僅爲一個時使用。
db.getCollection("classCol").aggregate([
{
$lookup: {
from: "studentCol",
localField: "_id",
foreignField: "classId",
as: "result"
}
}
])
// 方式二:推薦的連接查詢,該方式比較靈活,pipeline裏面可以使用aggregate的pipeline操作,而且支持多個關鍵字連接。
// 但是需要注意的是,該方式僅在MongoDB 3.6+版本的數據庫使用。
// 參考 https://docs.mongodb.com/manual/reference/operator/aggregation/lookup/
db.getCollection("classCol").aggregate([
{
$lookup: {
from: "studentCol",
let: {
alias_local_class_id: "$_id"
},
pipeline: [{
$match: {
$expr: {
$and:[
{
$eq: [
"$schoolId", // schoolCol foreign id的字段
"$$alias_local_class_id"
]
}
]
}
}
}],
as: "alias_result_set"
}
}
])
其返回的結果的形式是嵌套式的文檔(並非SQL中左連接的查詢結果,該查詢會返回所有的被查詢的主表數據,而副本中沒有數據時,嵌套文檔爲空)。MongoDB指定的默認返回集大小爲100M,若超過默認值,MongoDB將會拋出異常(細節參考MongoDB官網)。若需要大量查詢時,可指定返回集 allowDiskUse 爲 true, 以及設置 Cursor(在3.6+版本中使用aggregate將強制設置Cursor,除非指定返回explain)。這一組合很好的解決了大量查詢數據返回的問題。
使用這allowDiskUse和 Cursor這一組合進行大量查詢時,MongoDB會將查詢的結果寫入磁盤,同時,返回Cursor(細節參考MongoDB官網)。該方式很好的做到了一次查詢,然後對數據流進行操作,大大優化了查詢的性能。
語句如下:
// 參考1: https://docs.mongodb.com/manual/reference/method/db.collection.aggregate/#example-aggregate-method-initial-batch-size
// 參考2:https://docs.mongodb.com/manual/reference/method/db.collection.aggregate/#db.collection.aggregate
db.getCollection("classCol").aggregate([
{
$lookup: {
.... 與上同,省略
}
}
], { allowDiskUse: true, cursor: { batchSize: 1000 } }) // 1000爲單次返回的結果條數。
// cursor: { batchSize: 0 } batchSize是指定第一次返回的結果條數,指定爲0時,適用於快速返回結果和錯誤。
Java程序中使用聯合查詢參考《 java-mongo複雜管道聚合aggregate的填坑之路(分頁、allowDiskUse、統計)》