Общий индекс в MongoDB

Материал из Национальной библиотеки им. Н. Э. Баумана
Последнее изменение этой страницы: 01:08, 29 мая 2018.
MongoDB
MongoDB-Logo.svg
Разработчики: MongoDB Inc.
Выпущена: February 2009, 11 (11-02-2009) [1]
Постоянный выпуск: 3.4.9[2] / 11 September 2017 года; 20 months ago (2017-09-11)
Предыдущий выпуск: 3.5.12[3] / 22 August 2017 года; 20 months ago (2017-08-22)
Состояние разработки: Активное
Написана на: C++, C и JavaScript
Операционная система: Windows Vista и позднее, Linux, macOS 10.7 и позднее, Solaris,[4] FreeBSD[5]
Локализация: Английский язык
Тип ПО: Document-oriented database
Лицензия: Various; see § Licensing
Веб-сайт mongodb.org


MongoDB — документоориентированная система управления базами данных (СУБД) с открытым исходным кодом, не требующая описания схемы таблиц. Классифицирeтся как NoSQL. Написана на языке C++. Данные в MongoDB хранятся в документах, которые объединяются в коллекции. Каждый документ представляет собой JSON-подобную структуру. Проведя аналогию с реляционными СУБД, можно сказать, что коллекциям соответствуют таблицы, а документам — строки в таблицах. Максимальный размер документа в MongoDB 2.x составляет 16 Мб (в более ранних версиях — лишь 4 Мб). В отличие от РСУБД MongoDB не требует какого-либо описания схемы базы данных — она может постепенно меняться по мере развития приложения, что есть удобно. Поддерживаются индексы, в том числе по массивам и вложенным документам, а также геопространственные индексы. Поддерживаются уникальные и составные индексы. Пожалуй, самая значительная особенность MongoDB заключается в том, что документы могут быть автоматически сегментированы по нескольким наборам реплик. Сегментирование производится по диапазону; чтобы отнести документ к конкретному диапазону, используется сегментный ключ (shard key). Данные распределяются между наборами реплик так, чтобы каждый набор содержал примерно одинаковый объем данных. Если кластер перестает справляться с нагрузкой, можно просто добавить в него еще один набор реплик — перераспределение данных произойдет автоматически

Архитектура

Часто рекламируемым преимуществом документ-ориентированных баз данных является то, что они бесструктурны. Это делает их гораздо более гибкими, нежели традиционные реляционные базы данных. Можно согласиться с тем, что бесструктурность хороша, но только не в качестве упоминаемого многими главного преимущества.

Часто бесструктурность видится как хаотичная организация данных. Есть домены и наборы данных, которые и правда очень трудно смоделировать в терминах обычной реляционной базы данных, но рассматриваются они скорее как граничные случаи. Бесструктурность заманчива, однако большая часть данных должна быть хорошо структурированной. Конечно, иногда это может быть удобно, особенно для добавления нового функционала, однако на деле это можно решить и добавлениями новых необязательных полей.

Одно из главных преимуществ бесструктурной архитектуры — это отсутствие установки и сведённые к минимуму расхождения с ООП. Особенно это чувствуется при работе со статически типизированными языками. Например, работая с MongoDB [Источник 1] как в C#, так и в Ruby — разница бросается в глаза. Динамизм Ruby и популярная реализация ActiveRecord уже ощутимо сокращают расхождение объектной и реляционной моделей (object-relational impedance mismatch). Это не означает, что MongoDB — плохое решение для Ruby, напротив. Скорее , что большинство Ruby-разработчиков видят MongoDB как небольшое улучшение, в то время как разработчики, пишущие на C# или Java, видят пропасть разделяющую MongoDB и их подход к манипулированию данными.

Интересно рассмотреть MongoDB с точки зрения разработчика драйверов. Вам необходимо сохранить объект? Сериализируйте его в JSON (на самом деле в BSON, но это почти одно и то же) и отправьте в MongoDB. Нет никакого маппинга свойств или типов. Эта простота определённо должна подходить вам, как конечному разработчику.


Рис.1 Архитектура


Сильные стороны MongoDB

Поддержка множества официальных драйверов

У MongoDB есть множество официальных драйверов для различных языков. Их можно рассматривать как драйверы уже привычных реляционных БД. На их основе сообщество разработчиков построило множество высокоуровневых драйверов — для определённых языков и фреймворков. Например, NoRM это библиотека для C#, реализующая LINQ, а MongoMapper для Ruby, с поддержкой ActiveRecord. Программировать напрямую, используя низкоуровневые драйверы MongoDB, или же с применением высокоуровневых библиотек — решайте сами.


Отсутствие схемы

Это самое очевидное преимущество. Если вы работаете над новым стартапов, в которым бизнес-модель еще не до конца ясна и с большой вероятностью проект до выхода на рынок претерпит множество изменений, в том числе на уровне организации данных - посмотрите в сторону NoSQL [Источник 2]. решений, в частности на MongoDB. Все дело в том, что в отличие от многими горячо любимого PostgreSQL в MongoDB просто нет необходимости создавать таблицы, менять их схемы, создавать миграции, заботиться о типах данных. Однако тут следует быть осторожным. Очевидный плюс этого пункта, что вам гораздо проще создавать новые таблицы, добавлять и убирать поля. Настолько просто, что в Mongoid (ORM для MongoDB) вы просто добавляете в вашу модель строчку, например, field :text, type: String и все, при следующей записи в базу данных у нового элемента будет это поле. Если же вы вставляете данные без ORM - то вам и никакие "строчки" не нужны - просто добавляйте, что необходимо. Но тут здесь кроется недостаток. Вы не можете быть вполне уверены, что в конкретном документе у вас есть конкретное поле. (документами в MongoDB называются записи в коллекции). Т.е. если раньше у вас не было поля text и вы добавили несколько записей, а потом добавили это поле и добавили еще записи - в старых записях у вас этого поля, конечно же, не появится. Mongoid, он сделает вид, что такое поле есть и у них и просто вернет значение null.


Легкость горизонтального масштабирования

Горизонтальное масштабирование требуется когда вам необходимо добавить в базу данных информации больше, чем диск на вашем сервере. Все, что связано с горизонтальным масштабированием является визитной карточкой любой NoSQL базы данных. Дальше они уже соревнуются в том, у кого выше надежность, у кого быстрее запись, у кого быстрее чтение и т.д. На момент написания этой статьи в PostgreSQL нет встроенного механизма горизонтального масштабирования. Есть сторонние проприетарные решения (не могу не упомянуть компанию Citus, они делают прекрасную работу). В MongoDB же это делается предельно просто, а статей про это написано много, и механизмы репликации вкупе с шардированием на ней работают прекрасно. Кроме того можно как позволить балансировщику автоматически выбирать, в какой шард складывать конкретные докуенты, так и задать правила относительно какого-то поля или набора болей.


Богатая функция агрегации

Язык SQL очень богатый и всем привычный. Однако то, как позволяет получать данные MongоDB однозначно заслуживает похвалы. Тут присутствует и map-reduce, и группировки по сложным условиям, и переформатирование документов на лету, и получение случайных документов, и сортировки. В общем все то, что вы можете выжать из SQL БД, плюс возможность записывать все это в формате pipeline'ов и с более читаемым синтаксисом.


Рис.2 Пример агрегации


Синтаксис SQL

SELECT cust_id,
       SUM(price) as total
FROM orders
WHERE status = 'A'
GROUP BY cust_id
HAVING total > 250


Синтаксис MongoDB

db.orders.aggregate( [
   { $match: { status: 'A' } },
   {
     $group: {
        _id: "$cust_id",
        total: { $sum: "$price" }
     }
   },
   { $match: { total: { $gt: 250 } } }
] )

Пример MongoDB выше более многословный, но гораздо более адекватно структурированный. Если вы только пришли с SQL базы данных, потребуется какое-то время на привыкание, но потом привыкаешь и возвращаться не хочется.


Создана для денормализации

В MongoDB принято хранить данные так, как вам это удобно. В SQL базах данных всегда стоит заботиться об организации данных, чтобы таблицы были нормализованные, а запросы строить так, чтобы они были корректными и выполнялись эффективно . В MongoDB если вам неудобно, в каком формате или в каком месте лежат данные - вы с чистой совестью можете их или переместить, потому что отсутствие схемы это подразумевает, или просто продублирвать данные в нужное место. Т.е. фактически у вас может быть одно и то же поле с одними и теми же данными, но в разных коллекциях. Или два поля в одной коллекции, а плюс к ним еще одно поле, которое является композицией первых двух. Но это опять таки та сила, которую следует применять с умом. Если вы будете неконтролируемо плодить поля в документах, поддержка на уровне понимания разработчиком сущности будет становиться все труднее и труднее.


Простой формат индексов

Индексы в MongoDB называются предельно понятно и их использование практически лишено подводных камней. Например в PosstgreSQL если у вас есть b-tree индекс на одно поле и gist индекс на другое - при запросе, который использует оба этих индекса, использоваться будет только один из них. В MongoDB таких сюрпризов меньше.

Индексы в  Mongo DB

Общая информация

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

Как бы мы обходились без индексов

Стандартный алгоритм поиска последовательностей, который приходит в голову любому человеку демонстрирует долгую работу, противоречит принципу работы любой современной БД - скорости : он зачем-то просматривает все значения в таблице. Например, нам нужно использовать вот такой простой SQL-оператор для поиска значения “Captain Nemo”:

SELECT * FROM users WHERE name = 'Capitan Nemo' ORDER BY id ASC LIMIT 1


Рис.3 Поиск последовательности в SQL


Если бы таблица содержала миллионы значений, и требуемый элемент поиска был где-то в конце, то поиск занял бы очень много времени. Конечно, этого можно избежать, если убрать сортировку и переписать запрос так, чтобы он был более эффективным. Но проблема более глубока, и заключается она в неэффективности стандартного механизма поиска. Использование поиска последовательностей для сравнения каждого значения в таблице — процесс медленный, неэффективный и зависящий от порядка размещения записей. Должен быть другой способ! Решение простое: нужно создать индекс.

Cоздание индекса

Сделать это просто, достаточно выполнить команду:

db.users.createIndex({"name" : 1})

Таким образом с помощью метода createIndex устанавливается индекс по полю name. MongoDB позволяет установить до 64 индексов на одну коллекцию. Мы указываем имя коллекции, для которой мы хотим создать индекс, а число ‘1’ – порядок по возрастанию. Для сортировки по убыванию используется число – ‘-1’.


Настройка индексов

Если мы просто определим индекс для коллекции, например, db.users.createIndex({"name" : 1}), то мы все еще сможем добавлять в коллекцию документы с одинаковым значением ключа name. Однако, если нам потребуется, чтобы в коллекцию можно было добавлять документ с одним и тем же значением ключа только один раз, мы можем установить флаг unique:

db.users.createIndex({"name" : 1}, {"unique" : true})

Теперь, если мы попытаемся добавить в коллекцию два документа с одним и тем же значением name, то мы получим ошибку.

В тоже время тут есть свои тонкости. Так, документ может не иметь ключа name. В этом случае для добавляемого документа автоматически создается ключ name со значением null. Поэтому при добавлении второго документа, в котором не определен ключ name, будет выброшено исключение, так как ключ name со значением null уже присутствует в коллекции.

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

db.users.createIndex({"name" : 1, "age" : 1}, {"unique" : true})

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

Кроме того, тут есть свои ограничения. Например, значение поля, по которому идет индексация, не должно быть больше 1024 байт.


Управление индексами

Все индексы базы данных хранятся в системной коллекции system.indexes. Обратившись к ней, мы можем получить все индексы и связанную с ними информацию:

db.system.indexes.find()

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

db.users.getIndexes()

Данная команда вернет вывод наподобие следующего:

[
    {
        "v" : 1,
        "key" : {
                "_id" : 1
        },
        "ns" : "test.users",
        "name" : "_id_"
    },
    {
        "v" : 1,
        "key" : {
                "name" : 1
        },
        "ns" : "test.users",
        "name" : "name_1"
    }
]

Как мы видим, здесь для коллекции users (из бд test) определено 2 индекса: id и name. Поле key используется для поиска максимального и минимального значений, для различных операций, где надо применять данный индекс. Поле name применяется в качестве идентификатора для операций администрирования, например, для удаления индекса:

db.users.dropIndex("name_1")


Общий индекс Mongo DB

Предпосылки использования

Спроектируем схему документа воспользовавшись возможностью хранения полей документа в виде списка объектов:

{
    _id: 123,
    props: [
    { n: "firstName", v: "John"},
    { n: "lastName", v: "Smith"},
    { n: "age", v: 25},
    ...
    ]
}

Для решения проблемы поиска создается составной индекс по имени и значению объектов внутри списка. Для наглядности создадим 5 миллионов документов состоящих из фиктивных свойств от prop0 до prop9 которые имеют случайное значение от 0 до 1000.

for (var i = 0; i < 5000000; ++i) { 
    var arr = [];
    for (var j = 0; j < 10; ++j) {
        arr.push({n: "prop" + j, v: Math.floor(Math.random() * 1000) }) 
    }
    db.generic.insert({props: arr}) 
}
db.generic.findOne()
{
  "_id": ObjectId("515dd3b4f0bd676b816aa9b0"),
  "props": [
    {
      "n": "prop0",
      "v": 40
    },
    {
      "n": "prop1",
      "v": 198
    },
...
    {
      "n": "prop9",
      "v": 652
    }
  ]
}
db.generic.ensureIndex({"props.n": 1, "props.v": 1})
db.generic.stats()
{
  "ns": "test.generic",
  "count": 5020473,
  "size": 1847534064,
  "avgObjSize": 368,
  "storageSize": 2600636416,
  "numExtents": 19,
  "nindexes": 2,
  "lastExtentSize": 680280064,
  "paddingFactor": 1,
  "systemFlags": 1,
  "userFlags": 0,
  "totalIndexSize": 1785352240,
  "indexSizes": {
    "_id_": 162898624,
    "props.n_1_props.v_1": 1622453616
  },
  "ok": 1
}


В таком случае размер индекса равен 1.6 Гб т.к в индексе хранится как имя свойства, так и его значение. Теперь попробуем найти документы в которых prop1 равен 0:


db.generic.findOne({"props.n": "prop1", "props.v": 0})
{
  "_id": ObjectId("515dd4298bff7c34610f6ae8"),
  "props": [
    {
      "n": "prop0",
      "v": 788
    },
    {
      "n": "prop1",
      "v": 0
    },
...
    {
      "n": "prop9",
      "v": 788
    }
  ]
}


db.generic.find({"props.n": "prop1", "props.v": 0}).explain()
{
  "cursor": "BtreeCursor props.n_1_props.v_1",
  "isMultiKey": true,
  "n": 49822,
  "nscannedObjects": 5020473,
  "nscanned": 5020473,
  "nscannedObjectsAllPlans": 5020473,
  "nscannedAllPlans": 5020473,
  "scanAndOrder": false,
  "indexOnly": false,
  "nYields": 0,
  "nChunkSkips": 0,
  "millis": 252028,
  "indexBounds": {
    "props.n": [
      [
        "prop1",
        "prop1"
      ]
    ],
    "props.v": [
      [
        {
          "$minElement": 1
        },
        {
          "$maxElement": 1
        }
      ]
    ]
  },
  "server": "agmac.local:27017"
}

Такое решение не дало ожидаемого результата: найдено ~50,000 документов за 252 секунды. Это происходит потому что каждый запрос n=prop1 и v=0 не требует выполнения обоих условий одновременно для вложенных документов, поэтому в итоговый результат попадают документы удовлетворяющие как требованию n=prop1, так и v=0 по раздельности, а это совсем не то что ожидалось. Можно уточнить запрос воспользовавшись $elemMatch:

db.generic.findOne({"props": { $elemMatch: {n: "prop1", v: 0} }})


Использование общего индекса

Другой способ решения проблемы поиска - это хранение поля в списке в виде объектов property: value. Это решение работает в MongoDB версии v2.2 и старше. Создадим документы вида:

for (var i = 0; i < 5000000; ++i) { 
    var arr = [];
    for (var j = 0; j < 10; ++j) {
        arr.push({n: "prop" + j, v: Math.floor(Math.random() * 1000) }) 
    }
    db.generic.insert({props: arr}) 
}


db.generic2.findOne()
{
  "_id": ObjectId("515e5e6a71b0722678929760"),
  "props": [
    {
      "prop0": 881
    },
    {
      "prop1": 47
    },
...
    {
      "prop9": 717
    }
  ]
}


Построим индекс:

db.generic2.ensureIndex({props: 1})
db.generic2.stats()
{
  "ns": "test.generic2",
  "count": 5000000,
  "size": 1360000032,
  "avgObjSize": 272.0000064,
  "storageSize": 1499676672,
  "numExtents": 19,
  "nindexes": 2,
  "lastExtentSize": 393670656,
  "paddingFactor": 1,
  "systemFlags": 1,
  "userFlags": 0,
  "totalIndexSize": 2384023488,
  "indexSizes": {
    "_id_": 162269072,
    "props_1": 2221754416
  },
  "ok": 1
}

Размер индекса получился размером в ~2.2 Гб что на 40% больше чем в решение #1 потому что BSON вложенных документов хранит себя в индексе как BLOB'ы. Теперь выполним запрос:


db.generic2.ensureIndex({props: 1})
db.generic2.stats()
{
  "ns": "test.generic2",
  "count": 5000000,
  "size": 1360000032,
  "avgObjSize": 272.0000064,
  "storageSize": 1499676672,
  "numExtents": 19,
  "nindexes": 2,
  "lastExtentSize": 393670656,
  "paddingFactor": 1,
  "systemFlags": 1,
  "userFlags": 0,
  "totalIndexSize": 2384023488,
  "indexSizes": {
    "_id_": 162269072,
    "props_1": 2221754416
  },
  "ok": 1
}

Размер индекса получился размером в ~2.2 Гб что на 40% больше чем в решение #1 потому что BSON вложенных документов хранит себя в индексе как BLOB'ы. Теперь выполним запрос:

db.generic2.find({"props": {"prop1": 0} }).explain()
{
  "cursor": "BtreeCursor props_1",
  "isMultiKey": true,
  "n": 4958,
  "nscannedObjects": 4958,
  "nscanned": 4958,
  "nscannedObjectsAllPlans": 4958,
  "nscannedAllPlans": 4958,
  "scanAndOrder": false,
  "indexOnly": false,
  "nYields": 0,
  "nChunkSkips": 0,
  "millis": 15,
  "indexBounds": {
    "props": [
      [
        {
          "prop1": 0
        },
        {
          "prop1": 0
        }
      ]
    ]
  },
  "server": "agmac.local:27017"
}


Запрос выполнился за 15 мс что быстрее первого решения! Но есть одно условие, при составлении запроса необходимо описывать объект поддокумента целиком. Для того что бы выполнить выборку документов удовлетворяющие запросу где prop1 может быть равен от 0 до 9, необходимо выполнить запрос:


db.generic2.find({"props": { $gte: {"prop1": 0}, $lte: {"prop1": 9} })


Немного неудобно, а так же если есть и другие поля во вложенном документе, то они должны участвовать при составлении запроса (т.к вложенные документы хранятся в виде BLOB'ов). Так же есть еще одно ограничение: нельзя индексировать отдельно только значения полей, тогда как в обычном индексировании решении можно построить индекс props.v для поиска например всех документов имеющие значение 10. Другие решения этого не позволяют.

Туториал по установке и подключению MongoDB

Установить MongoDB проще и лучше всего через пакетный менеджер HomeBrew, который имеет свои аналоги для множества *nix подобных дистрибутивов, например LinuxBrew.


Использование MongoDB совместно с ODM Mongoose [Источник 3]


Исходный код примера:

db.generic2.findOne()
const mongoose = require('mongoose');
mongoose.set('debug', true);

mongoose.connect('mongodb://localhost/intro');

const userSchema = new mongoose.Schema({
  email: {
    type:       String,
    // встроенные сообщения об ошибках (можно изменить):
    // http://mongoosejs.com/docs/api.html#error_messages_MongooseError.messages
    required:   'Укажите email', // true for default message
    unique:     true,
    validate: [{
      validator: function checkEmail(value) {
        return /^[-.\w]+@([\w-]+\.)+[\w-]{2,12}$/.test(value);
      },
      msg: 'Укажите, пожалуйста, корректный email.'
    }],
    lowercase:  true,
    trim:       true
  },
  displayName: String,
  gender: {
    type:       String,
    enum:       ['М', 'Ж'], // enum validator
    default:    'Ж'
  }
}, {
  timestamps: true // createdAt, updatedAt
});

userSchema.index({ email: 1, gender: -1 });

// публичные (доступные всем) поля
userSchema.methods.getPublicFields = function() {
  return {
    email: this.email,
    gender: this.gender
  };
};

const User = mongoose.model('User', userSchema); // users

// User.collection - это mongo native driver collection
// console.log(User.collection.conn.db); // доступ к connection, db на уровне native driver

const mary = new User({
  email: 'mary@mail.com'
});

console.log(mary);
console.log(mary.getPublicFields());

// mary.toObject() - обычный объект без методов, с данными

User.remove({}, function(err) {
  mary.save(function(err, result) {
    console.log(err);
    console.log(result);

    User.findOne({
      email: 'mary@mail.com'
    }, function(err, user) {
      console.log(user);

      //закрываем соединение
      mongoose.disconnect();
    });

  });

});

Выводы

Несомненно MongoDB не панацея в использовании. В ней есть свои неожиданные подводные камни. Например, она автоматически не отрубает медленные запросы и они продолжают висеть, пока вы их не закроете саомостоятельно. Еще вы можете испытать низкую производительность при count запросе на больших коллекциях. Несомненно у разработчиков MongoDB не ставится цель переманить backend-разработчиков с SQL БД и переводить продакшн проекты на MongoDB , но если стоит выбор, какую базу данных взять для нового проекта - это отличный вариант. С точки зрения производительности MongoDB занимает лидирующие позиции. Индексы заслуживают рассмотрения в начале проектирования любой БД. Исторический, эффективность на уровне доступа к данным была переложена на администраторов баз данных, это создавало слой оптимизации после проектирования. С документо-ориентированныим базами данных есть возможность этого избежать и использовать инструмент по-настоящему гибко и эффективно.

Источники

  1. MongoDB документация // Официальная документация. [2018]. Дата обновления: 16.05.2018. URL: https://docs.mongodb.com/ (дата обращения: 16.05.2018).
  2. NoSQL // Wikipedia. [2018]. Дата обновления: 16.05.2018. URL: https://ru.wikipedia.org/wiki/NoSQL (дата обращения: 16.05.2018).
  3. Mongoose // Официальная документация. [2018]. Дата обновления: 16.05.2018. URL: http://mongoosejs.com/ (дата обращения: 16.05.2018).


Ссылки

  1. "State of MongoDB March, 2010". DB-Engines (in английский). 
  2. "Release Notes for MongoDB 3.4". MongoDB. 
  3. "Core Server Versions". MongoDB. 
  4. "How to Set Up a MongoDB NoSQL Cluster Using Oracle Solaris Zones". Oracle. 
  5. "How-To: MongoDB on FreeBSD 10.x". FreeBSD News.