在一个日活过万的电商类微信小程序里,商品搜索是用户使用频率最高的功能之一。用户在搜索框输入关键词后,小程序会调用云函数去云数据库里查询商品集合,再按销量或价格排序返回结果。这个功能上线初期一切正常,但随着商品数量从几千条涨到十几万条,问题来了:搜索接口的响应时间从最初的几百毫秒恶化到了三秒以上,高峰期甚至出现云函数超时报错。排查后发现,问题根源出在数据库查询没有命中索引,触发了全表扫描。这篇文章就来完整复盘这次索引优化的全过程,包括问题定位、索引设计、避坑细节和最终的优化效果。

一、先搞清楚云数据库索引的工作原理和默认限制
微信小程序云开发使用的底层是腾讯云的文档型数据库,索引机制和MongoDB基本一致。很多开发者有一个误区,认为只要查询能返回结果就没问题,实际上查询语句写法不同,索引的命中情况可能天差地别。云数据库默认只给每个集合创建一个_id索引,这个索引只对_id字段的等值查询有效。也就是说,如果你的查询条件是name、categoryId这类业务字段,在没有额外建索引的情况下,数据库需要把集合里的文档逐条扫描一遍,筛选出满足条件的结果,这就是常说的全表扫描。
全表扫描在数据量小的时候几乎无感,几千条文档扫一遍也就几十毫秒。但数据量到了十万级别,一次扫描可能就是秒级耗时,再叠加排序操作,性能雪崩是必然的。判断查询是否命中索引,可以在云开发控制台的数据库监控里观察扫描文档数和返回文档数的比例,如果扫描数远大于返回数,基本可以断定走了全表扫描。我们当时的商品集合有十二万条文档,一次关键词搜索扫描了全部文档,这就是响应时间飙到三秒的直接原因。
二、电商搜索场景的复合索引设计
电商商品搜索的典型查询模式是这样的:按类目筛选,加上关键词匹配,再按销量倒序排列。对应的查询语句类似下面这种写法:
// 云函数中的商品搜索查询
db.collection('products')
.where({
categoryId: categoryId, // 类目等值筛选
name: db.RegExp({ // 关键词模糊匹配
regexp: keyword,
options: 'i'
}),
status: 1 // 只查上架商品
})
.orderBy('sales', 'desc') // 按销量倒序
.limit(20)
.get()针对这种查询,需要设计一个复合索引。复合索引的字段顺序有一条核心原则:等值条件字段在前,排序字段居中,范围或模糊匹配字段放最后。原因是复合索引遵循最左前缀匹配规则,等值条件能精确缩小索引扫描区间,而正则匹配本质上是一种范围查询,放在前面会让后续字段无法利用索引排序。最终我们创建的复合索引字段顺序是categoryId、sales、name,其中sales设置为降序,与查询排序方向保持一致。
在云开发控制台创建索引的步骤是:进入数据库模块,选中products集合,切换到索引管理页签,点击添加索引。索引名称可以自定义,比如取名为cate_sales_name_idx,然后依次添加三个字段并指定排序方向。需要注意的一点是,如果集合里已有大量数据,创建索引会触发后台构建,期间数据库读写性能可能有短暂波动,建议在业务低峰期操作。索引构建完成后,可以再次执行搜索查询,对比控制台监控里的扫描文档数,验证索引是否生效。
三、正则查询走不了索引的坑及替代方案
这里必须重点提一个坑:即使建好了复合索引,db.RegExp写的正则查询也大概率命中不了索引,尤其是当前缀不是固定字符串的时候。MongoDB的索引按字段值的排序组织,只有前缀固定的正则(例如/^手机/这种以^开头的写法)才能利用索引做前缀定位,而电商搜索里常见的中间包含匹配,比如用户搜"充电器"要匹配"快充充电器",正则无法走B树索引。我们的第一版优化上线后,扫描数虽然从十二万降到了两万多(类目索引生效了),但正则部分依然在拖后腿。
解决这个问题的思路有两个。第一个思路是冗余字段加多路匹配:给商品文档增加一个keywords数组字段,把商品名、品牌、热门搜索别名做分词后存进数组,查询时改用_.in做数组包含匹配。数组字段的等值查询是可以命中多键索引的,配合在keywords字段上建索引,扫描数能进一步大幅下降。改造后的查询写法如下:
// 改用数组字段做关键词匹配
db.collection('products')
.where({
categoryId: categoryId,
keywords: keyword, // 数组包含匹配,可命中多键索引
status: 1
})
.orderBy('sales', 'desc')
.limit(20)
.get()第二个思路是引入搜索能力更强的方案,比如把商品数据同步到云开发提供的搜索能力或自建的分词检索服务。对于中小体量的电商小程序,第一种冗余字段方案成本最低,改造量也小,只需要在商品入库和编辑时维护好keywords数组。我们最终采用的就是方案一,配合复合索引调整,把keywords字段加入复合索引末尾,整体效果非常明显。
四、优化前后的实测效果与几点经验总结
优化完成后我们做了对比测试。优化前:商品集合十二万条文档,搜索接口平均响应约3100毫秒,扫描文档数十二万,高峰期频繁触发云函数超时。优化后:同样的查询条件,平均响应时间降到180毫秒左右,扫描文档数降到一千以内,超时问题彻底消失。用户端最直观的感受是搜索结果几乎秒出,搜索功能的次日留存也有小幅提升,说明性能优化对用户体验的反馈是实打实的。
复盘整个过程,有几条经验值得记录。第一,索引不是建得越多越好,每个索引都会占用存储空间并拖慢写入,只给高频查询模式建索引,低频运营类查询可以容忍慢一些。第二,查询语句的写法要和索引设计配套,比如排序字段方向、查询条件的字段顺序都要对齐,否则索引建了也白建。第三,数据规模增长要有预判,几千条数据时掩盖的问题在十万级会集中爆发,建议在商品数突破一万之前就把索引方案规划好。第四,养成看数据库监控的习惯,扫描数与返回数的比值是最直观的健康指标。索引优化看似是个小课题,但做好了能以极小的成本换来数量级的性能提升,这对资源受限的小程序云开发环境来说尤其划算。