小程序云开发用久了,几乎每个开发者都会撞上同一堵墙:订单存在orders集合里,商品信息存在goods集合里,用户想看订单列表时还得在客户端循环发起第二次查询,数据量一上来页面卡顿明显,而且小程序端一次最多只能拉取20条记录,体验非常糟糕。其实云数据库的聚合查询早就提供了解决方案,lookup操作符可以在数据库端完成多表关联,把拼装数据的活儿全部交给服务端,客户端只拿最终结果。本文以一个电商场景为例,完整讲解lookup的用法,以及如何配合统计类操作完成数据分析。

聚合查询与普通查询的区别
先厘清一个容易混淆的概念。我们平时用的db.collection('orders').get()属于普通查询,它只能对单个集合做筛选、排序和分页,返回的是原始文档。而聚合查询走的是一条完全不同的流水线:数据像在工厂传送带上一样,依次经过一个个阶段(stage),每个阶段对数据进行加工,最后输出成品。常见的阶段包括match筛选、lookup关联、group分组统计、project字段裁剪、sort排序、limit截取等。
两者还有一个关键差异:聚合查询默认同样受20条限制,但可以通过.limit()把单次拉取上限提到1000条(云函数端),对于统计类需求来说基本够用。更重要的是,group统计产生的结果是聚合后的记录数,而不是原始记录数,比如统计全店商品销量时,即使orders里有十万条记录,group之后可能只剩下几百个商品维度,一次就能全部取回。
在写法上,聚合查询通过.aggregate()进入流水线模式,每个阶段用.链式调用,最后.end()收尾。理解了流水线思想,后面的lookup就很好懂了——它只是传送带上的一道工序。
lookup关联本地集合:订单拼接商品信息
lookup最常见的用法是把当前集合与另一个集合关联起来。假设orders集合里每条订单只存了goodsId,商品名称、价格都在goods集合中,我们希望查出订单列表并附带商品详情,写法如下:
const cloud = require('wx-server-sdk')
cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV })
const db = cloud.database()
exports.main = async (event, context) => {
const res = await db.collection('orders').aggregate()
.match({
status: 1 // 只查已支付的订单
})
.lookup({
from: 'goods', // 要关联的集合名
localField: 'goodsId', // 当前集合的关联字段
foreignField: '_id', // 目标集合的匹配字段
as: 'goodsInfo' // 关联结果存放的字段名
})
.limit(20)
.end()
return res.list
}这里有几个细节要注意。localField和foreignField不需要带$前缀,直接写字段名即可,这一点和MongoDB原生语法不同,是云数据库做的一层封装。关联结果的as字段永远是一个数组,即使只匹配到一条数据也是长度为1的数组。所以如果想让商品信息直接平铺在订单对象上,通常要再接一个unwind阶段把数组拆开:
.lookup({
from: 'goods',
localField: 'goodsId',
foreignField: '_id',
as: 'goodsInfo'
})
.unwind('$goodsInfo') // 把数组拆成单个对象,注意这里要加$unwind的参数必须带$符号,表示引用字段值,这也是新手最容易踩的坑之一。拆开之后就可以直接用goodsInfo.name、goodsInfo.price这样的路径访问商品属性了。另外建议用project把不必要的大字段剔除,比如订单里的快照图片base64之类,减少传输体积。
lookup的pipeline写法:跨集合与多条件关联
基础写法只能做单字段等值匹配,如果关联条件更复杂,比如要同时匹配商品ID和规格ID,或者要在关联时就把子表数据过滤掉一部分,就需要用到pipeline形式的lookup。这种写法把关联集合的查询条件写成一个子流水线,灵活性大得多:
const res = await db.collection('orders').aggregate()
.lookup({
from: 'order_items',
let: {
orderNo: '$orderNo' // 把主表字段赋值给变量
},
pipeline: $.pipeline()
.match($.expr($.and([
$.eq(['$orderNo', '$$orderNo']), // 双表字段相等
$.eq(['$isDeleted', false]) // 关联时顺带过滤软删除数据
])))
.sort({ createTime: -1 })
.limit(5) // 每个订单只取最近5条明细
.done(),
as: 'items'
})
.end()这里的let相当于声明变量,把主表的orderNo取出来存成$$orderNo供子管道使用。子管道里的match必须配合expr来做字段间的比较,普通的match条件只能写死常量。注意云数据库要求先执行const $ = db.command.aggregate拿到聚合操作符对象,代码里才能使用$.pipeline()和$.expr()。
pipeline写法还能实现一种特殊需求:lookup本集合自身。比如商品分类表里每个分类存了parentId,想一次性查出分类及其子分类列表,from填自己的集合名即可。这种自关联在处理树形结构时非常实用,避免了在客户端写递归。
配合group做数据统计:销量排行榜实战
多表关联的真正威力在于统计场景。接着上面的例子,做一个商品销量排行榜:先从orders流水线进入,unwind拆开每条订单里的商品数组(假设一条订单包含多个商品项),再按商品ID分组求和,最后lookup回商品集合补全名称。完整代码如下:
const $ = db.command.aggregate
const res = await db.collection('orders').aggregate()
.match({ status: 1 })
.unwind('$items') // 每条订单含多个商品项,拆开统计
.group({
_id: '$items.goodsId', // 按商品ID分组
totalSales: $.sum('$items.count'), // 累加销量
orderCount: $.sum(1) // 订单笔数
})
.sort({ totalSales: -1 })
.limit(10)
.lookup({
from: 'goods',
localField: '_id',
foreignField: '_id',
as: 'goodsInfo'
})
.unwind('$goodsInfo')
.project({
_id: 0,
goodsName: '$goodsInfo.name',
totalSales: 1,
orderCount: 1
})
.end()这个例子体现了聚合查询的核心思路:数据的加工顺序是可以自由编排的。lookup不一定非要在最前面,放在group之后关联,能显著减少需要关联的文档数量——只有前10名商品才需要去goods集合查名称,而不是把所有订单都关联一遍再统计,性能差距可能是数量级的。
group阶段还有不少实用的累加器,比如$.avg()求平均客单价、$.max()取最新下单时间、$.push()把分组内的字段收集成数组。如果要做按天统计的销售曲线,可以用$.dateToString先把时间戳格式化成日期字符串再分组:
.group({
_id: $.dateToString({
date: '$.toDate: $createTime',
format: '%Y-%m-%d',
timezone: 'Asia/Shanghai' // 时区一定要指定,否则差8小时
}),
dailyAmount: $.sum('$payAmount')
})实战中的注意事项与性能优化
第一,索引是聚合查询的生命线。match阶段用到的字段、lookup的foreignField字段,都应该在控制台建立索引。尤其是被关联集合的匹配字段,没有索引时每条主表记录都要触发一次全集合扫描,数据量过万后查询会明显变慢甚至超时。
第二,控制进入流水线的数据量。尽量把match放在流水线最前面,先用时间范围、状态值把数据过滤到最小集合,再做关联和分组。一个经验法则是:match越靠前,lookup的输入越小,整个查询越快。如果统计周期固定,也可以考虑用定时触发器每天凌晨把统计结果写入一个汇总集合,页面直接读汇总结果,把复杂聚合从用户请求链路上摘掉。
第三,注意小程序端与云函数端的权限差异。小程序端直接跑聚合查询受集合权限规则限制,如果集合设置为仅创建者可读,聚合结果可能查不到别人的数据;而统计类需求通常要读全量数据,建议把这类聚合放到云函数中执行,配合run或者直接用云函数默认的管理员权限来绕开前端权限限制。此外聚合结果在小程序端单次上限20条、云函数端可到1000条(通过limit设置),做分页统计时要记得翻页拼接。
第四,字段命名尽量统一。主表存关联ID时,建议直接存对方集合的_id,这样lookup时foreignField写_id就能天然命中默认索引,省去了单独建索引的麻烦。如果业务上用的是自增编号之类的业务主键,别忘了给它建索引。
总结一下,lookup配合整条聚合流水线,基本能覆盖小程序里绝大多数多表查询和报表统计需求。记住三条原则:match前置过滤、关联字段建索引、能group后再lookup就不先lookup,把握好这三点,聚合查询的性能和可维护性都不会差。
小程序云数据库聚合查询lookup多表关联修改时间:2026-09-13 12:30:48