寫在前面:這是一篇工具文,如果沒有需要,不建議看完;如果有需要,可以隨時查詢內容。
高級的一些查詢,很多的數據是在查詢的時候就做完了,正常理論來說,數據庫是一定要對查詢優化到極致的,如果能夠將復雜的數據格式放到后台來處理的話,會節省大量的時間。
除非說你能夠做到把業務處理的代碼性能優化到極致的同時又讓它可讀性不差,並且易於更變,否則這種冗余是可以接受的。
然后,我就直接用一些復雜查詢開場了蛤~
接下來的內容在Nodejs版本:10.15,雲開發sdk版本:~2.1.2下使用
先寫一套基本的代碼,接下來的所有代碼都需要把這段內容加到中間
const cloud = require('wx-server-sdk')
cloud.init({
env: cloud.DYNAMIC_CURRENT_ENV
})
const db = cloud.database();
// 后面兩個按需引入即可
const _ = db.command;
const $ = db.command.aggregate;
exports.main = async (event, context) => {
// 內容區
}
一些詞法的基礎說明
先說兩個東西,project
和group
。
這兩個東西,一個是對數據進行橫向的操作,一個則是對數據的縱向操作,這么說可能不大明確,不過可以先看一下下面的一張表,而后聽我娓娓道來。
name | age | gender | clan |
---|---|---|---|
Astroline | 18 | male | Dragon |
Eve | 11 | girl | Arunoido |
橫縱的數據就是那么來的,橫向數據是Astroline, 18, male, Dragon
;而縱向數據則是name, Astroline, Eve
project
是處理單條數據的,而group
是處理縱向數據的,多用於數據的匯總、歸類使用。
不管是project
還是group
,他們都是需要優先使用aggregate
的。
較大時間顆粒度查詢
一般來說,有些數據存在數據庫的時候並不理想,沒有規律,並不適合直接查詢,而project
這個參數,則是對數據進行一次預處理將數據轉換為理想的數據。
這里舉一個栗子:我想查詢某個月的訂單,但是我存入的時間格式為YYYY-mm-dd
,而我的想法是查詢出某一個月的所有數據,很明顯這是一個非常困難的過程,我最初甚至是想在后台循環輪詢月份的天數,然后把數據做整合。。。
這里的思路是用字符串操作將時間拆分為年、月、日不同的顆粒度;如果你用的是Datetime的形式存儲的,也可以使用小程序里的時間操作工具做預處理,也是可以達到同樣的效果的。
return db.collection('order')
.aggregate() // 注意這里哦,aggregate一定要加上,標記后面的查詢為聚合階段
.project({
price: true,
quantity: true,
year: $.substr(['$create_time', 0, 4]),
month: $.substr(['$create_time', 5, 2]),
date: $.substr(['$create_time', 8, -1]),
})
// match也是聚合階段的方法,匹配的是`project`預處理后的結果
.match({
year: '2020',
month: '04'
})
.end(); // 和基礎模板不同的,在aggregate里只能用.end()結尾,返回的數據和get()有所出入。
// .get()返回的是data: [{name: 'Astroline'}, {name: 'Eve'}];
// .end()返回的是list: [{name: 'Astroline'}, {name: 'Eve'}];
注:$
即是聚合操作符號,這里使用了一個字符串操作的方法,然后在方法里面還有一個'$create_time'
,這一段匹配的是訂單里的一個字段,我這里匹配的是創建時間。
當然,上面的代碼還是有問題,一方面是查詢是死的,沒有動態的數據;另一方面,微信數據庫一次僅能查出來100條數據,所以需要做個拼接。
一套完整可用的代碼貼在這里 代碼比較長,建議先跑一遍再理解 僅需要把表名和時間傳入即可使用,或者直接雲數據庫測試(需要刪掉`skip`、`limit`,並且修改變量為實際表名)
const { collection, date } = event;
const MAX_LIMIT = 100;
// 日期分組 0年 1月 2日
const date_time = date.split('-'); // 根據自己的數據格式調整
const tasks = [];
// 取出集合記錄總數
const countResult = await db.collection(collection)
.aggregate()
.project({
year: $.substr(['$create_time', 0, 4]),
month: $.substr(['$create_time', 5, 2]),
})
.match({
year: date_time[0],
month: date_time[1]
})
.count('total') // 聚合階段的count和基礎的count略微不同,返回的結果名稱要標記上
.end();
const total = countResult['list'][0]['total'];
const batchTimes = Math.ceil(total / 100);
for (let i = 0; i < batchTimes; i++) {
const promise = db.collection(collection)
.aggregate()
.project({
name: true,
quantity: true,
price: true,
year: $.substr(['$create_time', 0, 4]),
month: $.substr(['$create_time', 5, 2]),
})
.match({
year: date_time[0],
month: date_time[1]
})
.skip(i * MAX_LIMIT)
.limit(MAX_LIMIT)
.end();
tasks.push(promise)
}
// 等待所有
return (await Promise.all(tasks)).reduce((acc, cur) => {
return {
list: acc.list.concat(cur.list),
errMsg: acc.errMsg,
}
})
某月份訂單的的總價
然后業務就開始涉及到了一些匯總方面的內容了,因為匯總的內容不在小程序內部展示,於是我寫了一套外部的API,避免雲函數的運算過大(超時時間三秒鍾),導致的返回返回超時(其實超時時間可以修改,不過等三秒。。已經交互非常不友好了)
這次做的是一個某一個月份的訂單價格匯總,因為如果把業務丟在雲函數里,計算是會非常龐大的(查詢速度不說,還要對查詢出來的結果重新遍歷)
const {
date,
} = event;
// 日期分組 0年 1月 2日
const date_time = date.split('-');
return db.collection('order')
.aggregate()
.project({
price: true,
quantity: true,
create_time: true,
totalPrice: $.multiply(['$quantity', '$price']),
year: $.substr(['$create_time', 0, 4]),
month: $.substr(['$create_time', 5, 2]),
})
.match({
year: date_time[0],
month: date_time[1]
})
.group({
_id: 'Eve!',
quantity: $.sum('$quantity'),
price: $.sum('$totalPrice'),
})
.end();
注:在match
和group
階段的數據都是基於project
查出來的數據,舉個栗子,如果你要把project
里的quantity: true
改成false
的話,查出來的結果在進行group
操作的時候quantity
字段找不到,就會返回為0
。
查詢數據分類
這里就是要說到數據的分類了查詢查詢了,打個比方說,我需要查出今年的所有訂單,每個月要一個匯總(一般搞數據可視化展示需要用到這種數據,咳)
我了解的有兩種分類方式,一種是創建一組歸類好的模板,然后用lookup拉外鍵查詢,這種方式並不好,還需要額外建表,並且不夠靈活;而第二種,就是我下面要說的了。
在做數據可視化,整理數據的時候我需要一組可以用在柱狀圖的數據,我不大想用后台創建一堆`POJO`類,然后就干脆放在了數據庫里處理了...
const {
date,
} = event;
// 日期分組 0年 1月 2日
const date_time = date.split('-');
return db.collection('order')
.aggregate()
.project({
quantity: true,
create_time: true,
totalPrice: $.multiply(['$quantity', '$price']),
year: $.substr(['$create_time', 0, 4]),
month: $.substr(['$create_time', 5, 2]),
})
.match({
year: date_time[0],
month: date_time[1]
})
.group({
_id: '$month',
price: $.sum('$totalPrice'),
create_time: $.first('$create_time'),
quantity: $.sum('$quantity'),
})
.end();
條件分類查詢
當然,上一個業務提到的數據分類,我們還可以再改一下
比如說我加一點預處理的內容——
訂單有四種:未成交未付款、成交未付款、成交已付款、取消訂單(未成交且超時、訂單被取消)。按照一般查詢,我需要查詢四次才可以把數據查出來(`match({ type: 0 })、match({ type: 1 })、match({ type: 2 })、match({ type: 3 })`),而如果它是個橫向的數據(一條數據里返回四種狀態),那對我來說是非常的舒服了。
const {
date,
} = event;
// 日期分組 0年 1月 2日
const date_time = date.split('-');
return db.collection('order')
.aggregate()
.project({
price: true,
status: true,
quantity: true,
year: $.substr(['$created', 0, 4]),
month: $.substr(['$created', 5, 2]),
date: $.substr(['$created', 8, -1]),
// 判斷條件
uu: $.cond({ // Unsettled and unpaid 我想取名2u的,不過命名規范里不可以數字打頭哦
if: $.and([ // 這里僅展示一些復合的條件判斷,一般訂單不會出現status的....不過我還是做了一個判斷,僅判斷可用訂單(1可用,0凍結)
$.eq(['$type', 0]),
$.not($.eq(['$status', 0]))
]),
then: '$quantity',
else: 0
}),
tp: $.cond({ // Transaction paid 成交已付款
if: $.eq(['$type', 1]),
then: '$quantity',
else: 0
}),
tnp: $.cond({ // Transaction not paid 成交未付款
if: $.eq(['$type', 2]),
then: '$quantity',
else: 0
}),
oo: $.cond({ // Outstanding orders 訂單已取消 2o
if: $.eq(['$type', 3]),
then: '$quantity',
else: 0
}),
})
.match({
year: date_time[0],
month: date_time[1]
})
.group({ // js、python等語言我習慣用下划線,java、C#、ts一類的語言變量習慣用小駝峰,類名用大駝峰
_id: 'Eve~',
unsettled_and_unpaid: $.sum('$uu'),
transaction_paid: $.sum('$tp'),
transaction_not_paid: $.sum('$tnp'),
outstanding_orders: $.sum('$oo')
})
.end()
最后,多表聯查案例
我很少寫電商類的程序,不過我還是知道電商里面有兩個基本的表,商品類目以及商品表(項目體積若再大些,可能會拆出更細致的表)。
一般來說,這方面的業務處理方式不會用lookup
做的,除非數據不多(比如類目表上面還有一個商家表,美團這樣B2B的軟件,這樣就能很好的限制了查詢出來的商品數量,可以使用lookup
查出所有關聯數據,換來極好的交互體驗)
這里的場景模擬在用戶在小程序端,點擊某個商家,進入時候查看商品的查詢(雖然我寫的不是電商,不過還是能改成電商的查詢的,並且我相信用電商這個命題會更容易理解的吧)。我決定將
lookup
拆成兩部分來說,第一個部分是簡單的查詢,用於客戶的使用;另一個部分是用於企業領導層查看的,做數據可視化使用。
(其實我根本不需要寫基礎的lookup
查詢,小程序官方文檔里已經寫的非常清楚了,主要是復雜的查詢我寫了一套出來)
有這么三張表,店鋪表、類目表、商品表,關系為:店鋪和類目是1:m,類目和商品是1:m。
const { storeId } = event;
// 一般一個店鋪的類目不會超過100條的,所以大膽食用吧
return db.collection('category')
.aggregate()
.match({
storeId: storeId
})
.lookup({
from: 'product',
localField: '_id', // 小程序里的默認唯一標識是 _id
foreignField: 'category_id',
as: 'productList'
})
.end()
進階的多表聯查
在lookup
里我依舊使用了很多復雜的處理,如pipeline、let變量,兩種查詢方式的權重是相同的,一條查詢里不可能講明兩種方法,所以這里單獨寫了一條。
對我而言,我覺得這種查詢除非是你想偷懶不寫后台的業務處理,否則盡量不要寫這種代碼....
有一個需求,上級想要看到一個銷售的柱狀圖數據...我真不想編各種各樣奇奇怪怪的需求了。。。饒了我吧QAQ
我們需要看到一個商家不同的商品的銷售狀況如何,做柱狀圖統計...
我真想直接把項目里的查詢貼出來...但是寫的程序不允許我貼。。。只好再寫一套查詢用於博客記錄...
const { // 注意要傳值
date,
store_id
} = event;
// 日期分組 0年 1月 2日
const date_time = date.split('-');
return db.collection('product')
.aggregate()
.lookup({
from: 'order',
let: { // 變量聲明 引用的時候使用雙$符號 如'$$product_id'
product_id: '$_id'
},
pipeline:
$.pipeline() // 流水處理 你可以直接理解pipeline就是正常查詢里的,只不過它針對的對象是外鏈的表 .aggregate()
.project({
quantity: true,
price: true,
create_time: true,
year: $.substr(['$create_time', 0, 4]),
month: $.substr(['$create_time', 5, 2]),
})
.match(_.expr( // 這里僅展示了一下lookup內的and操作符使用
$.and([
$.eq(['$product_id', '$$product_id']), // 商品相同的內容
$.eq(['$year', date_time[0]]), // 時間規定在月份
$.eq(['$year', date_time[1]]), // 時間規定在月份
])
))
.group({
_id: 'Eve...',
quantity: $.sum('$quantity'),
})
.done(), // pipeline需要用done結尾
as: 'result'
})
.project({
// 這個自己寫好啦
name: true,
type: true,
create_time: true,
sold: $.arrayElemAt(['$result', 0]),
})
.match({
store_id: store_id,
})
.end();
注:比較神奇的操作,lookup是可以嵌套的,也就是傳說中的傳說中三表聯查
該代碼我沒有跑過測試,知道有這么個東西即可,關於三表聯查的內容是可以搜到的
const { storeId } = event;
// 一般一個店鋪的類目不會超過100條的,所以大膽食用吧
return db.collection('store')
.aggregate()
.match({
storeId: storeId
})
.lookup({
from: 'category',
localField: '_id', // 小程序里的默認唯一標識是 _id
foreignField: 'store_id',
as: 'categoryList'
})
.lookup({
from: 'product',
localField: '_id', // 小程序里的默認唯一標識是 _id
foreignField: 'category_id',
as: 'productList'
})
.end()
目錄跳轉:微信小程序雲開發數據庫查詢指南