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/ 开头 - 例如,_design/application。除此之外,它只是一个普通的 CouchDB 文档。为了确保 CouchDB 理解您正在定义一个视图,您需要以特殊格式准备该设计文档的内容。以下是一个示例

{
    "_id": "_design/application",
    "_rev": "1-C1687D17",
    "views": {
        "viewname": {
            "map": "function(doc) { ... }",
            "reduce": "function(keys, values) { ... }"
        }
    }
}

我们正在定义一个名为 viewname 的视图。视图的定义包括两个函数:映射函数和归约函数。指定归约函数是可选的。我们将在后面讨论函数的性质。请注意,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 视图,您需要对以下 URI 执行 HTTP GET 请求

/database/_design/application/_view/viewname

database 是您创建设计文档的数据库的名称。接下来是设计文档名称,然后是视图名称,前面加上 _view/。要查询 anotherview,请将该 URI 中的 viewname 替换为 anotherview。如果您想查询不同设计文档中的视图,请调整设计文档名称。

3.2.4.1.3. MapReduce 函数

MapReduce 是一种通过应用一个两步过程来解决问题的概念,该过程恰如其分地命名为映射阶段和归约阶段。映射阶段逐个查看 CouchDB 中的所有文档,并创建一个 映射结果。映射结果是键值对的有序列表。编写映射函数的用户可以指定键和值。映射函数可以为每个文档调用内置的 emit(key, value) 函数 0 到 N 次,为每次调用创建一个映射结果中的行。

CouchDB 足够智能,可以为每个文档仅运行一次映射函数,即使在对视图的后续查询中也是如此。只有文档的更改或新文档需要重新处理。

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}
]}

它是一个按键值排序的行列表。id 是自动添加的,并引用创建此行的文档。value 是您要查找的数据。例如,它是女孩的年龄。

生成此结果的映射函数是

function(doc) {
    if(doc.name && doc.age) {
        emit(doc.name, doc.age);
    }
}

它包含 if 语句作为健全性检查,以确保我们正在操作正确的字段,并使用名称和年龄作为键和值调用 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);
    }
}

这将创建一个按 value 字段中的数据排序的具有 value 字段的文档列表。要查找与“searchterm”匹配的所有记录,我们需要查询视图并指定搜索词作为查询参数

/database/_design/application/_view/viewname?key="searchterm"

考虑上一节中的文档,假设我们正在对文档的 age 字段进行索引以查找所有五岁儿童

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 类型为图像的文档。

解决方案与前面的示例非常相似:我们只需要一个比第一个更聪明的映射函数。但首先,一个示例文档

{
    "_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 中执行此操作?

用例:从您的数据中计算派生值。

我们还没有解释归约函数。归约函数类似于 SQL 中的聚合函数。它们计算多个文档上的值。

为了解释归约函数的机制,我们将创建一个没有太大意义的函数。但这个例子很容易理解。我们将在后面探讨更实用的归约。

归约函数对映射函数的输出(也称为映射结果或中间结果)进行操作。归约函数的工作,毫不奇怪,是减少映射函数生成的列表。

以下是我们的求和归约函数的样子

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;
}

注意

不要错过有效的内置 归约函数,如 _sum_count

此归约函数接受两个参数:一个键列表和一个值列表。为了我们的求和目的,我们可以忽略键列表,只考虑值列表。我们正在遍历列表并将每个项目添加到一个运行总计中,我们在函数结束时返回该总计。

您会看到映射函数和归约函数之间的一个区别。映射函数使用 emit() 来创建其结果,而归约函数返回一个值。

例如,从一个指定年龄的整数列表中,计算新闻标题 “786 年生命出现在活动中。” 中所有生命年的总和。有点牵强,但非常简单,因此适合演示目的。考虑本文档前面使用的文档和映射视图。

计算所有女孩的总年龄的归约函数是

function(keys, values) {
    return sum(values);
}

请注意,我们没有使用前面两个版本,而是使用了 CouchDB 的预定义 sum() 函数。它与另外两个函数做的事情相同,但它是一段如此常见的代码,以至于 CouchDB 将其包含在内。

现在我们的归约视图的结果如下所示

{"rows":[
    {"key":null,"value":15}
]}

我们所有文档中所有 age 字段的总和为 15。正是我们想要的。结果对象的 key 成员为 null,因为我们无法再知道哪些文档参与了归约结果的创建。我们将在后面介绍更高级的归约案例。

一般来说,reduce 函数应该将结果缩减为单个标量值。也就是说,一个整数;一个字符串;或者一个小的、固定大小的列表或对象,其中包含来自 values 参数的聚合值(或值)。它不应该仅仅返回 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 函数,因此它无法知道是否已经发出过相同的键。在这个阶段,我们必须接受这一点。为了实现唯一性,我们需要一个 reduce 函数。

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}
]}

在这种情况下,我们可以忽略 value 部分,因为它始终为 true,但结果包含所有标签的列表,并且没有重复项!

通过一个小小的改变,我们也可以充分利用 reduce 函数。让我们看看每个标签有多少个非唯一标签。为了计算标签频率,我们只需使用我们已经学过的求和方法。在 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 字段,CouchDB 将为您强制执行唯一性。

不过,有一个需要注意的地方:在分布式情况下,当您运行多个接受写入请求的 CouchDB 节点时,只能保证每个节点的唯一性,或者在 CouchDB 之外。CouchDB 将允许将两个相同的 ID 写入两个不同的节点。在复制时,CouchDB 将检测到冲突并相应地标记文档。