3.2.4. 查看SQL骑师食谱

这是一些常见SQL查询的集合,以及如何在CouchDB中获得相同的结果。这里要记住的关键是CouchDB根本不像SQL数据库那样工作,而且SQL世界中的最佳实践不能很好地或者根本不能转换成CouchDB。本文档的“食谱”假设您熟悉CouchDB的基本知识,例如创建和更新数据库和文档。

3.2.4.1. 使用视图

如何在SQL中执行此操作:

CREATE TABLE

或:

ALTER TABLE

你怎么能在CouchDB中做到这一点?

使用视图需要两个步骤。首先定义一个视图,然后查询它。这类似于使用 CREATE TABLEALTER 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 你喜欢什么都行: usersby-nameby-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/htmlimage/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将检测到冲突并相应地标记文档。