云开发数据库的高级查询
写在前面:这是一篇工具文,如果没有需要,不建议看完;如果有需要,可以随时查询内容。
高级的一些查询,很多的数据是在查询的时候就做完了,正常理论来说,数据库是一定要对查询优化到极致的,如果能够将复杂的数据格式放到后台来处理的话,会节省大量的时间。
除非说你能够做到把业务处理的代码性能优化到极致的同时又让它可读性不差,并且易于更变,否则这种冗余是可以接受的。
然后,我就直接用一些复杂查询开场了蛤~
接下来的内容在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()
目录跳转:微信小程序云开发数据库查询指南