Contents

Mongo Big Data Paging Query

现象分析

在做用户画像时要用户标签表5000多万行,一个用户可以对应很多个tag,而且有的tag可以不存在。

用户标签表(user_tags)数据结构如:

1
{"user_id":"xxx", "tag_1":1, "tag_2":1, "tag_3":1, ...other_tag}

所以,我们只给user_id加了索引,tag_id并没有加索引。 用户信息表(user_props)是包含user_id和其他信息的宽表。如:

1
{"user_id":"xxx", "age": 100, ...}

前端在展示的时候,先获取一个标签的所用用户。

问题就在这里,由于前端用的分页组件,需要的数据是10行,total数。每次点击完tag,查询对应用户都需要花很长时间

所以,我们从查询语句分析开始。

我的查询如下:

加入explain是分析查询是否走了索引,queryPlanner.winningPlan.inputStage.stage列显示查询策略stage参考

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
//db.user_portrait_tag_list_20170117.find({30003:1}).skip(0).limit(10).sort({'user_id':1}).explain()
"winningPlan" : {
	"stage" : "LIMIT",
	"limitAmount" : 10,
	"inputStage" : {
		"stage" : "FETCH",
		"filter" : {
			"30003" : {
				"$eq" : 1
			}
		},
		"inputStage" : {
			"stage" : "IXSCAN",
			"keyPattern" : {
				"user_id" : 1
			},
			...
		}
	}
}

//db.user_portrait_tag_list_20170117.explain().count({30003:1})
"winningPlan" : {
	"stage" : "COUNT",
	"inputStage" : {
		"stage" : "COLLSCAN",
		"filter" : {
			"30003" : {
				"$eq" : 1
			}
		},
		"direction" : "forward"
	}
}

结果很清晰,find是LIMIT/FETCH/IXSCAN,其中FETCH是根据索引检索doc。

而count是COLLSCAN,它是全表扫描。(没有query的count依然很快,因为它走的state是COUNT,专门运算方式) 结论大部分的时间,都花在了count上

还有一个隐藏的skip在大数据量时会出现问题,文档说明如下,

The cursor.skip() method is often expensive because it requires the server to walk from the beginning of the collection or index to get the offset or skip position before beginning to return results. As the offset (e.g. pageNumber above) increases, cursor.skip() will become slower and more CPU intensive. With larger collections, cursor.skip() may become IO bound.

发现skip在分页时,是一条一条数过来的,所有当偏移量大的时候,它就会很耗时。

解决方案

那么问题已经慢慢浮现了:

  1. 由于tag_id没法加索引,我们只能通过规避count来减少查询时间。
  2. skip在偏移量很大时,会很耗时。

对应第一个问题,技术上无法满足时,我们可以换位思考问题。平时我们在刷微博的时候,假设它们的信息是存储在mongo上,我们的交互是通过上拉刷新,然后刷出最近20条信息。这样就避免查询出total数。

对应第二个问题,依然假设微博,由于每条信息都会有时间戳,直接查询大于当前时间,再limit取出数据。直接避免skip。

因此,我们可以仿照这种思路,对应现有的用户画像展示,table分页时只保留前一页和后一页,规避total数查询;分页查询时通排序user_id后,第二页数据user_id大于第一页最大的user_id,来规避skip。

注意,我们这里的排序是通过索引的排序。在MongoDB中,排序操作可以通过从索引中 按照索引顺序获取文档的方式 来保证结果的有序性,否则内存排序需要大量内存。参考

同时,最近在做的实时数据,也需要进行大数据量的排序、分页。就可以参考上面链接里的联合索引,进行查询。

思考

其实,日常遇到的一些问题,一时我们没有思路,无法解决。大部分是没有搞清楚真正问题在哪,即那些隐藏在表面下的东西。 想到了以前读过的一本书:你的灯还亮着吗,它以故事的方式,引导我们通过不同的角度,去思考一个问题。软件开发也是如此,会遇到各种各样的问题,我们要去发现本质的问题,就一定要一查到底,当问题慢慢清晰后,自然会找到解决办法。