Spec-Zone.ru › CouchDB 3.5

Секционированные базы данных

Секционированная база данных объединяет документы в логические секции с помощью ключа секционирования. Все документы относятся к какой-либо секции, и многим документам обычно назначается один и тот же ключ секционирования. Преимущество секционированных баз данных заключается в том, что вторичные индексы могут значительно эффективнее находить подходящие документы, поскольку их записи содержатся в соответствующей секции. Это означает, что при чтении по вторичному индексу сканируется только один диапазон секции, а не выполняется чтение из копии каждого шарда.

Чтобы познакомить вас с секционированными базами данных, рассмотрим пример использования, который поможет описать преимущества этой функции. Для этого примера возьмём базу данных, в которой хранятся показания датчиков влажности почвы из крупной сети.

Примечание

Перед прочтением этого документа вам следует ознакомиться с теорией шардирования в 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]
    ]
}

Примечание

Хотя в этом примере используются датчики IoT, главное, на что следует обратить внимание, — это логическая группировка документов. В подобных сценариях документы могут группироваться по пользователям, а научные данные — по экспериментам.

Итак, у нас есть множество датчиков, сгруппированных по полям, которые они отслеживают, а также их показания за определённый день (или другой подходящий период времени).

Помимо документов, для запросов к базе данных нам могут понадобиться два вторичных индекса, примерно такого вида:

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 при чтении по любому из этих индексов необходимо найти копию каждого шарда и запросить документы, относящиеся к определённому датчику или полю. Это означает, что по мере увеличения числа шардов в базе данных для каждого запроса к индексу требуется выполнять больше работы. Это излишне, поскольку нас интересует лишь небольшое количество документов. К счастью для вас, дорогой читатель, секционированные базы данных были созданы именно для решения этой проблемы.

Что такое секция?

В предыдущем разделе мы представили гипотетическую базу данных с показаниями датчиков из сервиса мониторинга полей с помощью IoT. В этом конкретном случае вполне логично сгруппировать все документы по полю sensor_id. В таком случае sensor_id будет ключом секционирования.

Хорошая секция обладает двумя основными свойствами. Во-первых, она должна иметь высокую кардинальность. То есть в большой секционированной базе данных секций должно быть намного больше, чем документов в любой отдельной секции. База данных с одной секцией будет антипаттерном для этой функции. Во-вторых, объём данных в каждой секции должен быть «небольшим». Обычно рекомендуется ограничивать размер отдельных секций десятью гигабайтами (10 ГБ). Для документов с показаниями датчиков из нашего примера это соответствует примерно 60 000 лет данных.

Примечание

Параметр max_partition_size в CouchDB задаёт ограничение на размер секции. Значение этого параметра по умолчанию — 10 ГиБ, но его можно изменить. Если задать для этого параметра значение 0, ограничение на размер секции будет отключено.

Зачем использовать секции?

Главное преимущество секционированных баз данных — производительность секционированных запросов. В больших базах данных с множеством документов часто встречаются группы связанных документов, к которым обращаются вместе.

С помощью секций мы можем эффективнее выполнять запросы к отдельным группам документов, размещая всю группу в определённом шарде на диске. Таким образом, при выполнении запроса движку представлений нужно обратиться только к одной копии заданного диапазона шардов, а не выполнять запрос по всем шардам q базы данных. Это означает, что вам не нужно ждать ответа от всех шардов q, что повышает эффективность и скорость работы.

Секции на примерах

Чтобы создать секционированную базу данных, достаточно передать параметр строки запроса:

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

Убедиться, что наша база данных секционирована, можно, просмотрев информацию о ней:

shell> curl http://adm:pass@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) должен иметь формат «partition:docid». Точнее говоря, секцией для данного документа является всё, что находится до первого двоеточия. Идентификатор документа — это всё, что находится после первого двоеточия; он может содержать и другие двоеточия.

Примечание

Системные базы данных (например, _users) нельзя секционировать. Это связано с тем, что у системных баз данных уже есть собственные несовместимые требования к идентификаторам документов.

Теперь, когда мы создали секционированную базу данных, пора добавить в неё документы. Используя наш предыдущий пример, можно сделать это следующим образом:

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://adm:pass@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"
}

Единственное изменение в первом примере документа состоит в том, что теперь мы добавляем имя секции к идентификатору документа: оно ставится перед прежним идентификатором и отделяется от него двоеточием.

Примечание

Имя секции в идентификаторе документа не обладает особыми свойствами. Внутри базы данных для хеширования документа и определения его шарда используется только секция, а не весь идентификатор документа.

Работа с документами в секционированной базе данных ничем не отличается от работы с документами в несекционированной базе данных. Доступны все API, а существующий клиентский код будет работать без изменений.

Теперь, когда мы создали документ, можно получить некоторую информацию о секции, в которой он находится:

shell> curl http://adm:pass@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://adm:pass@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 похожа на ту, что использовалась в предыдущем примере, за исключением того, что теперь она учитывает изменение формата идентификатора документа.

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://adm:pass@127.0.0.1:5984/my_new_db -d @ddoc.json
{
    "ok": true,
    "id": "_design/sensor-readings",
    "rev": "1-13859808da293bd72fde3b31be97372a"
}
shell> curl http://adm:pass@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}
]}

Ура! Наш первый секционированный запрос. Опытных пользователей это может не особенно впечатлить: изменились лишь формат идентификатора документа и путь для обращения к представлениям. Однако для тех, кто ценит повышение производительности, это действительно важно. Поскольку известно, что результаты представления находятся в указанной секции, секционированные запросы теперь выполняются почти так же быстро, как поиск документов!

Последнее, что мы рассмотрим, — как запрашивать данные из нескольких секций. Для этого реализуем запрос из первоначального примера, который выбирает датчики по полю. Функция map будет учитывать новый формат идентификатора документа, но в остальном останется такой же, как и предыдущая версия:

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://adm:pass@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://adm:pass@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 в документе проектирования, глобальные документы проектирования и запросы ведут себя так же, как документы проектирования в несекционированных базах данных.

Предупреждение

Иными словами, глобальные запросы выполняются так же, как запросы к несекционированным базам данных. Повышение производительности обеспечивают только секционированные запросы к секционированной базе данных.

Copyright © 2025 The Apache Software Foundation — Licensed under the Apache License 2.0
https://docs.couchdb.org/en/3.5.1/partitioned-dbs/index.html

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API