nin-jin / HabHub

Peering social blog
The Unlicense
62 stars 0 forks source link

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

Open nin-jin opened 7 years ago

nin-jin commented 7 years ago

https://page.hyoo.ru/#!=6awt9d_jsspx4

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

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

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

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

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

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

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

  1. Они не поддерживают SQL и предоставляют лишь простой специализированный API. OrientDB написана на Java и может быть интегрирована в другое Java приложение в качестве библиотеки. При этом есть несколько уровней публичного 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 отняла куда больше времени, но это уже совсем другая история..

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

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

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

На этом всё. Кто заинтересовался, но что-то недопонял - не стесняйтесь задавать вопросы в комментариях.