Выпуск 53 · подкаст «Тысяча фичей»

#53: Datomic: самая рок-н-рольная БД

1:29:32
↓ скачать mp3

Александр Пахомов и Никита Прокопов (Nikitonsky — автор Fira Code, DataScript и телеграм-канала «Стой под стрелой») разбирают Datomic — базу данных, которая идёт против течения индустрии. Начав с боли, когда оптимизатор `Postgres` внезапно замедляет запрос в сто раз, они переходят к устройству Datomic: модель `Entity-Attribute-Value` вместо таблиц, язык запросов `Datalog` вместо `SQL`, разделение чтения и записи, однопоточный транзактор и иммутабельная база с путешествием во времени. Отдельная линия разговора — как маленькая команда за счёт радикальной простоты решает те же задачи минимальными средствами.

Главное

  • Оптимизатор `Postgres` снимает головную боль в ~90% случаев, но когда инженер точно знает нужную последовательность операций, отсутствие прямого контроля над планом становится проблемой — добавление no-op-фильтра может замедлить запрос примерно в 100 раз.
  • Зависимость запроса от индекса в `SQL` неявная: запрос молча предполагает, что индекс есть, но нигде его не указывает — отсюда идея спустить абстракцию на уровень ниже, вплоть до `SELECT ... FROM table USING INDEX`.
  • Datomic хранит данные не таблицами, а тройками `Entity-Attribute-Value` (датомами); это делает разреженные поля, множественные значения и ссылки бесплатными по overhead, а обратные ссылки — такими же дешёвыми, как прямые.
  • Datomic использует `Datalog` — паттерн-матчинг по тройкам с автоматическим джойном и рекурсивными правилами; он эквивалентен `SQL` по мощности, но `order by`/`limit` в нём нет — их выносят в постобработку на клиенте.
  • Datomic «деконструирует» базу данных: чтение идёт напрямую из network-attached storage мимо сервера, а записи обслуживает единственный однопоточный транзактор, который просто линеаризует поток транзакций.
  • Однопоточного writer'а хватает для подавляющего большинства бизнес-приложений (порядка тысячи транзакций в секунду); отказ решать проблемы, которых у тебя нет, радикально упрощает систему.
  • Datomic — иммутабельная база: клиент держит ссылку на конкретную версию (эпоху) и видит консистентный снапшот всей базы, а также умеет запрашивать состояние на любой момент в прошлом — фантомные чтения как явление просто исчезают.
  • Радикальная простота Datomic родилась из ограничения маленькой команды; эта же модель `TripleStore` + `Datalog` реализуется в ~200 строк и легла в основу `DataScript` — in-memory клона, удобного даже без персистентности.

В выпуске

  • Никита ПрокоповNikitonsky — автор шрифта Fira Code, библиотеки DataScript и телеграм-канала «Стой под стрелой». tonsky.me ↗ GitHub ↗ mastodon.online ↗
Расшифровка

[00:00] Александр: Здорово! Это выпуск подкаста «Тысяча фичей» про самую рок-н-рольную базу данных — Datomic. В гостях — Nikitonsky, автор шрифта Fira Code, библиотеки DataScript и телеграм-канала «Стой под стрелой».

[00:13] Александр: Вначале мы поговорим про Postgres и его проблемы, а потом погрузимся в уютный диалог про Datomic. Это база данных, которая сильно отличается от большинства. Основная её идея — это TripleStore, Entity-Attribute-Value. Такая организация данных позволяет гибко работать с сущностями и… Так, стоп. Давайте по порядку. Поехали.

[00:50] Александр: Привет, Никит. У тебя была прикольная история в телеграм-канале «Стой под стрелой» — про то, как запрос в Postgres сначала работал довольно быстро, а потом ты что-то в нём поменял, и он начал работать долго. Я бы хотел начать сегодняшний подкаст с этой истории. Не мог бы ты её рассказать?

[01:06] Никита: Да, история очень простая. Postgres — база данных, работает на SQL. SQL — это декларативный язык: ты в целом описываешь, что ты в среднем хочешь получить, то есть описываешь форму результата. После этого некая часть Postgres — оптимизатор запросов, или планировщик, как он называется, — берёт это твоё описание и генерирует из него каким-то сильно непрозрачным способом план: что он будет делать на самом деле, чтобы получить твой результат. И планов может быть много, это очень нетривиальная штуковина, и её выход часто трудно предсказать — он иногда зависит от совсем, казалось бы, неадекватных вещей.

В моём примере было как? Довольно простой запрос: табличка плюс join плюс какой-то фильтр — выбирает примерно 2000 записей. После этого мы добавляем фильтр по ещё одной колонке из того, что мы уже выбрали. Причём этот фильтр — чтобы было ещё смешнее — ничего не фильтрует: все значения в этой колонке равны тому значению. То есть это, по сути, no-op. Заранее мы этого не знаем, но по факту это no-op. И в любом случае — 2000 записей отфильтровать по одной колонке. И время запроса улетает примерно в 100 раз в большинстве случаев.

Почему? Потому что оптимизатор запросов Postgres решил, что, поскольку у нас теперь есть другое условие, мы не можем использовать тот индекс, а должны использовать другой, — он же заранее не знает, — и попробовал вот так. И мне кажется, что это заранее проигранный бой. Ну, то есть — нет-нет, окей: оптимизатор запросов снимает с тебя значительную часть головной боли, и в 90% случаев, условно, он помогает — позволяет писать более декларативные запросы, которые выполняются за разумное время. Ты не паришь голову тем, какие у тебя индексы есть, надо их использовать или нет, в каком порядке джойнить таблицы. Он снимает с тебя эту головную боль. Проблема в том, что когда тебе это на самом деле надо…

Я часто пишу запросы и точно знаю, что хочу сделать. Я хочу, чтобы ты пошёл, сделал for-loop по вот этой таблице в каком-то рейндже, потом приджойнил вот эту таблицу хэш-джойном, а потом, не знаю, сгруппировал, или взял первую запись из той таблицы, или ещё что-то. Я точно знаю последовательность операций, и я знаю примерные размеры таблиц — в моём случае это не секрет. Они не настолько непредсказуемы — они очень предсказуемы. Если бы у меня были средства сказать конкретно «сделай то-то, то-то и то-то», я бы получил, во-первых, бóльшую производительность в данном случае, а во-вторых — гарантированную производительность. То есть я бы контролировал то, что происходит, вместо того чтобы полагаться, что оптимизатор сработает правильно или неправильно. Или ситуация изменится: сегодня он работает так, завтра по-другому. Тот же запрос, на самом деле, у меня начал работать быстрее через несколько часов, потому что что-то там…

[04:11] Александр: Статистику пересчитал.

[04:14] Никита: Да-да-да. Это смешно, но это не смешно, потому что мы делаем сервис, в котором я хочу вставить тысячу записей и тут же удалить тысячу записей. Я не хочу ждать несколько часов, прежде чем смогу их удалить. Что это за история вообще? Короче, я хотел бы получить предсказуемость: вот я сейчас сделал так, как мне нужно, и оно всегда будет так работать. Это спокойствие, гарантия.

Эта проблема, в принципе, существует с любыми высокоуровневыми инструментами. С Java у меня была такая же. Пока ты пишешь на Java что-то простое или нетребовательное к производительности — всё прекрасно. Но как только ты пишешь что-то супероптимизированное — у меня это был JSON-парсер, там реально влияет, — уже начинают влиять такие странные вещи, как, например, сколько локальных переменных ты объявил внутри функции. Одну убираешь — становится быстрее, добавляешь параметр — становится медленнее. Это такое шаманство на расстоянии: ты сидишь и угадываешь, какие твои действия, казалось бы, семантически абсолютно эквивалентные с точки зрения языка, в итоге повлияют на машину, в которую ты каким-то непрозрачным образом всё это транслируешь. И тебе нужно сидеть это угадывать — а потом оно может поменяться на разных версиях JVM, на разных версиях процессора и так далее. Это всё довольно печально. Но, опять же, в каких-то случаях ты не хочешь париться об этих проблемах — и тогда твоя жизнь прекрасна: ты пишешь меньше кода, более высокоуровневый код и доволен. А вот когда тебе это нужно — это очень…

[04:41] Александр: Ну, я как разработчик баз данных прекрасно понимаю, как так произошло. Вообще не удивлён. И, естественно, можно предвосхитить комментарии: «понятно, почему так произошло», «а как вы без SQL, что вы хотите — KV-store и прямой доступ?» и так далее. Мы про это ещё поговорим. Естественно, для аналитиков, для людей, которые не пишут код и не понимают структуры данных, — им SQL супер понятен. Эта декларативность как будто прямо создана для них: можешь написать SQL, определить себе какие-то таблички. Ты даже мыслишь другими терминами — реляционной алгеброй часто. Ты не думаешь, а как там реально, что за индексы, тут хэш-индекс, а как он сджойнится. Это уже нам, инженерам, хочется больше контроля.

Ты хорошо сказал, что контроль теряется. Ты ставишь перед данными — реально, перед тем, как они лежат там на диске в структурах данных, в B-plus-tree, в дата-файлах, — ты ставишь перед собой этот оптимизатор, или execution engine, как угодно можно назвать. И ты хотел бы общаться с этими файлами по-хорошему, потому что квалификация позволяет, и желание есть, и интерес. Но перед тобой стоит система, которая как будто не то что мешает, а иногда может просто сделать невозможной такую коммуникацию. Вот эти 66 секунд, про которые ты писал, или около того, — для сервиса это уже блокер, он так работать не может. И иногда это действительно может быть неразрешимая проблема. А что делать? Ты тратишь, не знаю, день, иногда больше, чтобы понять, в чём дело. Иногда понять план запроса и что-то переделать — этого недостаточно.

И мне, знаешь, что иногда обидно? Когда я начинаю с такими системами работать и сталкиваюсь с проблемами — это абсолютно бесполезный труд меня как инженера. Зачем оно мне? Я начинаю из-за этого сильно переживать: вот я сейчас занимаюсь чем-то, чем как будто не должен заниматься. Как будто я мог бы просто взять и сделать то, что хочу, а мне система мешает. С точки зрения эмоций — ты сильно расстраиваешься, когда такое происходит? Или тебе в целом пофиг, ты такой: «Ну, я это уже видел тысячу раз, тысяча первый — ничего страшного»?

[07:54] Никита: Именно расстраиваешься — ты хорошо подобрал слово, «чувство». Это именно чувство: «зачем я это делаю?». Это ситуация задом наперёд. Ты берёшь инструмент, который не предназначен для того, чтобы контролировать ситуацию, и пытаешься через него ситуацию контролировать. И ты идёшь против течения. Всё в Postgres говорит тебе: «Не делай этого. Ты не хочешь, ты не должен этого хотеть». Но когда ты этого хочешь — тебе это мешает.

Я вспомнил ещё один пример, тоже у меня был. Как-то я программировал на HTML и уже задолбался — полдня просидел. Мне нужно было поставить какой-то прямоугольник в конкретное место внутри другого скроллбокса. И это очень сложно было сделать на HTML, потому что там комбинация каких-то свойств: скроллбар иногда на каких-то системах занимает часть толщины контейнера, иногда не занимает, и так далее. И тебе нужно прописать кучу очень неочевидных свойств: типа «напиши flex то-то, и тогда контейнер начнёт вести себя другим образом, и тогда ты сможешь написать padding, который будет значить не padding, а то-то». И я сижу и думаю: господи, — я прям помню этот момент, — если бы я просто взял и написал на обычном каком-нибудь условном C вычисление координат этого прямоугольника, я бы закончил за 10 минут. Я бы уже всё посчитал, оно бы железно стояло в том месте, в котором мне нужно. Но поскольку ты используешь инструменты, которые гораздо более высокоуровневые и оперируют на другом уровне абстракции, чем тот, что тебе нужен, — пройти обратно, сделать реверс-инжиниринг этих инструментов, чтобы получить простой тупой результат, для которого они не предназначены, очень сложно. И с SQL та же самая история. Я буквально сижу, пишу это всё и думаю: господи, вот если бы мне дали просто прямой контроль — сказать: сделай здесь select, потом .join, потом .groupby, — причём каждая точка это не составить из SQL-запроса, а именно вот сейчас пойди и сделай это, не думай, как, когда, в каком порядке, а просто буквально пойди и сделай. Я бы тоже уже закончил — всё бы уже работало и было прибито гвоздями.

[10:39] Александр: Меня не покидает мысль, пока ты говоришь: а что, если бы мы могли как-то декларативно или даже на языке программирования — допустим, на каком-нибудь Lisp-подобном — написать что-то вроде вот этого execution-плана? Того самого конечного плана выполнения запроса, который Postgres из SQL генерирует и оптимизирует. Вот если бы мы могли его написать и передать системе — было бы это проще, и вообще, пользовался бы ты чем-нибудь подобным?

[10:59] Никита: Мне кажется, что да — по крайней мере, у меня есть такое ощущение. Может быть, не буквально план, потому что в плане иногда прям какие-то супермудрёные вещи. Но я не вижу особых проблем написать: сейчас выбери в этой таблице по такому условию, используй такой индекс — потому что ты заранее знаешь, что такой индекс есть. Ты берёшь колонки, тебе понятно, что fullscan будет медленно, поэтому ты такой: у меня есть индекс, отлично, я его использую. И ты держишь это в памяти. Я, знаешь, даже не очень уверен, что это приносит хоть кому-то пользу — тот факт, что в SQL ты пишешь select то-то, не указывая индекс. Хотя вся ваша система держится на неявном предположении, что этот индекс есть. Когда ты шлёшь запрос, ты предполагаешь, что он есть, — это условие. И если с ним что-то случится, кто-то забудет его сделать, или ты криво напишешь запрос так, что индекс не сможет быть использован, — это ошибка. Ты закладываешь, что он есть, но нигде явно этого не указываешь. И мне кажется, это проблема, потому что это явная зависимость, а её странно скрывать и делать неявной.

[11:54] Александр: То есть как будто бы SQL — тот, которым пользуется большинство, — слишком на высоком уровне абстракции находится, и хочется его немножко приземлить: например, включить такое понятие, как индекс, вообще в семантику языка. То есть не select from table, а можно select from table using index.

[12:16] Никита: Ну да, да, я не понимаю, почему, собственно, нет. Кажется, что довольно логично.

[12:20] Александр: Вообще это интересное размышление, потому что я раньше про это особо не думал. Все производители баз данных прямо говорят: знаешь, ты не должен об этом думать, это вообще не твоя проблема. Ты думаешь о представлении данных, о том, какие данные ты хочешь извлечь, а всё остальное мы берём на себя — это наша работа, ты нам за это деньги платишь. Мы вендоры баз данных, мы её 10 лет разрабатывали, нам надо на чём-то зарабатывать. А действительно — если так подумать, почему бы не сделать такую базу данных, которая, да, может быть SQL-compatible, потому что рынок так диктует, по крайней мере сейчас всё ещё, к сожалению, — но предоставить какой-то альтернативный API, который спускает тебя на уровень абстракции ниже? Индекс тому пример.

[13:05] Никита: Я думаю дальше, знаешь: я бы вообще хотел сделать какой-нибудь — ну, не стандарт, да ладно, можно и стандарт — того, как описывать план выполнения запросов. Как должен выглядеть план, какой у него должен быть API, какие сущности в нём должны быть. Потому что у каждой базы данных они называются по-разному, разные атрибуты, всё разное — это, что называется, implementation details. Но если бы была какая-то спецификация, по которой я мог бы свой план передавать, если у меня есть желание, — а если нет, то использовать чуть более высокоуровневый API, — как будто бы мне было бы более интересно с такой системой работать. То есть не знать эту чёрную магию, а… Знаешь, на собеседованиях раньше, по крайней мере — я давно не ходил, — спрашивали: вот у тебя есть такой-то запрос, как может выглядеть его план выполнения? Ты такой: ну, выполняешь analyze, видишь — вот там будет какой-нибудь цикл, тут hash join, тут merge join. Ты как будто должен это знать, но не можешь напрямую пользоваться этими инструментами. То есть ты косвенно про них знаешь — и это тоже какое-то некомфортное, непрактичное знание: ты должен знать, как оно работает, но пользоваться этим не можешь. Мне становится как-то неуютно от этой мысли.

[14:27] Александр: Ну да, это как будто от тебя прячут абстракцию, но при этом она настолько протекающая, что ты обязан про неё знать всё равно, — но по какой-то причине она спрятана.

[14:40] Никита: Да-да-да. И ещё: вот эта вся история с циклом, индексами и базами данных… Периодически, когда я на это жалуюсь, кто-то приходит и говорит: на самом деле ты должен написать запросы, а потом какой-то DB-админ напишет правильный индекс, и у тебя всё заработает. Я никогда в жизни не видел, чтобы это заработало. Я не знаю, в каком мире живут люди, которые это пишут.

[15:00] Александр: Да-да, что ты запрограммировал что-то новое или поменял свой запрос, а потом какой-то дядька посмотрел на то, что ты сделал, и поправил это.

[15:10] Никита: Это, на самом деле, такая же история с девопсами немножко. Я тоже не особо видел, чтобы это работало. Ещё когда была Java, была такая же история: есть какой-то сервер приложений, ты отдаёшь ему джарку, а он там её как-то правильно запускает и настраивает Spring за тебя. И ты такой: как это вообще работает? Во-первых, они откуда знают, что правильно, что неправильно? А во-вторых, раз я это уже всё делаю, наверное, проще мне самому сделать, что я хочу, чем им объяснять, что я хочу. Короче, я никогда такого не видел, и мне кажется, это какой-то делюжн. Может, когда-то это так работало — не знаю, где-то в мейнфреймах, — но с тех пор я никогда…

[15:48] Александр: Ну да, там, где стоял какой-нибудь Oracle, Golden Gate какой-нибудь…

[15:53] Никита: …система настолько сложная, что ей невозможно пользоваться без специалиста именно по этой системе. Тогда да, наверное, такие люди нужны. Но тогда возникает вопрос: если я строю свою компанию — нахрена мне вообще эта система, если мне ещё и люди нужны, которые её обслуживают? Давайте возьмём систему, которая понятная и которую могут обслуживать нормальные разработчики сами. Ну, может быть, ещё какой-то один инженер по отказоустойчивости всей инфраструктуры будет — это довольно важный пост, за которым надо следить. Но да, интересно.

[16:25] Александр: Я знаю такую систему, и знаю, что ты её очень любишь — Datomic называется, это база данных. И мне кажется, она представляет некоторое альтернативное видение того, как можно работать с данными, — по сути решая ту же самую задачу, что и Postgres, ну, в 90 процентах случаев, а может, и больше. Но представляет другие абстракции, сущности и в целом инструментарий, который как будто приближает эту систему в ту сторону, куда мы клоним: она должна быть понятной, контроль должен существовать, и у инженеров не должно возникать такой ситуации, которая возникла у тебя с Postgres.

Если ты не против, я бы хотел про Datomic поговорить поподробнее. В подкасте «Тысяча фичей», по-моему, эта база данных уже пару раз упоминалась — ты вскользь про неё говорил в предыдущем выпуске про редакторы кода: когда мы обсуждали Fleet, там архитектура этого редактора отчасти, насколько я понял, вдохновлена тем, как устроен Datomic. Я ни разу не пользовался этой базой данных из кода. То есть я про неё слышал, почитал документацию, примерно понимаю, что она собой представляет, но ни разу не пользовался. Представь, что я такой разработчик, которому надо заонбордиться в проект, где есть этот Datomic, и ты хочешь, чтобы он искренне и глубоко понял, как эта база данных устроена, как она работает. Я бы, конечно, хотел послушать твоё объяснение.

[17:27] Никита: Можно посмотреть с точки зрения пользователя, и с точки зрения того, как она именно работает, — там тоже очень интересно. С точки зрения пользователя она отличается от Postgres тем, что это TripleStore: у тебя нет таблиц, у тебя есть тройки значений, такие entity — кортежи по-русски. Entity-Attribute-Value.

Соответственно, если ты хочешь какое-то подобие таблицы — условно, не знаю, user с id, email, name, — то в Postgres это была бы большая таблица, а здесь это таблица всего с тремя колонками: e, a и v, Entity-Attribute-Value. Ты вставляешь туда несколько троек. Ты придумаешь какой-то id для этого юзера, или Datomic сгенерирует id за тебя — это будет какое-то число, entity id. У этого entity id будет атрибут user/email и value — собственно, значение этого email. То есть user/email — это keyword, ключевое слово, такой токен, а value — это тот email, который у него есть, например nikitonsky@…me. Это одна тройка, она ставит email этому юзеру. Дальше ты говоришь: чтобы задать тому же юзеру ещё и name, ты составляешь вторую тройку — тот же самый entity id, атрибут другой, user/name, и value, например, «Никита». И если у тебя 10 атрибутов, у тебя составляется 10 троек.

Эта история идёт ещё со времён, когда был такой проект RDF — семантический веб, он назывался. Чуваки хотели запрограммировать, засемантировать вообще всё на свете. И им не подходили таблицы, потому что таблицы очень жёсткие: у каждого ряда есть все колонки, которые в этой таблице есть. А тут у тебя гибкая структура. Почему она гибкая? Если ты это всё потом соберёшь, то у одного юзера может быть 10 атрибутов, а у второго — два. Может быть только user/name и user/id, а телефона не быть. И это значит, что такой тройки в принципе нет. Не то что там пустое значение в колонке, а именно вообще нет.

[18:14] Александр: Так, интересно, это как бы ключевая штука. По-моему, это называется datum, если я не ошибаюсь?

[18:16] Никита: Да-да-да, в Datomic это называется datum — как атом. То, из чего строится всё остальное.

[18:24] Александр: Действительно, атом — отличное слово, лучше не придумаешь. И я ещё раз проговорю, потому что мы всё-таки в подкасте, и там, может быть, человек делал разворот, полицейские на машине… Из чего состоит атом, вот как я его понимаю. Это по сути три значения всегда, неделимая сущность — всё состоит из трёх штучек. Первая штучка — это entity, по сути идентификатор сущности. То есть что это? Вот это юзер — значит, через entity мы определяем, что это. Наверное, там id-шник лежит — число, правильно?

[20:08] Никита: Нет-нет, это просто id-шник. Это числовой id-шник, ты через него ничего не определишь — ни что это за штука, ничего. Это просто число.

[21:00] Александр: Просто число, окей. Entity — просто число. Дальше за ним идёт атрибут — по сути, то, какой кусок данных в этом трипле лежит для этого id-шника. Это email — можно подумать про него как про название колонки: там email, или количество лет, или ссылка на его LinkedIn. И value — непосредственно тот блок данных, который представляет этот атрибут. Насчёт типов: value чем представлен? Это просто byte array или что?

[21:39] Никита: Ну, в Datomic вообще есть типы — число, строка, blob, там много типов на самом деле.

[21:46] Александр: А тип является частью value именно, или атрибут задаёт, что там есть тип?

[21:53] Никита: У атрибута есть типы. То есть в Datomic нужно заранее объявлять атрибуты. Если ты объявляешь, что user/name — это string, то у всех датомов с любым entity id и атрибутом user/name value будет типа string. Это разумное соглашение. Но, в принципе, в самой концепции TripleStore это не обязательно — value может быть любым, а вот именно в Datomic ты объявляешь заранее.

[22:04] Александр: Окей, понятно. То есть если я хочу своему пользователю задать… Кстати, это очень удобно, знаешь, с той точки зрения, что тебе не нужно сразу проектировать полную схему. Потому что — я сейчас сразу думаю, — если бы я делал юзера у себя в проекте, надо же сразу думать, какие поля ему добавлять, потому что потом миграции, всё это nullable/non-nullable, и ты сразу хочешь запечь эту модель данных в схеме, чтобы по минимуму об этом думать. И это создаёт какой-то огромный порог начала работы с проектом. А тут, в Datomic, я так думаю: ну, задам я трипл с таким атрибутом. По-любому мне нужен email, по-любому нужен хэш пароля, по-любому нужен возраст. Всё. Дальше, если что, задам новый трипл с новым атрибутом и буду с ним работать. То есть система эволюционирует за счёт своей структуры очень легко. Правильно ли я улавливаю эту мысль?

[23:24] Никита: Ну, в целом да. У твоих таблиц получается гораздо более гибкая структура. На самом деле это именно что другая модель — это не таблица, это именно TripleStore. И если ты стремишься понять Datomic, тебе нужно понять, что это просто список вот этих триплов. Потом из этой модели следуют всякие интересные следствия.

Например, очень эффективно иметь таблицы с большими дырками. Допустим, у тебя есть миллион юзеров, и только у одного из них есть телефон. В SQL тебе придётся создать колонку «телефон», у которой будет 999 тысяч null, и только в одном ряду будет телефон. А в TripleStore у тебя нет никакого overhead: просто у одного юзера будет дополнительный трипл user/phone с номером телефона, а у остальных его не будет, и это никак не увеличивает ни storage, ничего. То есть дырки хранить очень эффективно.

Потом ты можешь хранить множественные values. Допустим, у юзера есть несколько никнеймов, алиасов, или его предыдущий username, — по модели их несколько. В SQL тебе нужно создать другую таблицу и как-то джойнить её: там нет простого способа к одному ряду приделать множество строк.

[24:27] Александр: Интуитивного способа для программиста однозначно нет. Я всегда думал: да, нам нужно либо JSON городить, либо вторую таблицу городить. А эта таблица — значит, foreign key должен быть, а как это ещё нарисовать правильно… А мне нужен просто массив связать с id-шником. Это же так просто!

[24:44] Никита: Вот. А тут, допустим, у нас несколько юзернеймов — тогда ты говоришь: entity id, user/name, первый юзернейм — значение. Потом добавляешь следующий трипл: entity id, user/name опять же, и второе значение. То есть у тебя просто эти два трипла лежат рядом. Это максимально естественно, и их может быть от нуля до бесконечности — просто ещё один трипл лежит. То есть множественные значения делаются очень легко.

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

И ещё удобная штука — референсы. Референс — это когда какая-то таблица ссылается на другую. В нашем случае одна entity ссылается на другую entity. Типа у тебя юзер имеет родителя, parent-child. Соответственно, ты делаешь атрибут: entity id, parent, и value — это просто entity id твоего родителя. То есть ссылки — это такая же естественная часть триплов, как и всё остальное.

Ты можешь, короче, обратные ссылки сделать — вот что я пытаюсь сказать. Грубо говоря: entity id, атрибут parent, и второй entity id — это id родителя. Из чувака ты можешь пройти наверх к parent всегда напрямую — то же самое ты в Postgres мог сделать, добавив колонку parent. Но, опять же, у тебя есть индекс в обратную сторону. Ты можешь сказать: найди мне все attribute-value, у которых parent такого-то чувака, — и таким образом находишь детей. Короче, у тебя естественным образом ссылки получаются двухсторонними. Ты указываешь их в одну сторону, но пройти в обратную так же просто.

[26:54] Александр: Это, знаешь, как будто есть что-то общее с графами, с графовыми базами данных. То есть ты можешь ходить по этим данным.

[27:54] Никита: В каком-то смысле — да, это графовая база данных, наверное.

[28:00] Александр: Интересно. Это такой альтернативный взгляд на то, как можно представлять данные. Довольно непопулярный.

[28:09] Никита: Это уж точно. Хотя казалось бы.

[28:12] Александр: Мне всегда нравилась идея, когда тебе дают не инструмент, не систему, с которой ты должен учиться работать, а структуру данных и некоторые правила работы с ней, которые ты как программист интуитивно усваиваешь очень быстро — потому что ты всегда это делаешь, работаешь с этим в программировании. И ты такой: ну давайте я буду работать с этим и в базе данных. И ты начинаешь из этой структуры данных уже сам строить модели данных. Тебе не говорят «построим модель данных так» — ты такой: да я сам могу построить. И вот эта гибкость и какая-то творческая штука — что можно по-разному организовать того же самого пользователя в зависимости от задачи — она как будто вдохновляет меня. Я бы хотел поработать с такой базой данных, потому что мне было бы интересно это делать. У меня был бы полёт мысли шире, глубже, дальше, и я бы, может быть, больше работал, чем сидеть напротив Postgres и пытаться понять, что там оптимизатор опять тупит, и чувствовать себя тупым. Меня как будто вдохновляет работать с такой системой. У тебя есть такое?

[28:19] Никита: Да, мне в целом Datomic нравится гораздо больше, и по многим причинам. Во-первых, конечно, интересно — это действительно нестандартная модель, она немножко другая. Она эквивалентна по мощности тому, что делает обычный SQL, именно в плане хранения данных. Мне кажется, какие-то вещи даже более натурально в ней выглядят. Единственное, что не очень удобно, — это искать entity. Скажем, тебе нужно найти всех юзеров. Это обычно не самая удобная операция, потому что ты не знаешь, какие колонки у них есть: у кого-то может не быть телефона и так далее. Но, как правило, есть какой-нибудь user/email, обязательный для всех юзеров, и ты, в общем-то, ищешь по этому атрибуту. На практике это решается.

Короче, всё это сделано не просто так, а ещё и для того, чтобы вместо SQL использовать Datalog как язык запросов. Одно дело — хранить данные, а другое — как ты с ними работаешь. Datalog — это язык для запросов, который во многом полагается на модель TripleStore. И выглядит он на самом деле как паттерн-матчинг. Ты пишешь прямо такой же triple, в котором говоришь: найди мне все записи с таким entity, атрибутом и value. Но на месте любой из этих штук у тебя может быть вопросик. Ты такой: найди мне все — entity id 128, attribute user/name, value вопросик, — и он найдёт тебе все value у данного юзера с данным юзернеймом. Если ты скажешь: entity id 128, attribute вопросик и value вопросик, — он найдёт все атрибуты и их значения у этого юзера. Если скажешь: entity вопросик, дальше user/name, «Вася», — он найдёт все entity id с атрибутом user/name и юзернеймом «Вася». Короче, такой прикольный паттерн-матчинг: очень естественно записывается, очень легко читается.

И дальше — мощность всей этой истории в том, что ты можешь написать несколько таких паттернов подряд, и они все джойнятся друг с другом. Например: ?e, user/name, «Вася», — а потом тот же самый ?e, user/age, ?age. Первый паттерн найдёт тебе все entity id у юзернейма «Вася», а второй у этих entity id найдёт их возраст. Таким образом ты узнаёшь возраст Васи. И ты можешь делать довольно длинные цепочки. Там есть всякие штуки — вызовы функций; по умолчанию это всё складывается как and, можно на or переключиться. И есть ещё рекурсивные правила: ты можешь делать правила, которые позволяют рекурсивно всё это проходить. Например, начинаешь с какого-то юзера, ищешь всех его детей, потом их детей, их детей — пока не упрёшься в конец. Это по мощности эквивалентно SQL, но плюс ещё рекурсия. На самом деле, немножко не так — не совсем эквивалентно SQL, есть и минусы, не всё напрямую портируется, но способ есть.

[33:43] Александр: Как будто бы, когда ты начинаешь мыслить в такой системе, ты обретаешь какие-то новые ограничения — тебе нужно по-другому придумать, как решить задачу, которую ты уже знаешь, как решать в SQL. Это на самом деле, мне кажется, довольно сложная тема для людей: когда ты уже знаешь, как это решается где-то, и решается довольно просто, а в новой системе надо решить ту же задачу новым способом. Это дискомфортный мыслительный процесс, я бы так сказал. Если бы я первый раз это увидел и не знал бы, что есть SQL, мне, может, даже проще было бы это решить, потому что ты не знаешь, как по-другому. А тут ты постоянно такой: блин, я бы сейчас просто селектом сделал, а нет. С другой стороны, ты перестаёшь думать про какие-то вещи, которые нагружали твою оперативную память. Например, из того, что я слышу, — как будто ты ни разу не искал само слово «индекс» как сущность, которую ты создаёшь и которая как-то должна существовать. Как будто есть какие-то индексы, встроенные в саму структуру, но если тебе нужно что-то выбрать, ты не делаешь какой-то специальной сущности, трипла под названием «индекс». А как вообще разные проекции, разные выборки данных решаются? То есть проблема, которую в SQL решают индексами, — как она решается в Datomic, есть ли она вообще?

[34:35] Никита: Да. Сначала я тебе скажу про непривычность — она абсолютно точно есть. У меня, кстати, в какой-то момент был обратный процесс. Я долгое время работал в Datomic — не знаю, лет пять, а может, и десять. И недавно перешёл обратно на SQL и такой: блин, как здесь неудобно, а вот это что, нельзя сделать, а вот это не получается. То есть как всё сложно у вас тут, как много писать. Это абсолютно только вопрос привычки.

А про индексы такая история. Вообще, вся идея этого TripleStore в том, что ты можешь делать универсальный индекс. В Datomic есть несколько базовых индексов. Первый — Entity-Attribute-Value: буквально сначала по entity, потом по attribute, потом по value. Если ты ищешь все атрибуты какой-то entity, ты используешь этот индекс. Если ищешь value конкретного атрибута конкретной entity — тоже этот индекс. Потом есть Attribute-Entity-Value — грубо говоря, тебе нужно найти всех юзеров: делаешь запрос по юзернейму, и тебе нужно, чтобы индекс был по атрибутам, то есть атрибут на первом месте. И третий индекс — там то ли три, то ли четыре их, они условные — это Value-Attribute-Entity, либо Value-Entity-Attribute. Это обратный индекс — именно для прохода ссылок в обратном направлении. Ты знаешь, что вот эта штука — ссылка, и тебе нужно пройти её в обратном направлении: ты начинаешь с value. Найди все триплы, в которых значение атрибута child равно вот этому чуваку, — то есть пройди child в обратном направлении.

Там, на самом деле, есть заморочки. Если совсем ужиматься, то можно, по-моему, двумя индексами обойтись, но в Datomic их три. И ещё иногда есть Attribute-Value-Entity индекс — это просто чтобы искать рейнджи. Тебе нужно найти всех сотрудников в возрасте от 20 до 40 лет: атрибут — это возраст, value — от 30 до 40.

Соответственно, вот этот индекс — единственный, который нужно явно включать. Ты можешь некоторым атрибутам сказать: индексируй значение этого атрибута. Если тебе нужно читать по атрибуту, то индекс для этого атрибута надо включать. Иначе у тебя всё на всё индексируется. То есть если ты ищешь юзеров по email и по возрасту, тебе нужно для email и возраста включить этот индекс. Он там автоматически включается, если у тебя значение уникальное, потому что для проверки уникальности индекс всё равно нужен. Есть один момент, где тебе нужно включать индекс, и он включается на уровне именно атрибутов: этот атрибут включай в индекс, остальные не включай. То есть это как бы один большой индекс всё ещё, но атрибут включается в первый.

[38:00] Александр: Интересно. Я, когда смотрю на Datomic… Мы сейчас сразу начали говорить про триплы и про то, как это устроено. Мне прям нравится, что ты с этого начал, потому что это как бы атомы — то, на чём строится понимание системы. И оно у меня действительно проясняется, я прям начинаю понимать, как бы я строил данные приложения в этом. Хотя бы уже не страшно. Но ребята из Datomic — я так понимаю, за ним стоит компания, потому что они предоставляют какие-то enterprise-версии, — они просто используют немного другие слова, там, в интродакшене где-нибудь в начале. Они говорят про транзакции, про ACID, про все эти гарантии, про Serializable — сразу бьют в эту сторону. А мы за уже 40 минут разговора ни разу не упомянули ни транзакции, ни ACID. Может быть, про это можно поговорить?

[38:58] Никита: Давай. Мы закончим, наверное, с юзерской частью истории и перейдём на архитектуру — они, на самом деле, всё связаны. Короче, смотри: я тебе сказал, что не все вещи можно делать на Datalog, которые можно делать в Postgres. А именно — какие вещи нельзя сделать на Datalog? Нельзя сделать order by и limit. Вот этого в датомиковской версии Datalog нет. И их ответ на это: ты просто выбирай все данные, а потом сам постобрабатывай их — пиши функцию, которая делает order by, group by и limit. А group by, на самом деле, есть.

[39:36] Александр: Тут прибегают мамкины оптимизаторы и говорят: ты что, сошёл с ума? Все данные выбирать!

[39:42] Никита: Это будет небольшая закладочка вперёд, потому что в какой-то момент станет понятно, почему так и почему это не такая безумная идея.

Про транзакции. Когда тебе нужно писать в базу данных, это выглядит примерно так: ты буквально пишешь штуку типа «вставь triple» и «удали triple». То есть транзакция — это последовательность вот этих триплов, у которых есть ещё дополнительный атрибут: вставить или удалить. Если тебе нужно вставить юзера, ты пишешь n триплов db/add: db/add, entity-attribute-value, который нужно вставить; следующий db/add, entity-attribute-value. Если ты хочешь удалить какое-то поле у юзера — допустим, убрать email, — ты пишешь db/retract, entity-attribute-value, который нужно убрать. И этот triple просто вычёркивается из этого мешка. Там есть несколько удобных функций: удали все триплы у данной entity, или можешь передать мапу, и она автоматически развернётся в эти db/add.

[41:11] Александр: То есть можно группой триплы вставлять в рамках одной транзакции?

[41:11] Никита: Да-да-да. В одной транзакции это набор вот этих операций последовательно. В конечном итоге всё раскрывается в две базовые операции: добавить трипл и убрать трипл. Ну, и можно перейти тогда к архитектуре самого Datomic.

Короче, архитектура у него тоже во многом нестандартная. Одна из главных инноваций, которые он предложил, — помимо того, что используется Datalog и TripleStore, — это то, как устроена база данных. Они называют это так: «мы деконструировали базу данных». А именно — они разделили read path и write path, путь записи и путь чтения. И всё это начиналось с истории: слушайте, вот как обычно работает база данных — есть какая-то машинка, на ней крутится процесс, и этот процесс пишет в локальную файловую систему. Они говорят: а зачем? И дальше есть ты, юзер, который с этой базой данных общается. И вот зачем мы ходим к этому процессу, чтобы он посмотрел на свой диск, взял оттуда кусочек индекса и вернул нам, если мы можем точно так же сами сходить на сетевой, условно, диск и спросить его? Мы всё равно ходим по сети, network latency есть в любом случае, — но если он есть, то мы можем просто сходить напрямую в network-attached storage и взять оттуда то, что нас интересует. Зачем нам этот промежуточный процесс? Потому что промежуточный процесс — это overhead и точка конкурентности: мы ограничиваем, когда все должны сходить на поклон к этому Postgres, чтобы он дал им прочитать данные. Это точка синхронизации, которая при большом количестве чтений — а чтений обычно больше, чем записи — может служить бутылочным горлышком.

[42:45] Александр: Так-так-так, подожди, я сейчас… Это очень важно. Так мало кто работает, и мне кажется, слушатели могли пропустить вот эту суть. А суть в том, что в стандартных базах данных у нас почти всегда есть клиент и сервер. Серверов может быть много, клиентов, понятное дело, много, и клиенты подключаются к серверу по сети. Через сервер, через какой-то протокол — REST, TCP, кастомный, как угодно — просим у сервера: дай мне вот это. Сервер идёт, шуршит там своими процессорами, идёт на диск или в network-attached storage, достаёт и выдаёт: вот тебе, клиент, данные. И клиент такой: о, данные. А в Datomic сказали следующее: нахрена нам сервер? Клиент может сходить туда сразу. То есть это, как бы, serverless база данных?

[43:25] Никита: Ну, можно так сказать, да. Когда ты говоришь «сервер» — это именно сервер базы данных, а «клиент» — клиент базы данных, который в обычной архитектуре будет твоим сервером приложения.

[43:43] Александр: Да, мой сервер приложения — это клиент базы данных.

[43:46] Никита: Всё верно. Их наблюдение именно в том, что у нас есть network-attached storage той же latency, и мы можем ходить туда напрямую. То есть мы убрали ограничение на чтение — перестали завязываться на этот центральный процесс базы данных, читаем сами. Это одно решение.

Насчёт записи ситуация другая. Записи так сделать нельзя, потому что конкурентные записи — тебе нужно гарантировать эти ACID и так далее: транзакции, откат, прочую херню. Так сделать нельзя. Соответственно, что они предлагают? Процесс базы данных, сервер базы данных, есть — он называется транзактор, — но его работа именно обслуживать записи. Не чтение, а только записи. Ты держишь connection до него, ходишь и говоришь: чувак, вот моя транзакция, пожалуйста, примени. Он такой: окей, — и применяет. И, соответственно, обновляет storage и шлёт тебе обновление: storage обновился. То есть вторая функция этого транзактора — присылать клиентам базы данных информацию о том, какая последняя версия. По сути, инкрементировать чиселку на единицу и рассылать её всем клиентам. А чтение дальше идёт напрямую, без его участия.

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

[46:12] Александр: Блин, прикольно, знаешь, это такая рок-н-рольная мысль и в целом действие такое. Что нам диктует рынок, все вендоры, весь интернет? Что ACID, все эти уровни изоляции транзакций, как это сложно, — всё это нужно знать. Создаётся вокруг этого такой ореол комплексности, и что в это нужно инвестировать, в изучение всех этих баз данных. У меня в целом подкаст этому практически посвящён: чтобы понимать, как они работают, эффективно с ними работать и, возможно, какие-то такие системы разрабатывать. И в то же время есть просто Datomic, который такой: йоу, один транзактор, однопоточный, на хрен ваши проблемы — вы их сами себе придумали, а их нет. Это так круто. Это такая смелость.

[47:01] Никита: Да-да, это так. Это из серии, что мы знаем, что мы не решим абсолютно все проблемы. Наверняка есть какой-то мега-супер-хайлоад, которому этого будет недостаточно. Но в подавляющем большинстве бизнес-приложений этого хватит выше крыши. Поэтому — не парьтесь, примите это как данность. На самом деле, это жёсткая мысль: что действительно в подавляющем большинстве случаев нам хватит однопоточного writer’а. И когда к тебе приходят такие интерпрайз-инженеры и говорят: «ты чего, однопоточный, отказоустойчивость, строля-тополя», — ты думаешь: блин, ну, походу, реально я чего-то не понимаю. Тебя убеждают, что тебе нужно принести эту сложность в свою архитектуру. Но если реально так, out of the box подумать, — да нет, чел, ну а чё? Какая нагрузка? Реально, какой нужно достичь нагрузки на запись, чтобы тебе одного сервера не хватило по пропускной способности? Это же огромная нагрузка вообще-то. И если у тебя такие проблемы — ну, я тебе завидую, чувак, у тебя успешный проект.

[48:14] Александр: Да-да-да.

[48:14] Никита: Я не помню, какие они там цифры называют, но у них что-то типа тысяча записей в секунду, транзакций в секунду. Может, больше, могу на порядок ошибаться, не уверен. Но по факту хватает: если ты не делаешь что-то прям супер на весь мир — хватает. И да, это действительно рок-н-рольная мысль.

Есть такая штука: у нас в индустрии часто принято использовать инструменты, которые как супергигантские промышленные экскаваторы, — знаешь, чтобы выкопать себе ямку на заднем дворе. Если ты хочешь открыть магазинчик возле дома, тебе не нужна система поставок для какого-нибудь Walmart, это будет оверкил. И на самом деле, если ты осознаёшь свой масштаб, столько вещей становится проще: ты умещаешься на одну машину, на одну базу данных. Помнишь, даже история со стеком Stack Overflow это демонстрировала: они такие — у нас всего четыре сервера. Мы сделали всемирно интересный сайт, у нас всего четыре сервера, одна база данных Postgres, мы его вертикально масштабируем, этого хватает. И все такие: опа, нифига себе, а мы тут городим Hadoop-шмадуб при нагрузке в 100 раз ниже. Это очень хорошее, полезное наблюдение. Я всегда люблю систему поменьше, попроще, которой всё равно хватает. Потому что компьютеры, охренеть, какие быстрые.

[50:59] Александр: Да, они же не стоят на месте, реально. Один комп может просто переварить огромное количество данных очень быстро и эффективно. Если ещё оптимизировать так, чтобы это всё батчами, микробатчами делалось, ты вообще не упрёшься. Максимум упрёшься, может быть, в сеть, в latency, потому что этот сервер может где-то очень далеко быть. Но это очень узкие кейсы. А взамен что мы получаем? Мы убираем огромное количество сложностей. Такую систему, я такой думаю, — она у меня в голове помещается, и я могу её один развивать, поддерживать, и всё нормально будет. Одного бэкендера, там двух, достаточно для такого сетапа, как будто бы. То есть и команда меньше нужна, и как следствие, ресурсов ты меньше тратишь на то, чтобы поднять приложение. Мне кажется, в целом мы сейчас как индустрия в эту сторону движемся — может быть, не в сторону Datomic, но в сторону уменьшения костов, команд и более быстрого движения вперёд, в том числе агент нам это позволяет, и код мы уже быстрее пишем. Интересная тенденция, я прям её наблюдаю. Не знаю, насколько ты замечаешь, но я прям чувствую на себе.

[51:06] Никита: Ну, тенденцию не знаю, но в целом мне кажется, это здравый смысл — логичное направление.

И ещё, самая интересная часть, — нужно сказать про Datomic про чтение. Это тоже нестандартное решение, которое они предложили. Почему нужен сервис? Все думали, что нужен процесс базы данных, который будет отвечать на твои запросы. Почему запрос исполняется внутри Postgres? Потому что якобы Postgres находится рядом с данными. Если у тебя есть таблица на миллиард записей, а тебе нужно 100, было бы странно забирать этот миллиард, а потом фильтровать у себя. Postgres находится рядом, он может посмотреть в индекс, выбрать нужные записи и послать ровно 100 тебе — сетевой коммуникации меньше. И наблюдение Datomic, вставка, которую они сделали: к тому времени начали появляться в Amazon сетевые диски, latency которых была сравнима с чтением с локального диска. Соответственно, если кто-то должен прочитать данные — Postgres или кто-то ещё, — а latency та же самая, то почему бы тебе их не прочитать? Тебе, твоему процессу, клиенту базы данных.

Архитектура Datomic строится на том, что клиент базы данных ходит напрямую, читает из storage, и предполагается, что это не сильно медленнее, чем читать с локального диска. И у него есть все те же данные, которые в нормальной ситуации исполнял бы Postgres, — но они исполняются у тебя на процессе. Ты можешь смотреть в индекс точно так же, не запрашивать лишних данных, но если тебе нужно обработать миллион записей, сделать группировку, — ты поднимаешь эти данные из хранилища, пробегаешься по ним, группируешь в память и выдаёшь юзеру. В принципе, тот же самый процесс произошёл бы внутри Postgres. Это не то чтобы какая-то магия — Postgres пробежал бы тот же миллион рядов, сгруппировал бы их и отдал тебе ответ. Просто эта логика перенесена в клиента базы данных, и он общается с тупым storage.

Это позволяет делать некоторые прикольные штуки — например, вызов произвольной функции на данных. Внутри запроса ты можешь написать произвольную функцию: которая, не знаю, делает капитализацию имени, или умножает возраст на пи, или ещё что-то. Любой Clojure-код, который написан в твоём локальном процессе, ты можешь использовать внутри запроса. Второй момент: ты можешь использовать всю мощность Clojure для того, чтобы обрабатывать результаты. Какая засада с Postgres, которую я сейчас очень сильно переживаю? Что если тебе нужно сделать что-то сложное, тебе приходится извернуться, но засунуть это внутрь запроса. У тебя нет других опций — нужно извратиться, но написать это на SQL. А иногда приходится серьёзно извращаться. У нас, допустим, есть — мы делаем базу данных — lookup references: иногда ты хочешь обращаться к entity не по её id, а по, допустим, username, и тебе нужно этот lookup зарезолвить, прежде чем выполнять какие-то операции. И вся эта логика по resolving’у написана внутри запроса. Причём внутри каждого запроса, который мы делаем, потому что оно везде нужно. И это проблема, потому что язык Postgres — это SQL, и тебе нужно исполнять эту штуку рядом с данными, на которых это исполняется.

А в случае Datomic ты можешь выбрать какой-то набор данных — они у тебя так и так считаются по сети, так и так попадут в локальный процесс, — потом запустить свою кастомную логику: какая-то кастомная группировка, пробежаться, посчитать какие-то averages. И потом запустить следующий запрос, который на основе этого ещё что-то сделает. И вся эта промежуточная постобработка будет на чистом Clojure написана. Ты ничем не ограничен, можешь делать всё что угодно. Это очень сильно развязывает тебе руки: представь, что все сложные моменты, которые тебе приходилось писать в SQL, ты теперь можешь писать на своём любимом языке программирования — в данном случае на Clojure. Потому что ты как бы принёс базу данных внутрь своего процесса и можешь всё это программировать.

Это снимает кучу проблем и с создателей Datomic: им не нужно писать миллион разных хитрожопых функций, которые когда-то кому-то могут пригодиться, — какие-нибудь хитрые агрегации, average, медианы, статистические функции. Ничего этого не нужно писать, потому что если юзеру это нужно, он просто напишет это сам или возьмёт библиотеку и вызовет как код. А у Postgres такой возможности нет — тебе нужно всё это предоставлять внутри языка SQL. Такая вот история. Поэтому, как я уже говорил, order by, limit — их нет, group by есть, — но ты можешь всё это запускать в своём процессе.

[56:16] Никита: Стоит, наверное, проговорить: если у тебя миллиард записей — допустим, миллиард кликов пользователей по сайту, метрики вдруг решил складывать, странно, конечно, но окей, — и тебе нужно топ-100 последних кликов выбрать, это не значит, что ты миллиард записей будешь физически тянуть на клиент. Это всё делается через какие-то ленивые коллекции, курсоры, итераторы. Если ты правильно работаешь с инструментом, то лимит сделаешь так, что он вычтет тебе какой-то первый батч — не знаю, 1024 записи, — из них возьмёт первые 10. Вот всё, что по сети погоняется. То есть не надо приходить и кричать, что «они все данные тянут на клиент». Это бы просто не работало, если бы всё так тянулось тупо.

[57:19] Александр: Да. То есть получается, что как продукт Datomic — какие у него факты? Я так понимаю, это такая жирненькая client-side библиотека, которую ты себе подключаешь и которая содержит в себе весь этот код, всю эту машинерию, предоставляет тебе интерфейс, с которым ты работаешь. Ты в эту библиотеку при создании каких-то объектов передаёшь, наверное, какой-нибудь удалённый диск и путь до транзактора-процесса. И получается, сам транзактор, сервер, который тоже нужен, — это вторая часть Datomic как продукта. И вот что я подумал: получается, что очень много кода написано на клиенте, в этой клиентской библиотеке. Я так понимаю, что она изначально написана для Clojure, правильно? Что с другими языками? Мне интересно: если я хочу использовать это, допустим, из Rust — я сейчас на нём программирую, мне было бы интересно, — я же понимаю, что это не как с Postgres: взял, протокольчик в тоненьком клиенте реализовал, и дальше сервер делает работу. Здесь же работа должна быть написана на Rust, или какой-то локальный процесс поднимать нужно?

[58:28] Никита: Да-да, короче, это засада, большая засада на самом деле. По-хорошему, Datomic нормально можно использовать только из Clojure или из Java. Но с Java, наверное, со скрипом. Потому что именно из своей архитектуры: у тебя предполагается, что этот storage находится in-process, и это жирный клиент. У них есть опция, которую они предлагают: запускать вот этого клиента — он называется Peer, вот этот умный жирный клиент — как standalone-приложение и общаться с ним по REST. Соответственно, из другого языка. Я не знаю, делает так кто-то или нет. Ты теряешь, очевидно, все преимущества Datomic — ну, или значительную часть преимуществ. Поэтому не знаю, насколько это актуально, но в принципе это возможно, они предлагают.

[59:22] Александр: Да, вот тоже такой моментик: что по-хорошему Datomic равно писать на Clojure.

[59:30] Никита: Ну да-да-да. Поэтому мне кажется, он не увидел большого распространения. Если бы это можно было придумать как-то так, чтобы использовать из более популярного языка, то, возможно, жизнь была бы вообще другой. Но, к сожалению, нет — не получается. Можно сделать Datomic для, не знаю, какой хороший язык, — для JavaScript, для Python, наверное. Но это прям другой проект.

[59:56] Александр: Да, это другой проект. Знаешь, дух проекта, идея и вообще те мысли, которые прослеживаются в Datomic, как будто очень сильно переплетаются с идеями Clojure — с тем, как позиционирует себя этот язык среди языков программирования. Так же и Datomic позиционирует себя среди баз данных: он тоже такой сильно обособленный, требует некоторого изменения в мышлении, вводит какие-то свои структуры данных, которые не всем. И мне кажется, в императивном языке каком-нибудь, например, Go, просто не прижилась бы эта идея — она не нужна никому, там всё по-другому.

[1:00:33] Никита: Есть такое, может быть. Да, мы ещё, короче, не сказали пару важных вещей. Одна: ты сказал, что есть вот этот Peer, умный жирный клиент, и транзактор, который запускается, — это две важные части операции. И третья часть, естественно, — storage. В Datomic есть такая особенность: он подключается почти к любому storage. Эти данные, датомы — на самом деле индексы, а в индексах находятся датомы, — их нужно где-то хранить. И одно из решений Datomic было такое: мы не будем городить ничего своего, наоборот, позволим вам использовать существующие сервисы, в том числе существующие базы данных. Ты можешь хранить это в dev-режиме с локальной базой данных, можешь хранить в Postgres, можешь хранить в Amazon — по-моему, в нескольких вариантах амазоновских баз данных. То есть им, в принципе, пофиг на storage, и его можно даже pluggable сделать: написать под свой storage, потому что им от этого storage нужно не очень много. Им нужно хранить блоки, и им нужна атомарная операция compare-and-swap. По-моему, вот и все их требования. То есть там буквально у тебя будет таблица с двумя полями — id и blob, — знаешь, и всё, в общем-то. И, по-моему, parent id, чтобы деревья эти хранить. Поэтому он подключается к любым storage, но они буквально используются как тупые storage. Если ты подключишь его к Postgres, он будет хранить это там, используя абсолютно ноль возможностей Postgres, кроме, собственно, хранения. Если к SQLite — то же самое.

А второй момент, очень важный — на самом деле, надо было сказать в юзерской части, — мы не упомянули, что Datomic это иммутабельная база данных. Что тоже звучит очень странно, но, по сути, они повторяют философию Clojure, когда у тебя данные по умолчанию иммутабельные, но ты всегда можешь создать из них копию с какими-то изменениями. Datomic — вся база данных работает точно так же. Это одно из самых больших преимуществ Datomic перед SQL.

Ты говоришь: Datomic, при коннекции дай мне… у тебя здесь connection. В какой-то момент ты говоришь: дай мне базу данных. И он тебе даёт какую-то ссылку. И эту ссылку ты используешь, чтобы запускать все запросы, все чтения делать. И пока ты используешь эту ссылку, у тебя будут абсолютно консистентные результаты. Кто-то другой параллельно может писать в эту базу данных, коммитить, транзакции проходить будут, — но пока ты держишь эту ссылку и делаешь по ней запросы, ты будешь видеть эту базу данных такой, какой она была ровно на тот момент, когда ты сделал этот запрос. Эта ссылка в себе вот эту эпоху, этот таймстемп, эту чиселку хранит и всегда её передаёт, и ты относительно этой циферки видишь актуальные данные. Ну, не актуальные, а на тот момент времени актуальные.

В Postgres это сложнее. Тебе нужно делать какой-то isolated read, специальный тип транзакции, более дорогой, и держать открытую транзакцию — тогда получишь такую штуку. Но по умолчанию так, конечно, никто с базами данных не работает. Обычно как делают? Прочитали список пользователей, запустили запрос, получили результат. После этого ты говоришь: а давайте по каждому пользователю посчитаем, не знаю, сколько у него постов. И запускаешь второй запрос. Так вот, в момент, когда запускаешь второй запрос, какого-то пользователя могли удалить между этими двумя запросами. И ты никак эту ситуацию не обрабатываешь.

[1:04:25] Александр: Это фантомная запись, одна из аномалий транзакций.

[1:04:27] Никита: Да, всё строится на предположении, что за это короткое время ничего страшного не произойдёт. Но оно может произойти — это возможность. Если ты серьёзно относишься к своей системе, если действительно нужно прям серьёзные штуки посчитать, ты не можешь на это полагаться. Если ты банковские операции делаешь, допустим: выбрал список карточек пользователя, а потом тебе нужно просуммировать количество денег на каждой карточке. А что, если юзер в этот момент разделил карточку на две? У тебя получится вообще ерунда.

[1:04:51] Александр: Или удалил карточку — не сойдётся баланс.

[1:04:55] Никита: Короче, вот. И Datomic решает эту проблему очень красиво и естественно — в духе Clojure, опять же. Это то, как базы данных, по идее, должны работать. Но обычно это какой-то сложный геморрой, чтобы так устроить, а в Datomic это максимально естественно: ты получаешь консистентный вид.

Второй момент на эту тему: ты можешь путешествовать во времени. Datomic на самом деле не просто база данных, которая хранит текущее состояние. Она хранит ещё всю историю того, как у тебя данные менялись во времени. Это, собственно, то, что позволяет тебе держать снапшот: сколько ты держишь снапшот, он отображает какой-то момент времени, и ты можешь держать его сколько угодно. Но, в принципе, может пройти время, и ты можешь сказать: отдай-ка мне базу данных на тот момент времени, там, позавчера в 16:45. И он тебе даст такую же ссылку, и ты точно такие же запросы сможешь выполнить, как будто ты был вот тогда, — как вся система, вся база данных выглядела на тот момент времени, так данные и будут выглядеть. Это супер-мега-фича для всяких исторических запросов, для разбирательств, для аудита: что произошло, почему оно так сработало. Для аналитики: ты запускаешь аналитику в 4 часа утра, но хочешь посчитать её на конец дня. Или если она считается — читается в 4 часа дня, — ты всё равно держишь консистентный снапшот, и у тебя всё сходится.

[1:05:01] Александр: Слушай, а с точки зрения разработки приложения — такого супер-прикладного, вот я разрабатываю веб-сайт какой-то с JavaScript в браузере — и вот я решил использовать что-то подобное Datomic. Ну, кстати, из браузера-то его нельзя использовать? Ты на сервере: пишешь бэкенд для сайта. Пользователь просит какие-то данные, и получается, что каждый раз — если ты сессию нигде не хранил и специально это не сделал — ты берёшь каждый раз новую ссылку на базу данных на момент начала запроса, с этой ссылкой как-то работаешь, что-то возвращаешь, и каждый раз эту ссылку обновляешь. Или есть такое, что ты где-то её хранишь и потом из контекста достаёшь, продолжаешь с этой ссылкой работать, чтобы данные были консистентны?

[1:06:25] Никита: В принципе, оба способа возможны. Внутри одного запроса ты, наверное, используешь — хорошее и правильное решение будет использовать одну и ту же ссылку. Обычно ты пишешь middleware, который в начале запроса складывает эту версию базы данных в контекст, и потом используешь везде. Между разными запросами синхронизироваться сложнее, но если прям заморочиться, то можно, конечно.

[1:08:43] Александр: Просто, знаешь, это такой дополнительный элемент взаимодействия с API, который нужно иногда продумывать. Потому что всё-таки в классических базах данных tooling вокруг них такой огромный, что ты даже особо напрямую с драйвером Postgres редко работаешь — у тебя там будут какие-нибудь прослойки между вами. Ты просто пуляешь запросы куда-то, и тебе пуляют в ответ результаты. Ты даже не думаешь, что там connection pool, какая версия базы данных актуальная. В каком-то виде это немножко даже проще — если не думать. Но в другом виде, когда ты действительно начинаешь сталкиваться с проблемами, когда тебе нужно консистентное состояние более чем одной записи, более чем одной таблицы, а вообще всей базы данных, — эта проблема практически не решается. Тебе надо какую-то жирную транзакцию заводить. И, по сути, разные базы данных под разными соусами говорят — или умалчивают, — что некоторые гарантии они не соблюдают. Например, тот же phantom read, или phantom write это называется: ты взял какой-то сет данных, отобразил его в начале транзакции, что-то поделал, а потом посчитал агрегат по этому же сету, и агрегат может не сойтись, потому что под это условие может новая запись попасть, или из этого условия какая-то запись удалилась. И это в рамках одной транзакции может происходить, во многих базах данных, и никто про это просто не говорит. Когда тебе говорят «ACID», — они же не придумали, а просто есть эти проблемы изоляции транзакций, которые скрываются, — это те нюансы, которые за ACID стоят: собственно, что мы не поддерживаем. А в случае, когда у тебя есть вот эта циферка, эта актуальная ссылка на состояние базы данных, и ты знаешь, что из неё эти данные не удаляются, — у тебя этой проблемы нет. И это очень круто. Тебе тут просто говорят: вот этот вопрос не существует как явление. То, что существует в других базах данных.

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

[1:09:35] Никита: Ну, слушай, сделать как в Datomic нетривиально, но можно. Это показывает, что да, возможно. В принципе, на самом деле, если ты делаешь веб-сайт на Datomic, ты используешь его плюс-минус как обычную базу данных: пришёл запрос, ты дёрнул Datomic, получил какой-то результат. Часто, поскольку у тебя этот Peer локальный, у тебя высок шанс, что многие данные будут уже локально закэшированы. Для одного и того же пользователя, например, если у тебя нормально настроено, скорее всего, у тебя уже там всё будет.

[1:10:06] Александр: Вообще, я так подумал, что масштабирование в этом случае тоже довольно прикольно начинает работать, особенно на чтение, потому что чтение отвязано от записи. По-хорошему, если у тебя сервер, который выполняет какие-то сложные расчёты, отчёты строит, там действительно логика. Если ещё учесть, что теперь, когда ты используешь Datomic, ты не отправляешь логику исполнения на сервер базы данных, а сам её исполняешь, то вполне логично требование, что иногда тебе нужно будет больше одного сервера, просто потому что ты будешь в CPU упираться. И в таком плане тоже очень просто это всё масштабируется: ты взял просто этот execution-слой, замасштабировал, настроил так, чтобы один и тот же клиент ходил на один и тот же сервер, данные для одного клиента уже там закэшировались, и всё работает супер. До тех пор, пока ты не захотел что-то записать или прочитать что-то новое, — ну да, сходишь на диск. Но тоже, как будто бы, если ты используешь дата-центр с нормальными проводами, это вообще не проблема, это недолго.

Ты сказал хорошо, что в S3 появились… кстати, когда это случилось, я не знаю, мне кажется, это на моём веку, — диски, которые сопоставимы по скорости с локальными.

[1:11:43] Никита: Слушай, мне кажется, это с самого начала: когда Amazon начал делать свои облака, уже примерно тогда они были. А в целом ты правильно говоришь: когда этот Datomic создавали, рекламировали вначале, одно из преимуществ было — говорят, вот вы хотите делать аналитику, просто поднимите новый бокс, крутите на нём аналитику, он не будет ничему мешать. Все ваши веб-серверы будут работать под такой же нагрузкой, никак не замечая того, что где-то считается аналитика. И база данных не будет перегружаться. Если вас Postgres не увозит, то что вам нужно? Вам нужно какую-то репликацию придумывать, а репликация — это задержки и прочее. А тут вы подняли второй веб-сервер, у которого такой же Peer, который крутит логику базы данных, — и вот вы уже зашардировали, считай, чтение. Не зашардировали даже, а просто как-то зареплицировали, что ли. Короче, поделили нагрузку, которую в нормальной ситуации исполняла бы одна база данных, на N веб-серверов, — и не хватает добавки ещё.

[1:13:03] Александр: Опять же, больше контроля у инженера, больше вот этого контроля над системой, глубже понимание, комфортнее ты себя как инженер ощущаешь, потому что понимаешь, как эта система работает. И, наверное, кстати, — может быть, есть какие-нибудь байки из продакшена? Ну, наверное, какие-то проблемы всё-таки случались в эксплуатации. Какого рода вообще могут быть проблемы? Вот в классической базе данных я тебе накину: понятно, твоя проблема норм, но, да, какой-то запрос тормозит иногда. И ты начинаешь: логаешь этот запрос, смотришь в какой-нибудь Kibana по трейсингу, что там за SQL приходит, понимаешь, что вот этот SQL выполняется долго, смотришь на его план, план запроса тебе показывает, что там индекс продолбался, и ты fullscan делаешь, — берёшь и создаёшь индекс. Короче, типичная проблема эксплуатации какой-нибудь продакшен-базы данных. Есть ли какие-нибудь такие истории про Datomic?

[1:13:57] Никита: В принципе, отлаживание запросов тоже, мне кажется, как будто приятнее, потому что у тебя вся логика крутится на клиенте: ты сидишь и крутишь в REPL прям свой запрос, тебе не нужно отдельно ходить в какой-то Postgres, что-то там смотреть и как-то передавать в него данные, а потом ловить, что там произошло. Оптимизация запросов существует, конечно. В целом, во многом, с одной стороны, Datomic — это суперреволюционная архитектура, они прям перепридумали очень много всего. Но с другой стороны, этот проект был ограничен тем, что его делали несколько человек всего. Мне кажется, это дизайн-ограничение, которое во многом отображается в получившейся архитектуре. То, что я тебе говорил: Datalog — достаточно примитивный язык для запросов, остальное ты можешь сделать сам в юзерском коде. Почему? Чтобы не парить создателей базы данных сильно. Они такие: мы дали вам инструменты, вперёд, ребята. Собственно, TripleStore с индексами, мне кажется, тоже частично из-за этого: ты делаешь максимально тупую схему с тремя колонками, таблица с тремя колонками, и три базовых индекса делаешь, или четыре, — и всё, после этого отстаньте от меня, дальше сами.

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

[1:16:58] Александр: Очень мощная мысль, которая как-то витала у меня в воздухе, но я не понимал, как её преподнести. Это про команду, которая стоит за Datomic. Мы как разработчики, как люди, которые иногда знакомятся с технологиями, выбирают их, читают документацию, строят суждения об архитектуре, — мы почти никогда не принимаем во внимание тот факт, а кто вообще это делает, какие ресурсы у этих людей и сколько времени они этим занимались. А по-хорошему в первую очередь на это мы должны обращать внимание, потому что из этого как раз и вырастает техническое решение, эта архитектура. То есть первично — команда, люди и ресурсы, и потом уже выходит из неё какая-то идея, потому что идея подвержена тем, кто её делает. Они не могут сделать тебе мощную, крутую базу данных, которую 10 лет надо разрабатывать. Они вот так придумали. И от этой мысли — я не знаю, зачем она мне нужна, когда я про это думаю; в целом мне, наверное, пофиг, если меня устраивает архитектура. Но в плане расширения сознания и эрудиции довольно полезно иногда обращать на это внимание: а кто это делает? Кто стоит за проектом?

[1:17:27] Никита: Ну, в данном случае это важно. Я не уверен, что это важно всегда, но в данном случае важно. Тут видишь, какая ещё ситуация. Мне это всё очень импонирует: во-первых, Rich Hickey крутой. А во-вторых, это прикольный момент, потому что, когда у тебя мало людей и мало ресурсов, ты не можешь сделать что-то переусложнённое. Дальше у тебя развилка: если ты не такой крутой, ты просто ничего не сделаешь. А если ты крутой, то придумаешь, как решить проблему минимальными средствами. И как результат этих ограничений получается очень простое решение: в нём минимум компонентов, минимум функций, но оно покрывает основные юзкейсы. Это решение проблемы, но минимальными средствами. Иначе либо ничего не получится, либо тебе придётся радикально всё упростить. А дальше, как юзер, ты на это смотришь и видишь радикально простую систему. Ты такой: отлично, зашибись, она укладывается у меня в голове. Я могу построить себе ментальную модель, как это работает. Она понимаема, здесь нет лишних деталей, которые мне не нужны. И это классно.

Другой момент, когда мне тоже важно, кто делает продукт, — это Sublime Text. Sublime Text мне нравится, потому что его делают три человека, и у них просто нет времени пилить какие-то развесистые ненужные фичи. Зато все те фичи, которые там есть, супер важные, супер принципиальные. И ничего лишнего, никакого шума. Ну, и с Datomic похожая ситуация. И с Clojure похожая: Рич Хикки тоже делал его с небольшой командой. Я как-то смотрел, сравнивал в какой-то момент их жизни Clojure и Kotlin. В Kotlin было раз в 10 больше людей, раз в 100 больше кода — размер репозитория, — чем в Clojure. А в принципе языки примерно на JVM компилируются, примерно на то же дело. Но зато в Kotlin есть конференции, синтаксический сахар, поддержка IDE и всё на свете. А Clojure — такой bare-bones язык. Но зато он был сделан гораздо надёжнее, стабильнее, чем Kotlin. И проще.

[1:18:34] Александр: Я вот тоже так думаю, когда ты говоришь про эти маленькие команды. У меня такая мысль возникла: это ограничения очевидные, ограничения внешней среды, скажем так. И в рамках этих ограничений люди талантливые начинают проявлять смекалку. И это очень круто. Но когда этих ограничений нет — например, у тебя большая компания с бесконечными ресурсами, и ты хочешь сделать язык программирования, — то смекалки будет меньше. Или если она будет, то не такая выпуклая, не такая заметная. Естественно, какие-то сотрудники по-любому её проявляют. Но вот когда в таких условиях, в которых разрабатывался и разрабатывается Datomic, мощь и глубина смекалки — она поражает. И ты прям вдохновляешься этим, как от красивой картины наслаждаешься. Какое-то искусство в этом есть.

Очень круто, интересно поговорили. Есть ли тебе что-то, что именно про Datomic ты бы ещё хотел добавить? Может быть, вдохновить слушателей попробовать? Меня это уж точно вдохновило. Я думаю, что надо будет запустить серию видосиков на ютубчике, где я пытаюсь это делать. Но для этого нужно будет погрузиться в Clojure. Кстати, давно я это хотел — всё откладывал, но, похоже, неизбежное откладывать больше не стоит.

[1:21:06] Никита: Слушай, да, я бы хотел очень покиндовать, но не знаю, как это сделать без Clojure. Это реально проблема. Но в целом даже вот исходный доклад, где они анонсируют Datomic, как он работает, — примерно то, о чём мы сейчас поговорили, — это уже очень интересный и очень eye-opening: что вот как можно, оказывается, действительно. У нас были эти проблемы с базами данных, и мы не знали, что они есть, — оказывается, их можно решить.

Моя, кстати, любимая — я очень сильно ощутил момент перехода на Postgres, опять же к вопросу о сложности. В Postgres ты такой: нужно опять какие-то connection pool’ы держать, как-то к базе хитро коннектиться. Типа, почему connection pool’ы до сих пор не часть Postgres, почему я должен об этом париться? В Datomic ты просто указываешь URL до базы данных, и оно само все connection’ы мультиплексирует, всё что нужно делает, никаких вопросов не возникает. Просто, поскольку это новая штука, они могут пойти и порешать старые проблемы, которые исторически сложились. Короче, рекомендую восхититься, что так тоже можно. Может, кто-нибудь когда-нибудь сделает Datomic для JavaScript.

[1:22:35] Александр: В последние полгода я проводил время на YouTube за стримами, программируя свою базу данных на Rust. Ну, конечно, ничего нормального не получилось, но Rust я изучил.

[1:22:52] Никита: На самом деле модель Datomic очень удобная. Опять же, когда я это всё посмотрел, меня очень вдохновило это реимплементировать. И ты можешь написать движок Datalog, вот такой же, на TripleStore, в очень небольшое количество строк. Мне кажется, там строк 200. Я видел реализации в 200 строк на JavaScript. То есть ты можешь это всё повторить, потому что структура с индексами Entity-Attribute-Value очень простая, её можно сделать на мапах. Она будет, может, не слишком эффективной, но ты можешь буквально на мапах это сделать и там какой-то базовый Datalog тоже соорудить. То есть это просто как упражнение — тоже очень классная штука. У меня это выросло в базу данных для ClojureScript, DataScript, который как бы клон Datomic с некоторыми особенностями. Ну, чуть попроще, но тем не менее. Именно за счёт того, что Datomic так просто устроен, я такой: блин, подождите, это же легко можно воспроизвести. Пошёл воспроизводить — буквально за неделю накидал прототипчик, и оно работало, и я такой: опа, нифига себе. А дальше уже пошло-поехало: ну, тут уже недолго осталось, запросы сейчас добавим, ещё что-то добавим. И в итоге я такой: да что-то уже почти Datomic можно полностью повторить.

[1:23:42] Александр: Да, круто, то есть ты автор библиотеки DataScript, правильно? И этот кейс с фронтендом, который я пытался придумать и упёрся в JavaScript, ты, по сути, решил — ну, просто для ClojureScript.

[1:24:44] Никита: Ну да, это, по сути, такой же TripleStore, который может гоняться в браузере или на JVM тоже. Я его в итоге портировал на JVM, то есть он теперь на обеих сторонах может гоняться. Единственное, что я до сих пор так и не сделал — следующий шаг, кажется естественный, — это синхронизация между ними. Синхронизацию хотелось бы, но даже без синхронизации оно, в принципе, довольно классно работает. И это, помимо того что… Блин, зачем это нужно вообще? На самом деле, вот этот TripleStore с Datalog — очень удобная модель просто для работы с данными. Изначально DataScript была in-memory базой данных, что вообще кажется бредом, потому что ты не можешь это никуда сохранить. Ты просто скачал с сервера какие-то данные — список юзеров и что-то ещё, — положил это в TripleStore и тут же начинаешь в памяти с ним работать, просто чтобы отрендерить страничку. И всё, никуда не сохраняешь, ни с чем не синхронизируешь, просто вот это делаешь. И это уже удобно. Хранить данные в таком супернормализованном виде, в виде триплов, делать по ним запросы, делать эти графовые обходы — это оказалось настолько мегаудобно, что DataScript даже без всякой персистентности и синхронизации многие пользователи нашли полезным.

С тех пор я ещё какие-то server-side-проекты запилил, и просто такой: нужно хранить структурированные данные — DataScript, без вопросов. У меня есть сайт, например, с сеансами всех кинофильмов Германии. И там такая схема, но она не строго вложенная, а именно граф получается. Сеанс зависит: у него есть время по одной оси, а по другой оси — кинотеатр. Кинотеатр находится в каком-то районе, район — в городе, короче, вот это всё. Не сильно большая схема — сущностей пять-шесть, — но это модель данных. И в итоге я понял: а я хочу делать выборки всякие разные. Хочу показать все сеансы данного кинотеатра, или все фильмы, которые идут в это время, или в этот день. И ты потом в разных перпендикулярных срезах хочешь на это смотреть. И ничего проще, чем положить это всё в базу данных в супернормализованном виде, а потом писать запросы: выбери по кинотеатру, сгруппируй по городу, сгруппируй по жанру. Короче, я не придумал ничего проще. Это просто удобная модель для хранения данных, даже если не как базы данных.

[1:28:00] Александр: Вот такой интересный инсайт. Мы, кстати, про это не говорили — что в целом-то структура данных, которую тебе библиотека предоставляет в языке программирования, с которым ты работаешь, очень удобна для работы. Может быть, даже я так сейчас думаю: а вот если бы не было такой структуры данных, и мне нужно было бы решить такую задачу, я бы завёл пять-шесть мап, как-то их через лукапы, какими-то функциями всё обмазывал. Короче, выглядело бы не так элегантно, как ты описываешь с DataScript.

[1:28:31] Никита: Однозначно.

[1:28:32] Александр: И самое интересное, что для такого сайта, как кинотеатры Берлина, насколько я понимаю, у тебя в целом-то всё в память вмещается?

[1:28:42] Никита: Ну да. Ну, там Германия. Сколько там фильмов-то идёт? Ну, тысячи. Ну, и вот ты их положишь. Это очень мало данных. Инсайт такой: там что-то типа 3D-табличка, что ли, получается. По сравнению с ClojureScript, который ты грузишь как Single Page Application, это не так уж и много.

[1:29:00] Александр: Да, круто. Мне очень понравилось. Спасибо тебе большое за такой вдохновляющий разговор. Я надеюсь, что кого-нибудь из наших слушателей получилось тоже вдохновить попробовать Clojure и попробовать Datomic. Спасибо!

[1:29:12] Никита: Да. Пока-пока.