18 September 2015

Не пора ли реляционным базам данных на свалку истории?

SQLNoSQL
Здравствуйте, меня зовут Дмитрий Карловский и я… антиконформист, то есть человек, который не держится за свои привычки и всегда готов их поменять, если в том есть необходимость. Например, как и многие разработчики, я начинал изучение баз данных с реляционных. Хотя реляционная алгебра и довольно красива в своей простоте, я постоянно ловил себя на мысли, что пытаюсь впихнуть круглую фигуру в квадратное отверстие и получалось как-то не герметично.



Нет, я не буду рассказывать вам про MongoDB или ещё какую неполноценную «убийцу SQL». Статей на тему «SQL vs NoSQL» сравнивающих на самом деле реляционные субд с документными и так полно:


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

Давайте сравним типичных представителей упомянутых типов СУБД (от большего к меньшему).

  • Популярность: Oracle, MongoDB, Redis, HBase, OrientDB.
  • Функциональность: OrientDB, Oracle, MongoDB, HBase, Redis.
  • Скорость: очень сильно зависит от задачи, данных и реализации приложения. Я пересмотрел кучу бенчмарков, везде всё по разному.

SQL


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

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

Вот, сколько статей на одном только Хабре написано о проблеме, которая есть исключительно в РСУБД ввиду попытки упихнуть всё многообразие моделей предметной области в прямоугольные таблицы:


Все решения сводятся к трём основным:

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

Рекурсивный запрос поддерева:

WITH RECURSIVE Rec(id, parent, name, ord)
AS (
    SELECT id, parent, name, ord FROM tree
    UNION ALL
    SELECT Rec.id, Rec.parent, Rec.name, Rec.ord
    FROM Rec, tree
    WHERE Rec.id = tree.parent
)
SELECT * FROM Rec
WHERE parent = 123
ORDER BY ord

Запрос поддерева по денормализованной таблице смежности:

SELECT navi.id , navi.name , navi.parent
FROM tree , navi
WHERE tree.ancestor = 123 AND navi.id = tree.node
ORDER BY ord

Материализованный путь. Потомок хранит номера всех предков (с сохранением их порядка). В худшем случае изменение иерархии приводит к обновлению данных всех узлов. При отсутствии соответствующего АПИ со стороны СУБД, требует фигурной манипуляции над строками, что не во всех случаях приемлемо по скорости. Не допускает вхождение одного потомка в несколько предков.

Запрос поддерева с использованием ordpath:

SELECT RowId, name FROM dbo.Tree WHERE @ParentId.IsDescendant(RowId) = 123

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

Запрос поддерева:

SELECT node.id, node.name
FROM tree AS node, tree AS parent
WHERE node.left BETWEEN parent.left AND parent.right
AND parent.id = 123
ORDER BY node.left;

Изменение деревьев гораздо сложнее, примеры кода можно найти по ссылкам выше.

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

Запрос поддерева в графе:

SELECT name , parent FROM ( TRAVERSE child FROM #1:123 )

Да, нереляционные СУБД, не смотря на общее название «NoSQL», тоже могут поддерживать Structured Query Language, расширяя его своими операторами :-)

Многие SQL-профи тут обычно заявляют, мол деревья и тем более графы в предметных областях почти не встречаются. Но стоит немного выйти из зоны комфорта как тут же увидишь, что любая предметная область на самом деле представляет из себя граф — набор сущностей, между которыми есть разнообразные отношения. Если отношения эти 1-к-1 или хотя бы 1-ко-многим и при этом связывают лишь разнотипные сущности, то такие модели относительно легко ложатся на реляционную модель (если не учитывать разные виды джойнов с костылями в виде индексов). Но обычно всё не так. Во многих местах можно встретить отношения многие-ко-многим. В РСУБД для каждого такого отношения приходится заводить отдельную таблицу и несколько индексов к ней, а это усложнение архитектуры, раздувание данных и замедление работы с ними.

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

Программная разработка — штука очень динамичная. Сегодня мастер у вас должен иметь одну профессию и вы просто даёте ему текстовое поле вписать её. Завтра потребуется, чтобы он мог указать профессию лишь из предложенных вами и вы даёте ему селект для выбора нужной, сохраняя id профессии в модель мастера (один-ко-многим). После завтра потребуется, чтобы он мог выбрать несколько профессий разом (многие-ко-многим). А через неделю вам срочно потребуется реализовать уже иерархический каталог профессий. В реляционной СУБД сложность каждого следующего перехода значительно превосходит предыдущий. С графовой — вы больше времени потратите на обсуждение, чем на реализацию. Так что при старте проекта имеет смысл брать наиболее гибкий инструмент, который позволит вам не терять темп разработки в процессе изменения бизнес требований (или лучшего понимания оных). Да, специализированные инструменты могут в некоторых случаях быть быстрее и именно в этих случаях, если в этом есть необходимость, стоит заниматься такого рода оптимизацией.

NoSQL


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

Нет, MongoDB даже среди документных СУБД — так себе, а уж в качестве альтернативы полноценным реляционным её в принципе нельзя рассматривать. А вот другая документная СУБД — OrientDB, бьёт реляционные по всем фронтам. Да, OrientDB на самом деле документная СУБД, которая, благодаря прямым ссылкам между документами позволяет описывать произвольные графы.

Давайте развеем несколько типичных мифов по поводу NoSQL, о котором судят по наиболее громким представителям — MongoDB и Redis:

1. Они не контролируют структуру записываемых пользователем данных. У отсутствия схем есть и плюсы (можно делать миграции данных в фоне, можно хранить не только кортежи примитивов), но и минусы (нужно внимательно следить что записываешь в базу, не эффективно хранятся данные). В OrientDB нашли разумный компромисс: вы можете указать схему и указанные там поля будут валидироваться в соответствии с ней при записи и не будут тратить место на имена полей, а не указанные валидироваться не будут, но будут занимать чуть больше места. Тут важно отметить, что изменение схемы не приводит к изменению самих документов — просто вы не сможете изменить их пока не приведёте к новому формату.

2. Они не удовлетворяют требованиям ACID (Атомарность, Согласованность, Изолированность, Надёжность). OrientDB этим требованиям удовлетворяет. Более того, она из коробки умеет партицирование и мастер-мастер репликацию, так что поддерживает в том числе и распределённые транзакции. При этом вы можете регулировать баланс между согласованностью и скоростью ответа:
writeQuorum определяет число узлов, которые должны подтвердить запись, прежде чем СУБД вернёт ответ, об успешном завершении транзакции.
readQuorum определяет число узлов, которые должны подтвердить актуальность данных, прежде чем данные вернутся в ответ на запрос.
По умолчанию все транзакции синхронны (дожидаемся ответа всех реплик), а чтение соответственно происходит без подтверждения актуальности (за её не надобностью в этом случае).
Примечательной особенностью является прозрачная поддержка map-reduce: если узел содержит не все данные, то он сам делает запрос к остальным узлам и сливает их ответы. Клиент может работать с партицированной базой данных как с цельной. Есть даже автобалансировщик, раскидывающий документы в кластеры по разным стратегиям.

3. Они не поддерживают SQL и предоставляют лишь простой специализированный API. OrientDB написана на Java и может быть интегрирована в другое Java приложение в качестве библиотеки. При этом есть несколько уровней публичного API:
Объектное. Низкоуровневый доступ к записям. Не рекомендуется к использованию, так как через него вы можете нарушить совместимость с двумя остальными API.
Документное. Тут вы имеете весь основной функционал основанный на документах и перекрёстных ссылках.
Графовое. Оно предоставляет дополнительные удобства для организации графов, но на практике всё же удобней оказалось использовать документное апи.
В качестве фронтенда к этим Java-API есть несколько библиотек, позволяющих писать запросы на разных языках (SQL, Gremlin, SPARQL).
Можно расширять функционал плагинами на Java. Собственно Lucene индекс и админка реализованы в качестве плагинов.

Так что не все NoSQL одинаково бесполезны :-)

NewSQL


Для сравнения подходов работы с графовой и реляционной СУБД, давайте создадим простейшую бизнес сущность — персону:

SQL
create table Persons (
    name text,
    age smallint
)

OSQL
create class Person
create property Person.name string
create property Person.age short

Тут всё просто и почти одинаково. Теперь добавим отношение «друзья»:

SQL
create table Persons_friends (
    subject integer,
    object integer
)
create unique index Persons_friends on Persons_friends ( subject , object )

OSQL
create property Person.friend linkset Person

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

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

SQL
select
    Persons.rowid ,
    Persons.name ,
    Persons.age
from
    Persons_friends as friends,
    Persons
where
    friends.subject = 123 ,
    friends.object = Persons.rowid

OSQL
select expand( friend )
from #19:0
fetchplan *:-2 name:0 age:0

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

А теперь найдём друзей его друзей:

SQL
select
    Persons_1.rowid ,
    Persons_1.name ,
    Persons_1.age ,
    Persons_2.rowid ,
    Persons_2.name ,
    Persons_2.age
from
    Persons_friends as friends_1 ,
    Persons_friends as friends_2 ,
    Persons as Persons_1 ,
    Persons as Persons_2
where
    friends_1.subject = 123 ,
    friends_1.object = Persons_1.rowid ,
    friends_1.object = friends_2.subject ,
    friends_2.object = Persons_2.rowid

OSQL
select expand( friend )
from #19:0
fetchplan *:-2
    name:0
    age:0
    friend.name:0
    friend.age:0

В РСУБД запрос заметно усложнился. По хорошему его нужно усложнить ещё сильнее, чтобы данные друзей первого уровня не дублировались для каждого друга второго уровня.

Продолжать можно долго, но суть, я думаю, уже ясна. Немного больше подробностей о различиях реляционного подхода и графового можно почерпнуть из презентации "How Graph Databases started the Multi Model revolution".

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

Хватит теории. Что на практике-то?


Последний год я занимался разработкой бэкенда для проекта SKEDDY (Поиск мастеров и запись к ним на услуги). Звучит вроде бы не сложно, однако число бизнес сущностей сейчас уже равно 20 (20 таблиц сущностей, если бы я использовал РСУБД): person, mail, phone, social, token, application, profession, service, meeting, assessment, album, image, notification, place, track, payment, article, aspect, facet, salon.

Между ними порядка 50 отношений, из них около 20 являются многими-ко-многим (ещё 20 таблиц смежностей и 20-40 индексов в РСУБД). В OrientDB же потребовался лишь один индекс для трансляции внешних айдишников во внутренние (внешние — человекопонятные и изменяемые, внутренние — персистентные и «странные»), один для фасетного поиска услуг плюс ещё пяток полнотекстовых и всё. Граф полностью нормализован, большинство запросов идут по прямым ссылкам между узлами, что позволяет делать глубокие выборки, не теряя в производительности и наглядности запросов.

Благодаря графовой модели данных, отображение на неё бизнес модели происходит легко и просто — единожды написанный простой обобщённый код для синхронизации с базой данных, позволил программировать в терминах бизнес сущностей без лишних забот об оптимизации запросов к СУБД. Собственно борьба с AngularJS отняла куда больше времени, но это уже совсем другая история…

Теперь о недостатках:

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

Больше что-то на ум ничего не приходит :-)

На этом всё. Кто заинтересовался, но что-то недопонял — не стесняйтесь задавать вопросы в комментариях.
Tags:OrientDBSQLNoSQLреляционные базы данныхнереляционные базы данныхграфовые базы данныхдокументные базы данныхORMODMOOMрасширение сознаниямежду двух огнейпрощай карма
Hubs: SQL NoSQL
-7
28.9k 203
Comments 338
Top of the last 24 hours