4. 分区数据库

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

作为引入分区数据库的一种方法,我们将考虑一个激励性的用例来描述这个特性的好处。在这个例子中,我们将考虑一个数据库,该数据库存储来自一个大型土壤湿度传感器网络的读数。

注解

在阅读本文档之前,您应该熟悉 theory 属于 sharding 在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 分区键。

一个好的分区有两个基本属性。首先,它应该有很高的基数。也就是说,一个大的分区数据库应该比任何单个分区中的文档拥有更多的分区。对于这个特性,只有一个分区的数据库是一个反模式。其次,每个分区的数据量应该是“小”的。一般建议将单个分区的数据量限制在10gb以下。以传感器文档为例,这相当于大约60000年的数据。

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 .

注解

分区数据库中的每个文档(除了“设计”和“本地文档”)都必须具有以下格式“分区:docid”. 更具体地说,给定文档的分区是第一个冒号之前的所有内容。文档id是第一个冒号之后的所有内容,其中可能包含更多冒号。

注解

系统数据库(如用户)是 not 允许被分割。这是因为系统数据库对文档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中的分区名并不神奇。在内部,数据库只是使用分区将文档散列到给定的shard,而不是整个文档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 请求。访问 _all_docs 通过 /dbname/_partition/name/_all_docs 端点主要是为了方便起见,这样可以保证请求的作用域是给定的分区。用户可以自由使用 /dbname/_all_docs 从多个分区读取文档。两种查询样式具有相同的性能。

接下来,我们将创建一个包含索引的设计文档,用于从给定传感器获取所有读数。map函数与前面的示例类似,只是我们考虑了文档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稍作修改,以及使用稍微不同的路径访问视图。然而,对于任何喜欢性能改进的人来说,这实际上是一件大事。通过知道视图结果都位于提供的分区名称中,我们的分区查询现在的执行速度几乎与文档查找一样快!

我们最后要讨论的是如何跨多个分区查询数据。为此,我们将在初始示例中实现示例sensors by field查询。映射功能将使用相同的更新来说明新的文档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"
}

注解

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

注解

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

若要查看显示某个领域中所有传感器的请求,我们将使用如下请求:

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 参数在设计文档、全局设计文档和查询中的行为与非分区数据库上的设计文档的行为相同。

警告

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