3.2.4. 查看SQL骑师食谱¶
这是一些常见SQL查询的集合,以及如何在CouchDB中获得相同的结果。这里要记住的关键是CouchDB根本不像SQL数据库那样工作,而且SQL世界中的最佳实践不能很好地或者根本不能转换成CouchDB。本文档的“食谱”假设您熟悉CouchDB的基本知识,例如创建和更新数据库和文档。
3.2.4.1. 使用视图¶
如何在SQL中执行此操作:
CREATE TABLE
或:
ALTER TABLE
你怎么能在CouchDB中做到这一点?
使用视图需要两个步骤。首先定义一个视图,然后查询它。这类似于使用 CREATE TABLE
或 ALTER TABLE
并使用SQL查询进行查询。
3.2.4.1.1. 定义视图¶
定义视图是通过在CouchDB数据库中创建一个特殊文档来完成的。唯一真正的特别之处是 _id
文件的 _design/
-例如,设计/应用程序。除此之外,它只是一个普通的CouchDB文档。为了确保CouchDB理解您正在定义一个视图,您需要以一种特殊的格式准备该设计文档的内容。下面是一个例子:
{
"_id": "_design/application",
"_rev": "1-C1687D17",
"views": {
"viewname": {
"map": "function(doc) { ... }",
"reduce": "function(keys, values) { ... }"
}
}
}
我们正在定义一个视图 viewname . 视图的定义由两个函数组成:map函数和reduce函数。指定reduce函数是可选的。稍后我们将讨论函数的性质。请注意 viewname 你喜欢什么都行: users
, by-name
或 by-date
只是一些例子。
一个设计文档还可以包含多个视图定义,每个视图定义都由一个唯一的名称标识:
{
"_id": "_design/application",
"_rev": "1-C1687D17",
"views": {
"viewname": {
"map": "function(doc) { ... }",
"reduce": "function(keys, values) { ... }"
},
"anotherview": {
"map": "function(doc) { ... }",
"reduce": "function(keys, values) { ... }"
}
}
}
3.2.4.1.2. 查询视图¶
设计文档的名称和视图的名称对于查询视图非常重要。查询视图的步骤 viewname ,执行HTTP GET
请求以下URI::
/database/_design/application/_view/viewname
database是您在其中创建设计文档的数据库的名称。接下来是设计文档名称,然后是前缀为的视图名称 _view/
. 查询 anotherview ,更换 viewname 在那个URI中 anotherview . 如果要查询其他设计文档中的视图,请调整设计文档名称。
3.2.4.1.3. MapReduce函数¶
MapReduce是一个通过应用两个步骤来解决问题的概念,这两个步骤被恰当地称为map阶段和reduce阶段。映射阶段逐个查看CouchDB中的所有文档,并创建一个 map result . 映射结果是键/值对的有序列表。键和值都可以由用户编写映射函数来指定。映射函数可以调用内置 emit(key, value)
每个文档0到N次函数,每次调用在映射结果中创建一行。
CouchDB足够聪明,可以对每个文档只运行一次map函数,甚至在对视图的后续查询中也是如此。只有对文档的更改或新文档需要重新处理。
3.2.4.1.4. 地图功能¶
映射函数对每个文档都是独立运行的。他们不能修改文件,不能与外界对话,也不能有副作用。这是必需的,这样CouchDB就可以保证正确的结果,而不必在只更改一个文档时重新计算完整的结果。
贴图结果如下所示:
{"total_rows":3,"offset":0,"rows":[
{"id":"fc2636bf50556346f1ce46b4bc01fe30","key":"Lena","value":5},
{"id":"1fb2449f9b9d4e466dbfa47ebe675063","key":"Lisa","value":4},
{"id":"8ede09f6f6aeb35d948485624b28f149","key":"Sarah","value":6}
]}
它是按key值排序的行列表。id将自动添加并引用回创建此行的文档。值就是您要查找的数据。比如说,是女孩的年龄。
产生此结果的map函数是:
function(doc) {
if(doc.name && doc.age) {
emit(doc.name, doc.age);
}
}
它包含if语句作为健全性检查,以确保我们在正确的字段上操作,并使用name和age作为键和值调用emit函数。
3.2.4.2. 按键查找¶
如何在SQL中执行此操作:
SELECT field FROM table WHERE value="searchterm"
你怎么能在CouchDB中做到这一点?
用例:获取与键(“searchterm”)关联的结果(可以是一条记录或一组记录)。
不管存储机制如何,要快速查找内容,都需要索引。索引是为快速搜索和检索而优化的数据结构。CouchDB的映射结果存储在这样一个索引中,这个索引恰好是一个B+树。
要通过“searchterm”查找值,我们需要将所有值放入视图的键中。我们只需要一个简单的映射函数:
function(doc) {
if(doc.value) {
emit(doc.value, null);
}
}
这将创建一个文档列表,其中的值字段按值字段中的数据排序。要查找与“searchterm”匹配的所有记录,我们查询视图并将搜索项指定为查询参数:
/database/_design/application/_view/viewname?key="searchterm"
考虑上一节中的文档,并假设我们在文档的“年龄”字段上索引,以找到所有五岁的孩子:
function(doc) {
if(doc.age && doc.name) {
emit(doc.age, doc.name);
}
}
查询:
/ladies/_design/ladies/_view/age?key=5
结果:
{"total_rows":3,"offset":1,"rows":[
{"id":"fc2636bf50556346f1ce46b4bc01fe30","key":5,"value":"Lena"}
]}
容易的。
请注意,您必须发出一个值。视图结果在每一行中都包含关联的文档ID。我们可以使用它从文档本身查找更多数据。我们也可以使用 ?include_docs=true
参数让CouchDB为我们获取单个文档。
3.2.4.3. 按前缀查找¶
如何在SQL中执行此操作:
SELECT field FROM table WHERE value LIKE "searchterm%"
你怎么能在CouchDB中做到这一点?
用例:查找字段值以开头的所有文档 searchterm . 例如,假设您存储了一个MIME类型(比如 text/html 或 image/jpg )对于每个文档,现在需要根据MIME类型查找所有作为图像的文档。
解决方案与上一个示例非常相似:我们只需要一个比第一个稍微聪明一点的map函数。但首先,一个示例文档:
{
"_id": "Hugh Laurie",
"_rev": "1-9fded7deef52ac373119d05435581edf",
"mime-type": "image/jpg",
"description": "some dude"
}
线索在于从文档中提取要搜索的前缀,并将其放入视图索引中。我们使用正则表达式匹配前缀:
function(doc) {
if(doc["mime-type"]) {
// from the start (^) match everything that is not a slash ([^\/]+) until
// we find a slash (\/). Slashes needs to be escaped with a backslash (\/)
var prefix = doc["mime-type"].match(/^[^\/]+\//);
if(prefix) {
emit(prefix, null);
}
}
}
现在,我们可以使用所需的MIME类型前缀查询此视图,不仅可以找到所有图像,还可以找到文本、视频和所有其他格式:
/files/_design/finder/_view/by-mime-type?key="image/"
3.2.4.4. 聚合函数¶
如何在SQL中执行此操作:
SELECT COUNT(field) FROM table
你怎么能在CouchDB中做到这一点?
用例:从数据中计算派生值。
我们还没有解释reduce函数。Reduce函数类似于SQL中的聚合函数。它们在多个文档上计算一个值。
为了解释reduce函数的机制,我们将创建一个没有太大意义的函数。但是这个例子很容易理解。稍后我们将探讨更多有用的缩减。
Reduce函数对map函数(也称为map result或intermediate result)的输出进行操作。reduce函数的工作就是减少map函数生成的列表。
下面是我们的summing reduce函数:
function(keys, values) {
var sum = 0;
for(var idx in values) {
sum = sum + values[idx];
}
return sum;
}
下面是另一个更为惯用的JavaScript版本:
function(keys, values) {
var sum = 0;
values.forEach(function(element) {
sum = sum + element;
});
return sum;
}
注解
不要错过有效的内置 reduce functions 喜欢 _sum
和 _count
这个reduce函数接受两个参数:键列表和值列表。为了求和,我们可以忽略键列表而只考虑值列表。我们在列表中循环,并将每个项添加到函数末尾返回的运行总数中。
您将看到map和reduce函数之间的一个区别。map函数使用 emit()
创建其结果,而reduce函数返回一个值。
例如,从指定年龄的整数值列表中,计算新闻标题所有使用年限的总和, “786 life years present at event.” 有点做作,但非常简单,因此很适合演示。考虑一下我们在本文档前面使用的文档和映射视图。
计算所有女孩总年龄的缩减函数为:
function(keys, values) {
return sum(values);
}
注意,我们使用CouchDB的预定义版本,而不是两个早期版本 sum()
功能。它做的事情和另外两个一样,但是它是一个非常常见的代码段,CouchDB将它包含在其中。
我们的reduce视图的结果如下所示:
{"rows":[
{"key":null,"value":15}
]}
我们所有文档中所有年龄字段的总和是15。正是我们想要的。result对象的关键成员为null,因为我们无法再知道哪些文档参与了简化结果的创建。我们以后再处理更多的案子。
根据经验,reduce函数应该减少到单个标量值。也就是说,一个整数、一个字符串、一个小的、固定大小的列表或对象,其中包含values参数中的一个或多个聚合值。它绝不应该只返回值或类似值。如果您试图使用reduce“错误的方式”,CouchDB会给您一个警告:
{
"error":"reduce_overflow_error",
"message":"Reduce output must shrink more rapidly: Current output: ..."
}
3.2.4.5. 获取唯一值¶
如何在SQL中执行此操作:
SELECT DISTINCT field FROM table
你怎么能在CouchDB中做到这一点?
获取唯一值不像添加关键字那么容易。但是reduce视图和一个特殊的查询参数会得到相同的结果。假设你想要一个你的用户已经给自己做了标记的标签列表,并且没有重复的标签。
首先,让我们看一下源文档。我们继续 _id
和 _rev
此处的属性:
{
"name":"Chris",
"tags":["mustache", "music", "couchdb"]
}
{
"name":"Noah",
"tags":["hypertext", "philosophy", "couchdb"]
}
{
"name":"Jan",
"tags":["drums", "bike", "couchdb"]
}
接下来,我们需要所有标签的列表。一个map函数可以做到:
function(doc) {
if(doc.name && doc.tags) {
doc.tags.forEach(function(tag) {
emit(tag, null);
});
}
}
结果如下:
{"total_rows":9,"offset":0,"rows":[
{"id":"3525ab874bc4965fa3cda7c549e92d30","key":"bike","value":null},
{"id":"3525ab874bc4965fa3cda7c549e92d30","key":"couchdb","value":null},
{"id":"53f82b1f0ff49a08ac79a9dff41d7860","key":"couchdb","value":null},
{"id":"da5ea89448a4506925823f4d985aabbd","key":"couchdb","value":null},
{"id":"3525ab874bc4965fa3cda7c549e92d30","key":"drums","value":null},
{"id":"53f82b1f0ff49a08ac79a9dff41d7860","key":"hypertext","value":null},
{"id":"da5ea89448a4506925823f4d985aabbd","key":"music","value":null},
{"id":"da5ea89448a4506925823f4d985aabbd","key":"mustache","value":null},
{"id":"53f82b1f0ff49a08ac79a9dff41d7860","key":"philosophy","value":null}
]}
所有的标签都是承诺的,包括所有的副本。由于每个文档都是通过map函数单独运行的,因此它无法知道是否已经发出了相同的键。在这个阶段,我们需要接受这一点。为了实现唯一性,我们需要减少:
function(keys, values) {
return true;
}
此reduce不执行任何操作,但它允许我们在查询视图时指定一个特殊的查询参数:
/dudes/_design/dude-data/_view/tags?group=true
CouchDB回复:
{"rows":[
{"key":"bike","value":true},
{"key":"couchdb","value":true},
{"key":"drums","value":true},
{"key":"hypertext","value":true},
{"key":"music","value":true},
{"key":"mustache","value":true},
{"key":"philosophy","value":true}
]}
在这种情况下,我们可以忽略值部分,因为它总是真的,但是结果包括所有标记的列表,没有重复项!
只要稍作改动,我们也能很好地利用减量。让我们看看每个标记有多少个非唯一标记。为了计算标签频率,我们只需要使用我们已经学过的总结。在map函数中,我们发出1而不是null:
function(doc) {
if(doc.name && doc.tags) {
doc.tags.forEach(function(tag) {
emit(tag, 1);
});
}
}
在reduce函数中,我们返回所有值的总和:
function(keys, values) {
return sum(values);
}
现在,如果我们用 ?group=true
参数,则返回每个标记的计数:
{"rows":[
{"key":"bike","value":1},
{"key":"couchdb","value":3},
{"key":"drums","value":1},
{"key":"hypertext","value":1},
{"key":"music","value":1},
{"key":"mustache","value":1},
{"key":"philosophy","value":1}
]}
3.2.4.6. 强制执行唯一性¶
如何在SQL中执行此操作:
UNIQUE KEY(column)
你怎么能在CouchDB中做到这一点?
用例:应用程序要求某个值在数据库中只存在一次。
这很简单:在CouchDB数据库中,每个文档都必须具有唯一的 _id
字段。如果需要数据库中的唯一值,只需将它们分配给文档的 _id
field和CouchDB将为您强制执行唯一性。
不过,有一点需要注意:在分布式情况下,当您运行多个接受写请求的CouchDB节点时,只能保证每个节点或CouchDB之外的节点的唯一性。CouchDB将允许将两个相同的id写入两个不同的节点。在复制时,CouchDB将检测到冲突并相应地标记文档。