Почему вы никогда не должны говорить «никогда»

4 грудня 2014
Олег Полудненко, Senior PHP Developer

Эта моя публикация чуть более чем полностью — ответ на перевод статьи «Почему вы никогда не должны использовать MongoDB». Статья, которая, по сути, рекомендует держаться подальше от MongoDB, — самая заплюсованная в хабе. И это звучит как приговор. Поэтому логично либо хаб закрыть и больше никогда не читать, либо написать еще более рейтинговое опровержение. Конечно же, я выбрал второй вариант, рискуя рейтингом и кармой (ввиду крайней холиварности в комментах).

261a18f3c5e0aaae7f2511b352806b50
Картинка самоиронии

Выбор базы данных

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

Но какие есть альтернативы? Некоторые утверждаю что графовые базы данных подходят лучше всего, но я не буду их рассматривать: они слишком нишевые для массовых проектов. Другие говорят, что документарные идеально подходят для социальных данных и они достаточно мейнстримные для реального применения. Давайте рассмотрим, почему люди считают, что для социальных данных гораздо лучше подходит MongoDB, а не PostgreSQL.

Утверждение, что графовые базы слишком нишевые (???) для массовых проектов (???), работающих с социальными графами, очень сложно обосновать даже самым авторитетным источником. По опыту могу сказать, что графовые базы, несмотря на название, отлично работают с графами (в том числе, с социальными). Конечно, не стоит брать графовые базы в качестве основного хранилища данных. Для этих целей незаменимыми остаются такие инструменты, как MySQL, MongoDB, Postgres, etc. Точно так же, когда нужен, например, полнотекстовый поиск, мы используем Sphinx в связке с основным хранилищем данных, а не делаем мучительный выбор между ним, Redis и MariaDB.

Моделирование данных


Как правильно моделировать данные в MongoDB, можно прочитать в официальном руководстве. Обычно это желательно делать до того, как подписываешься на проект и авторитетно предлагаешь архитектурные решения и инструменты их реализации. Да, даже если в документе 40 страниц — читать всё равно надо. Иначе можно легко завалить проект неумелым выбором или неумелым использованием хороших инструментов. И писать большие статьи с веселыми картинками о том, что вилкой компот есть неловко.

Мы могли бы также моделировать эти данные в виде набора вложенных объектов (набор пар «ключ —значение»). Множество информации о конкретном сериале — одна большая структура вложенных наборов «ключ-значение». Внутри сериала, есть множество сезонов, каждый из которых также объект (набор пар ключ —значение). В пределах каждого сезона — массив эпизодов, каждый из которых представляет собой объект, и так далее. Так в MongoDB моделируют данные. Каждый сериал — документ, который содержит всю информацию об одном сериале.

У сериала есть название и массив сезонов. Каждый сезон — объект с метаданными и массивом эпизодов. В свою очередь, каждый эпизод имеет метаданные и массивы отзывов и актеров.

Если бы Сара прочитала руководство по моделированию данных в MongoDB, она бы знала, что в этой базе можно работать чуть больше чем с одной моделью данных. Но, на самом деле, у меня есть один очень простой вопрос: автор в руках калькулятор когда-нибудь держала? Про Google и Wikipedia спрашивать даже не буду. К чему это я? Давайте загуглим самый длинный сериал. Этот шедевр под названием «Направляющий свет» содержит 18262 серии разнообразия и философии. На его фоне «Санта-Барбара» с её 2137 сериями — белый карлик на звездном небе киноискусства. Но это всё лирика. Теперь я снова становлюсь занудой и утверждаю, что перед тем, как авторитетно сделать выбор базы данных, надо не только ознакомиться с рекомендуемыми моделями хранения данных, но и с ограничениями этой базы. Одно из ограничений — максимальный размер документа, который не может превышать 16 Мб. А теперь берём в руки калькулятор, держимнероняем и производим магический расчёт, который я вычитал в книге заклинаний DBA Architect:

16 Мб / 18.262 серии = 876 // Байт на серию  

Восемьсот семьдесят шесть байт на хранение каждой серии. Да этого не хватит даже на краткое описание того, что было в предыдущей серии! А нам же еще в этот самый документ надо вместить список актеров, рецензии (!!!) и ещё бог весть что. Нет, серьезно, автор об этом хоть на минуту задумывалась? В принципе, одного этого просчета уже достаточно, чтобы понять уровень критикуемой мною статьи.

C позволения читателя, покритикую еще пару-тройку утверждений автора.

Начав делать уродливые джоины вручную в коде Diaspora, мы поняли, что это — только первая ласточка проблем. Это был сигнал, что наши данные, на самом деле, реляционные, что существует ценность в этой структуре связей, и мы двигаемся против базовой идеи документарных СУБД.

На самом деле для меня загадка, как автору статьи раз за разом удаётся идти по наихудшему из всех возможных сценариев. То все данные пытается уместить в одном документе, не удосужившись просчитать элементарные ограничения. То берет «best practice» из мутной Diaspora, печально известной лишь самоубийством ее сооснователя, а теперь вот ещё и уродливыми джоинами. Почему бы просто не прочитать, что думают об использовании джоинов в официальной документации? Ведь там в самом первом предложении черным по белому написано, что джоины не поддерживаются. Точка. Попытка навернуть свои джоины оживляют в моей голове яркий образ дорогих коллег с берегов великой реки Ганг. Они смотрят на меня из Skype добрыми глазами и говорят, что реализовали часть функционала корзины прямо в ядре фреймворка, потому что так было быстрее и проще. Святой Шива! Но ведь это не повод винить фреймворк, не так ли?

В то же время, отсутствие джоинов не лишает разработчика возможности работать с несколькими документами. Если документы хранятся в одной коллекции, вы можете делать очень функциональные и выразительные запросы, используя Aggregation Framework. Можно, например, легко получить даже такую весьма экзотическую метрику, как среднее число фильмов, в которых снялись актёры с именем Sisi:

db.actors.aggregate( [     { $match : { name: "Sisi" } }     { $group : { _id : "$movieId", count: { $sum : 1 } } },     { $group : { _id : null, avgMovieCount : { $avg : "$count" } } }  ] )  

Если вам необходимо подтянуть связанные данные, то это делается отдельным запросом. И этот подход не уступает в эффективности сложному запросу с джоинами и используется во всех официальных MongoDB ORM и ODM. Главное преимущество отдельных запросов в том, что связанные данные запрашиваются только по мере необходимости (lazy loading). Если же вам нужно делать совершенно хардкорные запросы, в том числе между разными коллекциями, то на помощь приходит MapReduce. Но лично в моей практике, таких запросов на всё приложение могло насобираться максимум несколько штук.

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

На дворе Web 3.0, но Сара на полном серьезе проектировала систему таким образом, что желание клиента кликнуть на имя автора и увидеть список сериалов повергло ее в натуральный шок. И, на самом деле, это абсолютно закономерная расплата за все просчеты, которые были допущены ранее. Расплата за нежелание читать официальную документацию и рекомендации.

Хорошо. Но как насчет социальных данных?

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

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

Из комментариев

Для полноты картины решил включить в статью ответы на пару самых рейтингов комментариев на Хабре с конструктивной критикой в адрес MongoDB.

Комментарий от cleg:

и потом банальная задача «удалить сериал» при отсутствии поддержки целостности превращается в многоступенчатый процесс:

  • удаляем все связанные записи из коллекции с сериями;
  • удаляем все связанные записи из коллекции с отзывами;
  • удаляем запись из коллекции сериалов.

а если записей много, и где-то этот процесс навернулся — у нас остается неконсистентность

Удалить сериал? Это как? Все его копии сожгли, а актеров отправили на Альфа Центавра? Какой реальный кейс удаления сериала? Даже если таковой и найдётся, то удалять ничего не надо, достаточно пометить сериал как «удаленный». И такой подход применяется в любой крупной системе. Более того, на некоторых проектах, в разработке которых я принимал участие, было требование не только не удалять данные физически, но и фиксировать все изменения. Если и разрешать удаление чего-либо в сервисе сериалов, то разве что комментариев к обзорам. Да и то, наш всеми любимый Хабр не имеет даже такой возможности и позиционирует это как фичу. Объяснить такую ситуацию с удалениями можно, во-первых, ценностью данных, а во-вторых тем, что в том же MySQL, на больших объёмах, с шардированием и реплицированием, каскадное удаление работает далеко не так гладко, как может показаться.

Комментарий от gandjustas:

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

Как отсутствие схемы помогло? Похоже, что никак.

Совершенно согласен. Отсутствие схемы в базе не избавляет от необходимости продумывать модели хранения данных на этапе проектирования приложения. Это действительно абсолютно неверное понимание schemaless, к сожалению, весьма распространённое. И печальный опыт Сары — очередное тому подтверждение. Единственное, что дает отсутствие схемы в базе — избавление от необходимости дублирования это самой схемы в коде вашего приложения, в моделях данных. И лично мне это очень по вкусу. Например, зачем указывать и в базе и в правилах валидации модели пользователя, что имя — это строка с максимальной длиной 100 символов? MongoDB позволяет описывать модели данных только в приложении, не заставляя переносить часть этих правил в схему базы. И это всё, что даёт schemaless. MongoDB не умеет магическим образом нивелировать ошибки разработчика при проектировании моделей данных. Увы.

Заключение


В завершение хочу еще раз подчеркнуть, что нет смысла винить в отбитом пальце молоток, а в протекающем компоте — вилку. Вместо этого лучше правильно использовать правильные инструменты. А для этого надо читать документацию и изучать официальные best practice. На этом у меня всё. Всем добра и шардинга из коробки.