4. 分区数据库

分区数据库通过使用分区键将文档划分为逻辑分区。所有文档都被分配到一个分区,并且许多文档通常被赋予相同的分区键。分区数据库的好处是,当查找匹配的文档时,辅助索引可以显著提高效率,因为它们的条目包含在它们的分区中。这意味着给定的辅助索引读取将只扫描单个分区范围,而不是必须从每个分片的副本中读取。

为了介绍分区数据库,我们将考虑一个激励用例来描述此功能的好处。在这个例子中,我们将考虑一个存储来自大型土壤水分传感器网络的读数的数据库。

注意

在阅读本文档之前,您应该熟悉 CouchDB 中的 理论分片

传统上,此数据库中的文档可能具有以下结构

{
    "_id": "sensor-reading-ca33c748-2d2c-4ed1-8abf-1bca4d9d03cf",
    "_rev":"1-14e8f3262b42498dbd5c672c9d461ff0",
    "sensor_id": "sensor-260",
    "location": [41.6171031, -93.7705674],
    "field_name": "Bob's Corn Field #5",
    "readings": [
        ["2019-01-21T00:00:00", 0.15],
        ["2019-01-21T06:00:00", 0.14],
        ["2019-01-21T12:00:00", 0.16],
        ["2019-01-21T18:00:00", 0.11]
    ]
}

注意

虽然此示例使用物联网传感器,但要考虑的主要因素是文档的逻辑分组。类似的用例可能是按用户分组的文档或按实验分组的科学数据。

因此,我们有一堆传感器,它们都按其监控的字段分组,以及它们在特定日期(或其他适当时间段)的读数。

除了我们的文档之外,我们可能希望为查询我们的数据库定义两个辅助索引,它们可能看起来像

function(doc) {
    if(doc._id.indexOf("sensor-reading-") != 0) {
        return;
    }
    for(var r in doc.readings) {
        emit([doc.sensor_id, r[0]], r[1])
    }
}

function(doc) {
    if(doc._id.indexOf("sensor-reading-") != 0) {
        return;
    }
    emit(doc.field_name, doc.sensor_id)
}

使用这两个定义的索引,我们可以轻松地找到给定传感器的所有读数,或列出给定字段中的所有传感器。

不幸的是,在 CouchDB 中,当我们从这两个索引中读取时,它需要找到每个分片的副本并询问与特定传感器或字段相关的任何文档。这意味着随着我们的数据库扩展到更多分片,每个索引请求都必须执行更多工作,这是不必要的,因为我们只对少量文档感兴趣。幸运的是,亲爱的读者,分区数据库的创建是为了解决这个确切的问题。

4.1. 什么是分区?

在上一节中,我们介绍了一个假设的数据库,其中包含来自物联网现场监控服务的传感器读数。在这个特定用例中,将所有文档按其 sensor_id 字段分组是相当合理的。在这种情况下,我们将 sensor_id 称为分区键。

一个好的分区具有两个基本属性。首先,它应该具有高基数。也就是说,大型分区数据库应该拥有比任何单个分区中的文档更多的分区。具有单个分区的数据库将是此功能的反模式。其次,每个分区的​​数据量应该“小”。一般建议是将单个分区限制在不超过十吉字节(10 GB)的数据。对于传感器文档的示例,这相当于大约 60,000 年的数据。

注意

CouchDB 中的 max_partition_size 决定了分区限制。此选项的默认值为 10GiB,但可以根据需要进行更改。将此选项的值设置为 0 将禁用分区限制。

4.2. 为什么使用分区?

使用分区数据库的主要好处是提高分区查询的性能。具有大量文档的大型数据库通常具有类似的模式,其中存在一起查询的相关文档组。

通过使用分区,我们可以通过将整个组放置在磁盘上的特定分片中,更有效地对这些单独的文档组执行查询。因此,视图引擎在执行查询时只需要查询给定分片范围的一个副本,而不是跨数据库中的所有 q 个分片执行查询。这意味着您不必等待所有 q 个分片响应,这既高效又快。

4.3. 分区示例

要创建分区数据库,我们只需传递一个查询字符串参数

shell> curl -X PUT http://127.0.0.1:5984/my_new_db?partitioned=true
{"ok":true}

要查看我们的数据库是否已分区,我们可以查看数据库信息

shell> curl http://127.0.0.1:5984/my_new_db
{
  "cluster": {
    "n": 3,
    "q": 8,
    "r": 2,
    "w": 2
  },
  "compact_running": false,
  "db_name": "my_new_db",
  "disk_format_version": 7,
  "doc_count": 0,
  "doc_del_count": 0,
  "instance_start_time": "0",
  "props": {
    "partitioned": true
  },
  "purge_seq": "0-g1AAAAFDeJzLYWBg4M...",
  "sizes": {
    "active": 0,
    "external": 0,
    "file": 66784
  },
  "update_seq": "0-g1AAAAFDeJzLYWBg4M..."
}

您现在将看到 "props" 成员包含 "partitioned": true

注意

分区数据库中的每个文档(除了 _design 和 _local 文档)都必须具有“分区:docid”格式。更具体地说,给定文档的分区是第一个冒号之前的部分。文档 ID 是第一个冒号之后的部分,其中可能包含更多冒号。

注意

系统数据库(如 _users)**不允许**进行分区。这是因为系统数据库已经对文档 ID 具有自己的不兼容要求。

现在我们已经创建了一个分区数据库,是时候添加一些文档了。使用我们之前的示例,我们可以这样做

shell> cat doc.json
{
    "_id": "sensor-260:sensor-reading-ca33c748-2d2c-4ed1-8abf-1bca4d9d03cf",
    "sensor_id": "sensor-260",
    "location": [41.6171031, -93.7705674],
    "field_name": "Bob's Corn Field #5",
    "readings": [
        ["2019-01-21T00:00:00", 0.15],
        ["2019-01-21T06:00:00", 0.14],
        ["2019-01-21T12:00:00", 0.16],
        ["2019-01-21T18:00:00", 0.11]
    ]
}
shell> $ curl -X POST -H "Content-Type: application/json" \
            http://127.0.0.1:5984/my_new_db -d @doc.json
{
    "ok": true,
    "id": "sensor-260:sensor-reading-ca33c748-2d2c-4ed1-8abf-1bca4d9d03cf",
    "rev": "1-05ed6f7abf84250e213fcb847387f6f5"
}

第一个示例文档中唯一需要更改的是,我们现在通过将分区名称附加到旧 ID(用冒号分隔)来将分区名称包含在文档 ID 中。

注意

文档 ID 中的分区名称不是神奇的。在内部,数据库只是使用分区来对文档进行哈希到给定分片,而不是使用整个文档 ID。

在分区数据库中处理文档与非分区数据库没有区别。所有 API 都可用,并且现有的客户端代码将无缝运行。

现在我们已经创建了一个文档,我们可以获取有关包含该文档的分区的一些信息

shell> curl http://127.0.0.1:5984/my_new_db/_partition/sensor-260
{
  "db_name": "my_new_db",
  "doc_count": 1,
  "doc_del_count": 0,
  "partition": "sensor-260",
  "sizes": {
    "active": 244,
    "external": 347
  }
}

我们还可以列出分区中的所有文档

shell> curl http://127.0.0.1:5984/my_new_db/_partition/sensor-260/_all_docs
{"total_rows": 1, "offset": 0, "rows":[
    {
        "id":"sensor-260:sensor-reading-ca33c748-2d2c-4ed1-8abf-1bca4d9d03cf",
        "key":"sensor-260:sensor-reading-ca33c748-2d2c-4ed1-8abf-1bca4d9d03cf",
        "value": {"rev": "1-05ed6f7abf84250e213fcb847387f6f5"}
    }
]}

请注意,我们可以使用对 _all_docs 请求可用的所有正常功能。通过 /dbname/_partition/name/_all_docs 端点访问 _all_docs 主要是一种便利,这样可以确保请求的范围限于给定分区。用户可以自由使用正常的 /dbname/_all_docs 来从多个分区读取文档。两种查询样式都具有相同的性能。

接下来,我们将创建一个设计文档,其中包含用于获取给定传感器的所有读数的索引。映射函数与我们之前的示例类似,只是我们已经考虑了文档 ID 的更改。

function(doc) {
    if(doc._id.indexOf(":sensor-reading-") < 0) {
        return;
    }
    for(var r in doc.readings) {
        emit([doc.sensor_id, r[0]], r[1])
    }
}

上传设计文档后,我们可以尝试一个分区查询

shell> cat ddoc.json
{
    "_id": "_design/sensor-readings",
    "views": {
        "by_sensor": {
            "map": "function(doc) { ... }"
        }
    }
}
shell> $ curl -X POST -H "Content-Type: application/json" http://127.0.0.1:5984/my_new_db -d @ddoc2.json
{
    "ok": true,
    "id": "_design/all_sensors",
    "rev": "1-4a8188d80fab277fccf57bdd7154dec1"
}
shell> curl http://127.0.0.1:5984/my_new_db/_partition/sensor-260/_design/sensor-readings/_view/by_sensor
{"total_rows":4,"offset":0,"rows":[
{"id":"sensor-260:sensor-reading-ca33c748-2d2c-4ed1-8abf-1bca4d9d03cf","key":["sensor-260","0"],"value":null},
{"id":"sensor-260:sensor-reading-ca33c748-2d2c-4ed1-8abf-1bca4d9d03cf","key":["sensor-260","1"],"value":null},
{"id":"sensor-260:sensor-reading-ca33c748-2d2c-4ed1-8abf-1bca4d9d03cf","key":["sensor-260","2"],"value":null},
{"id":"sensor-260:sensor-reading-ca33c748-2d2c-4ed1-8abf-1bca4d9d03cf","key":["sensor-260","3"],"value":null}
]}

万岁!我们的第一个分区查询。对于经验丰富的用户来说,这可能不是最令人兴奋的发展,因为唯一改变的是对文档 ID 的轻微调整,以及使用略微不同的路径访问视图。但是,对于任何喜欢性能改进的人来说,这实际上是一件大事。通过知道视图结果都位于提供的分区名称中,我们的分区查询现在执行速度几乎与文档查找一样快!

我们将要查看的最后一件事是如何跨多个分区查询数据。为此,我们将实现我们初始示例中按字段分组的示例传感器查询。映射函数将使用相同的更新来考虑新的文档 ID 格式,但除此之外与之前的版本相同

function(doc) {
    if(doc._id.indexOf(":sensor-reading-") < 0) {
        return;
    }
    emit(doc.field_name, doc.sensor_id)
}

接下来,我们将使用此函数创建一个新的设计文档。请务必注意,"options" 成员包含 "partitioned": false

shell> cat ddoc2.json
{
  "_id": "_design/all_sensors",
  "options": {
    "partitioned": false
  },
  "views": {
    "by_field": {
      "map": "function(doc) { ... }"
    }
  }
}
shell> $ curl -X POST -H "Content-Type: application/json" http://127.0.0.1:5984/my_new_db -d @ddoc2.json
{
    "ok": true,
    "id": "_design/all_sensors",
    "rev": "1-4a8188d80fab277fccf57bdd7154dec1"
}

注意

分区数据库中的设计文档默认情况下是分区的。包含跨多个分区查询的视图的设计文档必须在 "options" 对象中包含 "partitioned": false 成员。

注意

设计文档要么是分区的,要么是全局的。它们不能包含分区索引和全局索引的混合。

要查看显示某个字段中所有传感器的请求,我们将使用类似的请求

shell> curl -u adm:pass http://127.0.0.1:15984/my_new_db/_design/all_sensors/_view/by_field
{"total_rows":1,"offset":0,"rows":[
{"id":"sensor-260:sensor-reading-ca33c748-2d2c-4ed1-8abf-1bca4d9d03cf","key":"Bob's Corn Field #5","value":"sensor-260"}
]}

请注意,我们没有使用 /dbname/_partition/... 路径进行全局查询。这是因为全局查询从定义上来说不涵盖单个分区。除了在设计文档中具有 "partitioned": false 参数之外,全局设计文档和查询的行为与非分区数据库上的设计文档相同。

警告

明确地说,这意味着全局查询的执行方式与非分区数据库上的查询相同。只有分区数据库上的分区查询才能从性能改进中受益。