12.7 数据库性能与优化

云开发的数据库虽然是高性能、支持弹性扩容,但是很多用户在使用的过程中,更加注重功能的实现,而忽视了数据库的设计、索引的创建以及语句的优化等对性能的影响,因此会遇到很多影响数据库性能的问题,因此这里特意总结一下云开发数据库性能优化的注意事项。

12.7.1 数据库性能与优化建议

以下是一些影响数据库性能的优化建议,当然要结合具体的业务情况来处理,不能一概而论。尤其是一些请求量比较大、比较频繁,比如小程序首页的数据请求,数据库的优化要格外重视。

1、要合理使用索引

使用索引可以提高文档的查询、更新、删除、排序操作,所以要结合查询的情况,适当创建索引。要尽量避免全表扫描,首先应考虑在 where 及 order by 涉及的列上建立索引。更多索引的细节在索引的章节里有介绍。

2、擅于结合查询情况创建组合索引

对于包含多个字段(键)条件的查询,创建包含这些字段的组合索引是个不错的解决方案。组合索引遵循最左前缀原则,因此创建顺序很重要,如果对组合索引不了解,可以结合索引的命中情况来判断组合索引是否生效。要善于使用组合索引做到用最少的索引覆盖最多的查询。

3、查询时要尽可能通过条件和limit限制数据

在查询里where可以限制处理文档的数量,而在聚合运算中match要放在group前面,减少group操作要处理的文档数量。无论是普通查询还是聚合查询都应该使用limit限制返回的数据数量。

其实云开发针对普通查询db.collection('dbName').get()默认都有limit限制,在小程序端的限制为20条(自定义上限也是20条),在云函数端的限制为100条(自定义上限可以设置为1000条),聚合则在小程序端和云函数端默认都为20条(自定义没有上限,几万条都可以,前提是取出来的数据不能大于16M),也就是云开发数据库已经自带了一些性能优化,我们不应该把这些默认的限制当成是一种束缚,而去随意突破这些限制。

4、推荐在小程序端增删改查数据库

可以结合数据库的安全规则,让数据库的增删改查在小程序端进行,这样速度会更快,而且还可以节省云函数的资源。

云开发数据库的增删改查可以在小程序端进行,也可以在云函数端进行,那到底应该把数据库的增删改查放在小程序端还是云函数端呢?一般情况下建议放在小程序端,这样就只会消耗数据库请求的次数,而不会额外增加消耗云函数的资源使用量GBs、外网出流量。而云函数虽然有数据库操作的更高的权限,但是小程序端结合安全规则也是可以让数据库的权限粒度更细,也能满足大部分权限要求。

5、尽可能限制返回的字段等数据量

如果查询无需返回整个文档或只是用来判断键值是否存在,普通查询可以通过filed、聚合查询可以通过project来限制返回的字段,减少网络流量和客户端的内存使用。

{ "title": "为什么要学习云开发", "content": "云开发是腾讯云为移动开发者提供的一站式后端云服务", "comments": [{ "name": "李东bbsky", "created_on": "2020-03-21T10:01:22Z", "comment": "云开发是微信生态下的最推荐的后台技术解决方案" }, { "name": "小明", "created_on": "2020-03-21T11:01:22Z", "comment": "云开发学起来太简单啦" }] }

云数据库是关系型数据库,一个记录里可以嵌套非常多的数组和对象,如果取出整个记录里的所有嵌套内容就太耗性能流量了,比如上面的嵌套数组,有时候业务上并不需要显示comments里的某些字段,是可以通过field的点表示法来限制返回的字段的。

//不显示comments里的created_on .field({ "comments.created_on":0 }) //只显示comments里的comment,comments里的其他字段都不显示 .field({ "comments.comment":1 })

6、查询量大时建议不要用正则查询

正则表达式查询不能使用索引,执行的时间比大多数选择器更长,所以业务量比较大的地方,能不用正则查询就不用正则查询(尽量用其他方式来代替正则查询),即使使用正则查询也一定要尽可能的缩写模糊匹配的范围,比如使用开始匹配符 ^ 或结束匹配符 $ 。

比如有人是这样用正则查询的,他想根据省市来筛选客户来源数据,但是客户来源的地址address填写的是”广东省深圳市“或”广东深圳“,省市数据并不规范一致,于是使用正则进行模糊查询,但是如果你需要经常根据地址来筛选客户来源,那你应该在数据库对数据进行处理,比如province和city来清洗重组数据从而替代模糊查询。

7、尽可能使用更新指令

通过更新指令对文档进行修改,通常可以获得更好的性能,因为更新指令不需要查询到记录就可以直接对文档进行字段级的更新,尤其是不需要更新整个文档只需要更新部分字段的场景。

还是上面的那个记录为例,比如我们需要给文章添加评论,也就是往comments数组里添加值,我们可以使用 _.push来给数组字段进行字段级别的操作,而不是取出整个记录,然后把评论用数组的concat或push的方法添加到记录里,再更新整个记录:

.update({ data:{ comments:_.push([{ "name": "小明", "created_on": "2020-03-21T11:01:22Z", "comment": "云开发学起来太简单啦" }]) } })

云开发数据库一个记录可能会嵌套很多层,因此也会很大,使用更新指令进行字段级别的微操比直接使用update这种记录级别的更新性能要更好。

8、不要对太多数据进行排序

不要一次性取出太多的数据并对数据进行排序,如果需要排序,请尽量限制结果集中的数据量,比如我们可以先用where、match等操作限制数据量,也就是通常要把orderBy放在普通查询或聚合查询的最后面。

这里尤其强调的是,发现有不少人由于对数据库的排序orderBy与翻页skip没有理解,竟然把数据库所有数据使用遍历取出来之后再来排序,哪怕是数据量只有百千条,这也是不正确的处理方式,应该禁止这么干。排序使用数据库的普通查询或聚合查询的orderBy就可以做到了,云开发默认的limit数据限制不会影响排序的结果,禁止遍历取出所有数据再来排序的愚蠢行为

当然如果业务会需要经常对同一数据的多个字段来排序,比如商品经常会按最新上架、价格高低、产地、折扣力度等进行排序,则建议一次性取出这些数据,存储在缓存中,使用JavaScript的数组来进行排序,而不是用数据库查询。

9、尽量少在业务量大的地方用以下查询指令

查询中的某些查询指令可能会导致性能低下,如判断字段是否存在的exists,要求值不在给定的数组内的nin,表示需满足任意多个查询筛选条件or,表示需不满足指定的条件not,尽量少在业务使用量比较大的地方用这些查询指令。

这里所说的尽量少用不代表不用,而是能够用最直接的方式就用最直接的方式代替,让数据库查询尽可能的简单而不是搞的过于复杂,尽可能少让查询指令做这些复杂的事情。

10、集合中文档的数量可以定期归档

集合中文档的数据量会影响查询性能,对不用的数据或过期的数据可以进行定期归档并删除。比如我们也可以借助于定时触发器周期性的对数据库里的数据进行备份、删除。

11、不要让数据库请求干多余的事情,尽量少干事

能够使用JavaScript替代的计算、数组、对象操作等,就尽量用JavaScript处理;能通过数据库设计让数据库查询少计算的就尽量合理设计数据库,要尽可能的让数据库少干活,不能一次查询多个指令、正则查询套来套去的。

12、在数据库设计时可以用内嵌文档来取代lookup

云开发数据库是非关系型数据库,可以对经常要使用lookup跨表查询的情况做反范式化的内嵌文档设计,通过这种方式取代联表查询lookup可以提升不少性能。

减少使用联表查询lookup的使用的方式要注意两点,一是通过内嵌文档的方式是可以减少关系型数据库那种表与表之间的关联关系的,比如要联表取出博客里最新的10篇文章以及文章里相应的评论,这在关系型数据库里原本是需要联表查询的,但是当把评论内嵌到文章的集合里时,就不需要联表了;二是有的时候我们只是需要跨表而不是联表,可以通过多次查询来取代联表。

13、推荐使用短字段名

和关系型数据库不同的是,云开发数据库是文档型数据库,集合中的每一个文档都需要存储字段名,因此字段名的长度相比关系型数据库来说会需要更多的存储空间。

"comments": [{ "name": "李东bbsky", "created_on": "2020-03-21T10:01:22Z", "comment": "云开发是微信生态下的最推荐的后台技术解决方案" }, { "name": "小明", "created_on": "2020-03-21T11:01:22Z", "comment": "云开发学起来太简单啦" }]

这里的字段名name、created_on、comment有多少个记录,有多少个嵌套的对象就会被写多少次,有时候比字段的值还要长,是比较占空间的。

12.7.2 数据库设计以及处理的优化建议

1、增加冗余字段

在业务上有些关键的数据可以通过间接的方式查询获取到,但是由于查询时会存在计算、跨表等问题,这个时候建议新增一些冗余字段。

比如我们要统计文章下面的评论数,可能你将文章的评论独立建了一个集合如comments,这时候要获取每篇文章的评论数是可以根据文章的id条件来count该文章有多少条评论的。或者你也可以把每篇文章的评论数组作为子文档内嵌到每个文章记录的comments字段,这个时候可以通过数组的长度来算出该文章的评论数。类似于评论数的还有点赞量、收藏量等,这些虽然都是可以通过count或数组length的方式来间接获取到的,但是在评论数很多的情况下,count和数组的length是非常耗性能的,而且count还需要独立占据一个请求。

遇到这种情况,建议在数据库设计时,要用所谓的冗余字段来记录每篇文章的点赞量、评论数、收藏量,在小程序端直接用inc原子自增的方式更新该字段的值。

{ "title": "为什么要学习云开发", "content": "云开发是腾讯云为移动开发者提供的一站式后端云服务", "commentNum":2, //新增一个评论数的字段 "comments": [{ "name": "李东bbsky", "created_on": "2020-03-21T10:01:22Z", "comment": "云开发是微信生态下的最推荐的后台技术解决方案" }, { "name": "小明", "created_on": "2020-03-21T11:01:22Z", "comment": "云开发学起来太简单啦" }] }

比如我们希望在博客的首页展示文章列表,而每篇文章要显示评论总数。虽然我们可以通过comments的数组长度以及如果存在二级三级评论(尤其是这种情况),也是可以通过数组方法获取到评论数,但是不如直接查询新增的冗余字段commentNum来得直接。

2、虚假删除

有时候我们的业务会需要用户经常删除数据库里面的记录或记录里的数组的情况,但是删除数据是非常耗费性能的一件事,碰到业务高峰期,数据库就会出现性能问题。这个时候,建议新增冗余字段做虚假删除,比如给记录添加delete的字段,默认值为false,当执行删除的时候,可以将字段的值设置true,查询时只显示delete为false的记录,这样数据在前端就不显示了,做到了虚假删除,在业务低谷时比如凌晨可以结合定时触发器每天这个时候清理一遍。

3、尽量不要把数据库请求放到循环体内

我们经常会有查询数据库里的数据,并对数据进行处理之后再写回数据库的需求,如果查询到的数据有很多条时,就会需要我们进行循环处理,不过这个时候一定要注意,不要把数据库请求放到循环体内,而是先一次性查询多条数据,在循环体内对数据进行处理之后再一次性写回数据库。

当然小程序有些接口不能进行数组操作,只能一条一条执行,比如发送订阅消息、上传文件等操作等,这个避免的不了的例外。但是有些是可以通过数据库的设计来规避这个问题的,比如把经常要新增大量记录的数据库设计为只需要新增内嵌文档的数组数据等。

4、尽量使用一个数据库请求代替多个数据库请求

在数据库的设计上以及在数据库请求的代码上,尽可能用一个数据库请求来代替多个数据库请求,尤其是用户最常访问的首页,如果一个页面的数据库请求太多,会导致数据库的并发问题。有些数据能够缓存到小程序端就缓存到小程序度,不必过分强调数据的一致性。

5、规划好文档适时创建空字段

我们有这样一个集合user,最终会用来存储用户的个人信息,比如当我们在用户点击登录时会获取用户的昵称和头像,于是一般的逻辑是我们会在数据库创建一个记录,如下所示:

_id:"", userInfo:{ "name":"李东bbsky", "avatarUrl":"头像地址" }

但是更好的方式是,我们应该创建一个完整的记录(按照最终的字段设计),哪怕现在还没有数据,也要一致性建好这些空字段,方便以后直接使用update的方式来往里面填充数据。

_id:"", userInfo:{ "name":"李东bbsky", "avatarUrl":"头像地址", "email":"", "address":"" ... }, stars:[],//存储点赞的文章 collect:[] //存储收藏的文章

12.7.3 慢查询与告警

目前我们没法直接查看数据库请求所花费的时间,但是有一些其他数据作为佐证,在云函数端进行数据库请求,如果云函数的执行时间超过100ms甚至更多,则基本可以判定为慢查询,数据库需要优化。这时,慢查询不仅会影响数据库的性能,还会影响云函数的性能。

我们知道云函数和云数据库的并发都是非常依赖他们的耗时的,如果数据库查询速度变慢,查询一次耗时由几十毫秒增加到几百毫秒,甚至以秒计算,都是十分耗费资源和影响并发的:

  • 云函数资源使用量 GBs:资源使用量 = 函数配置内存 X 运行计费时长,如果云函数里有数据库请求耗费了运行时长,云函数资源使用量也会增加;不过云函数的并发统一上限为1000,通常是很难达到的;
  • 数据库的QPS = 数据库同时连接数 * 1000ms/数据库请求的执行时间,如果数据库请求的执行时间出现大幅上升,QPS也就会成倍的下降,非常影响数据库的并发,会出现Connection num overrun的报错。

我们可以在云开发控制台设置-告警设置来给指定的云函数尤其是业务调用最频繁的云函数设置运行时间以及云函数运行错误的告警,以便随时了解云开发环境的运行状况。