查询view
合法的view定义必须包括一个map函数,具体来说就是包括一个可以把信息以行为划分进行输出的emit()函数。emit()函数所使用的key就是这个view允许的查询条件范围。
Key在查询view时作为过滤引擎使用,具体来说这么用:
- 单独的key ———显示精确匹配这个key的所有结果。
-
若干key ———显示匹配这若干个key中任意某个的所有结果 。
-
一个范围内的key —— 显示匹配在这个范围内任意某个key的的所有结果。
在查询view时,可以用一系列参数来特化view的查询操作以及最终返回的结果,比如限定条件,限定数量,排序等等。
一个没有任何额外参数特化的view查询会按照以下规则执行:
-
执行完整的view查询,比如说所有bucket下的文档都会经过view的处理。
-
Admin界面下返回结果限定10个,REST API下则无数量限制。
-
如果view内定义了reduce函数就使用它.
-
按照UTF-8的比较规则对结果按照升序返回。
view结果按照一个特定顺序处理查询时的参数。处理方式决定了view查询的写法和最终的返回结果。
![](https://i-blog.csdnimg.cn/blog_migrate/3aeb432ac0070f70b07125061fbb7f12.png)
无论是REST API接口还是客户端的SDK,查询语句的核心参数和筛选系统都是一样的。对这些参数的具体设置方法根据客户端不同而有所区别,不过参数名称以及支持类型在所有环境下都是一样的。
使用REST API查询view
可以通过REST API终端查询view。REST API支持并使用HTTP协议,而客户端也是使用相同的协议来获取数据。
筛选信息
Couchbase Server 支持诸多筛选方式。Key筛选是在view结果产生之后(包括redution函数),也在产生结果排序之后才开始进行。
给key筛选引擎使用的必须是JSON值。比如说,指定一个单独的key,如果这是个字符串,那么必须包含在引号内,写作"key"。
当使用参数进行key筛选时,key必须符合view中emit()函数所使用key的格式。比如说emit()函数使用了一个序列或者hash值之类的复合key,那么查询时使用的key也要匹配复合的方式。
Key筛选支持下列方式:
- 单独的key
使用参数key指明使用单独key。此时view查询仅返回精确匹配这个key的结果。
比如说,你以“tomato”为key查询,那么就只有完全匹配“tomato”的记录会被筛选出并返回,而“tomatoes”这种就无法匹配因而不会返回。
- Key 列表
使用参数keys指明要使用一串key筛选。此时序列中每一个key都会被当作单独的key进行匹配,而key之间用逻辑符or连接。
比如说,keys值是["tomato","avocado"],那么返回的结果就是精确匹配“tomato“或者”avocado“的记录。
当使用这个查询选项时,输出结果并不是按照key排序的。这是因为对value进行key排序需要在返回结果前收集并排序所有记录。(也就是排序操作在最后返回结果的那一步执行)
在使用复合key的情况下,每一个复合key都要在查询中指明,比如说:
```
keys=[["tomato",20],["avocado",20]]
```
- Key 范围
以startkey起始, endkey结束的key范围。这两个参数可以分开用或合起来用:
* `startkey` only
直到view检索到不小于`startkey`的key值时开始输出结果,直到view过程结束。
* `endkey` only
直到view检索到不小于`endkey`的key值时停止输出结果。
* `startkey` and `endkey`
从不小于`startkey`的key值开始输出结果,从第一个不小于`endkey`的key值停止输出结果。
使用endkey时,inclusive_end选项指定了输出是否包括最终的endkey(默认包括)。如果设置为false,输出将在endkey之前的最后一个记录停止输出。
匹配算法对于不完整key值也是生效的,这是key范围查询所必须的。
包括key的复合筛选
如果使用的view产生了复合key,比如说把日期划分为年月日分别输出,那么筛选时候的key也必须匹配这个格式和大小。
看下面的view结果:
{"total_rows":5693,"rows":[
{"id":"1310653019.12667","key":[2011,7,14,14,16,59],"value":null},
{"id":"1310662045.29534","key":[2011,7,14,16,47,25],"value":null},
{"id":"1310668923.16667","key":[2011,7,14,18,42,3],"value":null},
{"id":"1310675373.9877","key":[2011,7,14,20,29,33],"value":null},
{"id":"1310684917.60772","key":[2011,7,14,23,8,37],"value":null},
{"id":"1310693478.30841","key":[2011,7,15,1,31,18],"value":null},
{"id":"1310694625.02857","key":[2011,7,15,1,50,25],"value":null},
{"id":"1310705375.53361","key":[2011,7,15,4,49,35],"value":null},
{"id":"1310715999.09958","key":[2011,7,15,7,46,39],"value":null},
{"id":"1310716023.73212","key":[2011,7,15,7,47,3],"value":null}
]
}
使用key筛选就必须写全, i.e.:
?key=[2011,7,15,7,47,3]
如果只写了日期:
?key=[2011,7,15]
此时view就不会返回任何结果,因为没有匹配的记录。如果其实你这么写是因为需要进行范围查询,那么正确的方式是采用key范围查询的语法:
?startkey=[2011,7,15,0,0,0]&endkey=[2011,7,15,99,99,99]
此时会返回符合这个范围内的所有结果。
范围key下的局部筛选
对key的匹配按照从左到右的优先顺序并满足startkey和/或endkey规定的范围。因此长度不一样的字符串也可以按照匹配规则处理了。
比如说有下面这些view数据:
"a",
"aa",
"bb",
"bbb",
"c",
"cc",
"ccc"
"dddd"
指定”aa“作为startkey,返回结果就是下面的七个记录:
"aa",
"bb",
"bbb",
"c",
"cc",
"ccc",
"dddd"
字符串匹配时遵循从左到右的顺序进行比较(通用的字符串比较方式)。指定”d“作为startkey就会返回:
"dddd"
因为第一个字符不小于”d“的只有”dddd“,所有就得到了这样的结果。
比较更长的字符串和复合值的时候也是同样的匹配算法。比如说在数据库中搜索”almond“为startkey的调料,最终返回结果包括"almond","almonds"和"almond essence"。
如果需要查询匹配某个单词或值的整个范围,那么需要在endkey中加入null值。举个例子,在所有记录中搜索仅以单词"almond"开头的记录,那就指定startkey为"almond",指定endkey为"almond\u02ad"(i.e.以最后一个拉丁字符收尾)。如果是Unicode编码的字符串,你或许应该使用"\uefff"。
startkey="almond"&endkey="almond\u02ad"
这个例子中输出从”almond“开始,在词典排序大于almond的位置停止输出。尽管"almond\02ad"这样的值是永远不会出现的,但是当key大于这个值时候就会停止输出。
事实上,这种写法的效果是前缀查询,也即所有匹配这个前缀的记录会被输出。
复合key的局部筛选
复合key(比如说序列或哈希值)可以对view输出进行筛选,而且匹配的优先级别可以用来产生复杂的筛选范围。比如说如果时间按照下述方式映射:
[year,month,day,hour,minute]
精确的日期时间范围可以通过指定特定的key值完成筛选。比如说我们要获取2011.4.1 00:00至2011.9.30 23:59之间的数据:
?startkey=[2011,4,1,0,0]&endkey=[2011,9,30,23,59]
实际上key的结构是弹性的,灵活使用startkey和endkey可以帮助你完成各种方式的筛选。比如说现在我们获取今年直到3.5的数据:
?startkey=[2011]&endkey=[2011,3,5,23,59]
也可以从某一天开始查询直到月末:
?startkey=[2011,3,16]&endkey=[2011,3,99]
在上面这个例子中,endkey中的day值是个不可能取得的值,不过结合匹配算法就可以只输出三月的数据了,跟字符串匹配规则其实是一样的。
这个结构也並非就毫无规则可言,我们必须确保序列中的缺少的值是集中在序列末尾,也即不能从左到右跳过某些值而设置靠后的值。比如说筛选每天从10am到2pm的记录,你不能这么写:
?startkey=[null,null,null,10,0]&endkey=[null,null,null,14,0]
分析一下这个逻辑,其实就是不区分年月,所以我们应该这么写:
?startkey=[0,0,0,10,0]&endkey=[9999,99,99,14,0]
这么写就可以实现预期的功能了。
分页
对返回结果的分页可以通过使用skip和limit实现。比如说获取view结果的前10个:
?limit=10
再获取之后的10个:
?skip=10&limit=10
在数据库中选项skip其实就是跳过这些数量的结果后再开始返回剩余的结果,直至返回数量达到选项limit的限制(如果设置了选项limit的话)。
当使用更大的skip值进行分页时,跳过数据的开销比较大。更高效的解决方法是首先结合limit选项进行第一次查询并记录下这次查询中最后一个结果的document ID,然后使用startkey_docid在第二次查询中指定从哪个文档开始,先跳过这个文档然后从下一个文档开始输出结果。
我们来实际演示一下,目标是跳过前10个记录,获取之后的10个记录。
首先进行第一次查询,获取第10个记录的document ID:
?startkey="carrots"&limit=10
然后使用刚才拿到的ID进行第二次查询:
?startkey="carrots"&startkey_docid=DOCID&skip=1&limit=10
使用startkey_docid时必须同时使用startkey参数。通过startkey_docid参数,数据库会直接通过内部的B-Tree检索到这个ID,这个速度远快于直接使用skip和limit的方式。
聚合查询
如果你使用序列形式的复合key来查询view,那么可以使用reduce()函数来对输出结果进行一定程度的聚合。
当可以进行聚合时,view的输出会根据key序列进行聚合,而你可以使用参数group_level指定聚合的程度。
![](https://i-blog.csdnimg.cn/blog_migrate/421e4b0f705c168895bc422757084b77.png)
group_level参数指定聚合在key序列的第几位发生(位置从1开始),并且根据聚合程度分别输出聚合后的结果集:
-
0 就是直接返回整个结果集。
-
1就是按照序列第一位将结果集区分开并返回。上面那个示意图中这个级别就是把数据按照年区分开。
-
n级别的group level就是将结果集按照前n项的不同划分开。
只要你使用了序列形式的复合key,那么就可以使用聚合功能对输出结果进行处理。
聚合筛选
如果要使用key, keys, 或 startkey / endkey进行聚合筛选,那么最起码查询的key值应该符合聚合格式(以及聚合相应需要的元素数量)。
比如说使用下述的map()函数输出以时间序列为检索条件的信息:
function(doc, meta)
{
emit([doc.year, doc.mon, doc.day], doc.logtype);
}
如果你需要group_level=2,那么你查询的key至少要包含年和月的信息。比如说你要查询2012年8月的信息:
?group=true&group_level=2&key=[2012,8]
你也可以查询一个时间段内的信息,比如说2012年2月到8月的信息:
?group=true&group_level=2&startkey=[2012,2]&endkey=[2012,8]
在进行高级别聚合时也是可以进行更细致的查询的,也就是先按照查询逻辑得到一个结果集,然后再按照聚合等级把结果集区分开。比如说我们需要按照年/月的聚合度来区分两个特定日期之间的信息:
?group=true&group_level=2&startkey=[2012,2,15]&endkey=[2012,8,10]
但是如果复合key的元素个数少于聚合程度所需要的,就会根据筛选引擎对startkey和endkey的处理方式产生无法预期的结果。
排序
所有view输出的结果都是依据key值自动排序的,而排序主要依据下述的大小排列(由小到大):
-
null
-
false
-
true
-
Numbers
-
Text (大小写敏感,小写优先, UTF–8 order)
-
Arrays (按从左到右顺序依次比较对应位置元素)
-
Objects (按照从左到右顺序一次比较对应位置key值)
这种内置的排序方式很接近对文本和数字的通用排序方式。
没有外语排序进行特殊处理是否正确,一样基于UTF-8编码排序。
你可以使用选项descending来令输出结果按照降序排列。
因为筛选结果其实实在排序之后执行的,所以如果你设置了降序,那么相应你的查询语句也必须以降序为基准来指定key的起始范围。比如说你要查询”tomato“和”zucchini“之间的调料:
?startkey="tomato"&endkey="zucchini"
因为默认是升序,所以这么写我们就得到了正确的返回结果。
如果返回的结果是降序的:
?descending=true&startkey="tomato"&endkey="zucchini"
此时我们就只能得到匹配”tomato“的结果了。因为此时”tomato“才是筛选时候最后看到的,以此为使自然也就只能得到它自己匹配的结果了。
正确的写法必须交换起始key:
?descending=true&startkey="zucchini"&endkey="tomato"
这就对了。
View的输出和筛选是大小写敏感的。如果你指明要精确匹配”Apple“,那么就不会返回匹配到”apple“或”APPLE“之类仅在大小写上有所区分的结果。所以对view的输出结果和查询时候的条件进行统一的大小写处理会让你省心不少,不必考虑大小写的麻烦事。
理解view的排序规则
Couchbase Server使用基于Unicode的排序算法,所以你应该明确具体的排序方法。大多数开发者习惯基于ASCII的比特序,当然这也是大多数编程语言在对字符串排序时候的方法。
下面是ASCII中的排序优先级:
123456890 < A-Z < a-z
这种方式意味着任何以数字开头的记录都会排在以字母开头的记录前;任何以大写字母开头的记录都会排在以小写字母开头的记录前。这也就是说”Apple“永远排在”apple“前,”Zebra“也在”apple“前。我们把这种方式和couchbase所用的基于Unicode排序的方式比较一下:
123456790 < aAbBcCdDeEfFgGhH...
注意这里也是数字开头的要排在字母开头的前面。不过大小写之间的排序就变化了。这也就是说”apple“会在”Apple“之前,也在”Zebra“之前。另外重音字符按照下述规则排序:
a < á < A < Á < b
这也就是说所有a开头的以及其重音变体都会出现在A开头和其重音变体之前。
排序示例
字节序下,索引中的key会按照下述规则排列:
"ABC123" < "ABC223" < "abc123" < "abc223" < "abcd23" < "bbc123" < "bbcd23"
同样的key在couchbase的Unicode排序下会这么排列:
"abc123" < "ABC123" < "abc223" < "ABC223" < "abcd23" < "bbc123" < "bbcd23"
这对于你理解通过startkey和endkey来获得一个范围内的结果非常重要。因为基于字节序和基于Unicode序按照相同起始条件所获得的结果是不一样的。
排序和查询示例
下述示例演示了Couchbase中Unicode排序在范围查询中的实际效果。示例基于Couchbase自带的beer-sample数据。
假设你希望获取所有以Y开头的啤酒厂。那么你的查询语句应该是这样的:
startkey="Y"&endkey="z"
如果你希望啤酒厂的开头是y或Y,那么就要这么写:
startkey="y"&endkey="z"
这将返回所有以y到z之间字符开头的结果,但是因为不包括z,所以其实就是y和Y。如果要获取所有以y开头啤酒厂,那么就终止与Y:
startkey="y"&endkey="Y"
如果你实际这么去查询了就会发现,在beer-sample中没有一个啤酒厂是以y开头的。
异常处理
There are a number of parameters that can be used to help control errors and responses during a view query.
在查询发生错误时有若干参数可以对此时的错误处理和消息回复进行设定。
- on_error
on_error 参数指定view的输出结果是否遇到错误就停止,还是说仅在发生错误的节点停止输出,其它节点正常输出。
当返回view查询的信息时,默认会把查询中产生的错误合并在回复的JSON文档中,通过这种方式避免错误影响整个查询操作。这样的处理方式保证了部分节点的故障不会过分影响整个集群的查询操作。
为了便于理解,下面就是个把错误合并在回复中的JSON文档:
{
"errors" : [
{
"from" : "http://192.168.1.80:9503/_view_merge/?stale=false",
"reason" : "req_timedout"
},
{
"from" : "http://192.168.1.80:9502/_view_merge/?stale=false",
"reason" : "req_timedout"
},
{
"from" : "http://192.168.1.80:9501/_view_merge/?stale=false",
"reason" : "req_timedout"
}
],
"rows" : [
{
"value" : 333280,
"key" : null
}
]
}
如果不希望这种默认的错误处理方式,那么可以修改on_error参数。这个参数的默认值是continue。如果我们把这个值设置为stop,那么view的查询操作一旦发生错误就会直接终止。此时返回的JSON文档就仅包含第一次发生错误的节点信息。比如这样子的:
```
{
"errors" : [
{
"from" : "http://192.168.1.80:9501/_view_merge/?stale=false",
"reason" : "req_timedout"
}
],
"rows" : [
{
"value" : 333280,
"key" : null
}
]
}
```