Spec-Zone.ru › CouchDB 3.5

Сборник рецептов для поклонников SQL

Здесь собраны некоторые распространённые SQL-запросы и способы получить тот же результат в CouchDB. Важно помнить, что CouchDB совсем не работает как база данных SQL, а лучшие практики из мира SQL плохо применимы или вовсе неприменимы к CouchDB. В этом «сборнике рецептов» предполагается, что вы знакомы с основами CouchDB, например с созданием и обновлением баз данных и документов.

Использование представлений

Как это сделать в SQL:

CREATE TABLE

или:

ALTER TABLE

Как это сделать в CouchDB?

Работа с представлениями состоит из двух этапов. Сначала вы определяете представление, а затем выполняете запрос к нему. Это аналогично определению структуры таблицы (с индексами) с помощью CREATE TABLE или ALTER TABLE и выполнению запроса к ней с помощью SQL-запроса.

Определение представления

Представление определяется путём создания специального документа в базе данных CouchDB. Единственная особенность такого документа — его _id, которое начинается с _design/ — например, _design/application. В остальном это обычный документ CouchDB. Чтобы CouchDB поняла, что вы определяете представление, содержимое этого дизайн-документа нужно подготовить в специальном формате. Вот пример:

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

Мы определяем представление viewname. Определение представления состоит из двух функций: map-функции и reduce-функции. Указывать reduce-функцию необязательно. Позже мы рассмотрим особенности этих функций. Обратите внимание, что viewname может быть любым: users, by-name или by-date — лишь несколько примеров.

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

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

Запрос к представлению

Имя дизайн-документа и имя представления важны для запроса к представлению. Чтобы выполнить запрос к представлению viewname, отправьте HTTP-запрос GET к следующему URI:

/database/_design/application/_view/viewname

database — это имя базы данных, в которой вы создали дизайн-документ. Далее указывается имя дизайн-документа, а затем имя представления с префиксом _view/. Чтобы выполнить запрос к anotherview, замените viewname в этом URI на anotherview. Если нужно выполнить запрос к представлению в другом дизайн-документе, измените имя дизайн-документа.

Функции MapReduce

MapReduce — это концепция решения задач с помощью двух этапов, метко названных этапом map и этапом reduce. На этапе map каждый документ в CouchDB обрабатывается отдельно, один за другим, и создаётся результат map. Результат map — это упорядоченный список пар «ключ/значение». Ключ и значение задаются пользователем, который пишет map-функцию. Map-функция может вызывать встроенную функцию emit(key, value) от 0 до N раз для каждого документа, создавая строку в результате map при каждом вызове.

CouchDB достаточно умна, чтобы запускать map-функцию для каждого документа только один раз, даже при последующих запросах к представлению. Повторно обрабатывать нужно только изменённые или новые документы.

Map-функции

Map-функции выполняются изолированно для каждого документа. Они не могут изменять документ и взаимодействовать с внешним миром — у них не может быть побочных эффектов. Это необходимо, чтобы CouchDB могла гарантировать корректные результаты, не пересчитывая их полностью при изменении всего одного документа.

Результат map выглядит так:

{"total_rows":3,"offset":0,"rows":[
{"id":"fc2636bf50556346f1ce46b4bc01fe30","key":"Lena","value":5},
{"id":"1fb2449f9b9d4e466dbfa47ebe675063","key":"Lisa","value":4},
{"id":"8ede09f6f6aeb35d948485624b28f149","key":"Sarah","value":6}
]}

Это список строк, отсортированных по значению ключа. Идентификатор добавляется автоматически и указывает на документ, из которого была создана эта строка. Значение — это нужные вам данные. В нашем примере это возраст девочки.

Map-функция, создающая этот результат:

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

В ней используется инструкция if для проверки того, что мы работаем с нужными полями, а функция emit вызывается с именем и возрастом в качестве ключа и значения.

Поиск по ключу

Как это сделать в SQL:

SELECT field FROM table WHERE value="searchterm"

Как это сделать в CouchDB?

Сценарий использования: получить результат (которым может быть одна или несколько записей), связанный с ключом («searchterm»).

Для быстрого поиска чего-либо, независимо от способа хранения, нужен индекс. Индекс — это структура данных, оптимизированная для быстрого поиска и извлечения данных. Результат map в CouchDB хранится в таком индексе, который в данном случае представляет собой B+-дерево.

Чтобы найти значение по «searchterm», нужно поместить все значения в ключ представления. Для этого достаточно простой map-функции:

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

Просто.

Обратите внимание: нужно выдать значение с помощью emit. Результат представления содержит идентификатор соответствующего документа в каждой строке. Мы можем использовать его, чтобы получить дополнительные данные непосредственно из документа. Также можно использовать параметр ?include_docs=true, чтобы CouchDB сама извлекла для нас отдельные документы.

Поиск по префиксу

Как это сделать в SQL:

SELECT field FROM table WHERE value LIKE "searchterm%"

Как это сделать в CouchDB?

Сценарий использования: найти все документы, в которых значение поля начинается с searchterm. Например, предположим, что для каждого документа вы сохранили MIME-тип (например, text/html или image/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/"

Агрегатные функции

Как это сделать в SQL:

SELECT COUNT(field) FROM table

Как это сделать в CouchDB?

Сценарий использования: вычислить производное значение на основе ваших данных.

Мы ещё не объясняли reduce-функции. Они похожи на агрегатные функции в SQL. Такие функции вычисляют значение по нескольким документам.

Чтобы объяснить принцип работы reduce-функций, создадим функцию, которая не имеет особого практического смысла. Но этот пример легко понять. Более полезные варианты сокращения мы рассмотрим позже.

Reduce-функции работают с результатом map-функции (также называемым результатом map или промежуточным результатом). Задача reduce-функции, как нетрудно догадаться, — сократить список, созданный map-функцией.

Вот как выглядит наша суммирующая 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-функциях, таких как _sum и _count

Эта reduce-функция принимает два аргумента: список ключей и список значений. Для суммирования можно не учитывать список ключей и рассматривать только список значений. Мы перебираем список и добавляем каждый элемент к текущей сумме, которую возвращаем в конце функции.

Вы заметите одно различие между map- и reduce-функциями. Map-функция использует emit() для создания результата, а reduce-функция возвращает значение.

Например, по списку целочисленных значений, задающих возраст, вычислим суммарное количество лет жизни для заголовка новости: «На мероприятии присутствовало 786 лет жизни». Пример немного надуманный, но очень простой и потому хорошо подходит для демонстрации. Рассмотрим документы и map-представление, которые мы использовали ранее в этом документе.

Reduce-функция для вычисления суммарного возраста всех девочек:

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

Обратите внимание: вместо двух предыдущих вариантов мы используем предопределённую функцию CouchDB sum(). Она делает то же самое, что и два других варианта, но этот фрагмент кода встречается так часто, что CouchDB включает его в себя.

Теперь результат нашего reduce-представления выглядит так:

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

Общая сумма всех полей age во всех наших документах равна 15. Именно то, что нам нужно. Элемент key в объекте результата равен null, поскольку мы уже не можем определить, какие документы участвовали в создании сокращённого результата. Более сложные случаи использования reduce мы рассмотрим позже.

Как правило, reduce-функция должна сводить данные к одному скалярному значению. Это может быть целое число, строка или небольшой список либо объект фиксированного размера, содержащий агрегированное значение (или значения) из аргумента values. Она никогда не должна просто возвращать значения или нечто подобное. CouchDB предупредит вас, если вы попытаетесь использовать reduce «неправильно»:

{
    "error":"reduce_overflow_error",
    "message":"Reduce output must shrink more rapidly: Current output: ..."
}

Получение уникальных значений

Как это сделать в 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}
]}

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

Обеспечение уникальности

Как это сделать в SQL:

UNIQUE KEY(column)

Как это сделать в CouchDB?

Сценарий использования: приложению требуется, чтобы определённое значение встречалось в базе данных только один раз.

Это просто: в базе данных CouchDB у каждого документа должно быть уникальное поле _id. Если в базе данных нужны уникальные значения, просто присвойте их полю _id документа, и CouchDB обеспечит уникальность.

Однако есть один нюанс: в распределённой среде, когда запись принимают несколько узлов CouchDB, уникальность можно гарантировать только в пределах одного узла или вне CouchDB. CouchDB позволит записать одинаковые идентификаторы на двух разных узлах. При репликации CouchDB обнаружит конфликт и пометит соответствующий документ.

Copyright © 2025 The Apache Software Foundation — Licensed under the Apache License 2.0
https://docs.couchdb.org/en/3.5.1/ddocs/views/nosql.html

Spec-Zone.ru

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