<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>Ivan Grishaev's blog</title>
    <description>Writing on programming, education, books and negotiations.
</description>
    <link>https://grishaev.me/</link>
    <atom:link href="https://grishaev.me/feed.xml" rel="self" type="application/rss+xml"/>
    <pubDate>Fri, 27 Feb 2026 12:51:39 +0000</pubDate>
    <lastBuildDate>Fri, 27 Feb 2026 12:51:39 +0000</lastBuildDate>
    <generator>Jekyll v4.2.0</generator>
    
      <item>
        <title>Совет дня №20</title>
        <description>&lt;p&gt;В прошлый раз мы говорили вот о чем. Когда мы изучаем план запроса, то смотрим,
было ли попадание в индекс. Однако бывает подвох: индекс был, да не тот.&lt;/p&gt;

&lt;p&gt;Скажем, есть запрос: показать топ-100 активных заказов по убыванию
даты. Пожалуйста:&lt;/p&gt;

&lt;div class=&quot;language-sql highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;select&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;orders&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;where&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;status&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'active'&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;order&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;by&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;created_at&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;desc&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;limit&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;100&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Для поля status создан индекс &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;idx_orders_status&lt;/code&gt;, для &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;created_at&lt;/code&gt; —
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;idx_orders_created_at&lt;/code&gt; (по убыванию).&lt;/p&gt;

&lt;p&gt;Смотрим &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;EXPLAIN&lt;/code&gt;, а там примерно такое:&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;limit: 100
  filter scan condition: status = 'active'
    index scan idx_orders_created_at: desc
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Разработчик смотрит: index scan был? Был. Вот и ладушки.&lt;/p&gt;

&lt;p&gt;На самом деле произошло вот что. Вместо того, чтобы взять только активные заказы
и отсортировать их, Postgres сделал обратное. Он пошел по всей таблице (full
scan), просто не в случайном порядке (как записи лежат на диске), а согласно
индексу. При этом записи были отобраны вручную: для каждой из них выполнялась
проверка status = ‘active’. Фактически это был full scan, только в другом
порядке.&lt;/p&gt;

&lt;p&gt;В идеале нам нужен другой план:&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;limit: 100
  in-memory sort, key: created_at (desc)
    index scan idx_orders_status condition: status = 'active'
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Этот план читают так. Сначала Postgres выбирает активные заказы по
индексу. Ожидается, что в таблице много завершенных заказов, поэтому у активных
будет высокая селективность. Далее эти заказы сортируются в памяти по created_at
(уже без всякого индекса) и берутся первые сто.&lt;/p&gt;

&lt;p&gt;Почему был первый план, а не второй? Во-первых, индекса на &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;status&lt;/code&gt; может не
быть. Если он есть, у значения active низкая селективность: доля активных
заказов высокая. Может быть, настало время обновить статистику командой
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;analyze&lt;/code&gt;, и значение &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;active&lt;/code&gt; уйдет из массива частых значений, которые нельзя
брать в индекс. Наконец, можно признать, что тут ничего не поделаешь и мы
согласы жить с первым планом, а не вторым. Возможны и другие варианты, их
смотрят по ситуации.&lt;/p&gt;

&lt;p&gt;Общий принцип такой. Часто фильтры и сортировка тянут одеяло на себя. Postgres
должен решить, что выгоднее: отсортировать по индексу и пройтись вручную или
наоборот: сначала отсечь по индексу и отсортировать в памяти. Второй вариант
предпочтительней: когда выборка мала, любая операция над ней дешева: сортировка,
группировка и так далее. Стремитесь к тому, чтобы &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;WHERE&lt;/code&gt; отсекал как можно
больше записей, и только потом вступали в действие другие операции.&lt;/p&gt;
</description>
        <pubDate>Sat, 31 Jan 2026 00:00:00 +0000</pubDate>
        <link>https://grishaev.me/tip-020-pg/</link>
        <guid isPermaLink="true">https://grishaev.me/tip-020-pg/</guid>
        
        <category>postgres</category>
        
        <category>sql</category>
        
        
      </item>
    
      <item>
        <title>Совет дня №19</title>
        <description>&lt;p&gt;Чтобы узнать план запроса, предварите его командой &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;EXPLAIN&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;explain select * from applications;

Seq Scan on applications
  (cost=0.00..359524.33 rows=965633 width=1745)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Эта команда НЕ выполнит запрос, а только вернет его план и приблизительные
оценки.&lt;/p&gt;

&lt;p&gt;План представляет дерево узлов; вложенность определяется пробелами слева. План
читают от вложенных узлов к корню. Графические программы вроде PGAdmin
показывают его в виде графа. У каждого узла как минимум три характеристики:
стоимость (cost), число записей (rows) и средняя длина строки (width).&lt;/p&gt;

&lt;p&gt;Стоимость (cost) – это условные попугаи, в которых измеряется каждая операция. С
помощью глобальных настроек можно задать свою стоимость некоторым операциям, но
обычно это ни к чему. Кроме того, стоимость можно задать функциям при их
объявлении.&lt;/p&gt;

&lt;p&gt;Число записей (rows) – ожидаемое число записей, которые произведет узел. Для них
рассчитана средняя длина в байтах. Эти числа берутся из статистики и могут быть
неточны.&lt;/p&gt;

&lt;p&gt;Стоимость выражается двумя цифрами: X..Y. Первая цифра – столько усилий уйдет на
то, чтобы произвести первую запись. Вторая – последнюю, то есть все остальное.&lt;/p&gt;

&lt;p&gt;Зачем две цифры? Дело в том, что даже когда запрос производит много записей,
важно знать, сколько усилий требует предварительная работа. Например, у обхода
большой таблицы первый кост маленький, а второй – большой. Это нормально, потому
что записи передаются следующему узлу без задержки. Если же добавить
группировку, то следующий узел не получит данные, пока не выполнится
группировка. Из-за этого первый кост будет большим. В идеале нужно держать его
маленьким, чтобы клиент сразу начал получать строки.&lt;/p&gt;

&lt;p&gt;У команды explain много параметров, чтобы собрать дополнительные метрики
узлов. Например, время в секундах, число прочитанных страниц, попадание в
буферный кэш. Самый важный параметр называется &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ANALYZE&lt;/code&gt;. С ним запрос будет
выполнен, и метрики будут реальными, а не оценочными.&lt;/p&gt;

&lt;p&gt;Модуль &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;auto_explain&lt;/code&gt; автоматически логирует план запросов, которые выполнялись
дольше порога, например одной секунды. Иногда его включают на проде.&lt;/p&gt;

&lt;p&gt;Читать план тяжело, этот навык приходит с годами. Вы должны точно понимать,
какую информацию ищете и почему ее там нет. В простом случае вас интересует,
использовался ли индекс. Вы запускаете explain analyze и смотрите, был ли узел с
типом “index scan”.&lt;/p&gt;

&lt;p&gt;Узел “index only scan” означает, что данные получены из индекса без обращения к
таблице. Это самый жир, лучше которого ничего не бывает. Узел “bitmap index
scan” означает построение битовой карты, где каждый бит — номер блока. Такой
обход используется для совмещения нескольких индексов, и он тоже хорош.&lt;/p&gt;

&lt;p&gt;Узел “full seq scan” означает полный обход таблицы. Это нормально, если таблица
мала или нужна ее большая часть. Если вы рассчитывали на индекс, это повод
пересмотреть его.&lt;/p&gt;
</description>
        <pubDate>Sat, 31 Jan 2026 00:00:00 +0000</pubDate>
        <link>https://grishaev.me/tip-019-pg/</link>
        <guid isPermaLink="true">https://grishaev.me/tip-019-pg/</guid>
        
        <category>postgres</category>
        
        <category>sql</category>
        
        
      </item>
    
      <item>
        <title>Совет дня №18</title>
        <description>&lt;p&gt;Когда Postgres выполняет запрос, он строит план. Это сложная задача: сперва
нужно распрасить запрос, построить дерево, проверить синтаксис. Затем строится
логический план: здесь некоторые шаги разбиваются на подшаги, а другие,
наоборот, группируются в один. На последнем этапе выводится физический план: в
какую таблицу пойти, брать индекс или нет, в каком порядке выполнить джоины.&lt;/p&gt;

&lt;p&gt;Парсинг запроса протекает достаточно быстро, потому что это чистая
функция. Физический план, наоборот, дорогой. Вспомним: для того, чтобы
определить, использовать индекс или нет, Postgres должен проверить, сколько в
среднем строк в таблице; если она маленькая, проще сделать full scan. Далее
нужно проверить, не является ли значение частым (с низкой селективностью). При
анализе джоинов число комбинаций растет факториалом, поэтому Postgres применяет
разные эвристики.&lt;/p&gt;

&lt;p&gt;Подготовленное выражение – это запрос, который прошел этапы выше, и для него
построен план. Далее его можно выполнить с разными параметрами, и (в идеале)
план будет переиспользоваться. В идеале – потому что даже когда подготовленное
выражение готово, оно не гарантирует применения плана. Если параметров нет,
Postgres использует план. Если выражение вызывается с разными параметрами,
Postgres ведет хеш-таблицу: параметры -&amp;gt; план -&amp;gt; время. Для одинаковых
параметров план будет один и тот же. Накопив пять вызовов с разными параметрами,
Postgres наконец-то определится с планом (с минимальным временем), и далее он
используется всегда.&lt;/p&gt;

&lt;p&gt;Некоторые клиенты к Postgres ведут свой кэш вида SQL -&amp;gt;
preparedStatement. Каждый раз когда выполняется запрос, клиент ищет айди
подготовленного выражения во внутреннем кэше. Если он есть, вызывается команда
“выполни выражение foobar с такими-то параметрами” (аналог execute). Если
Postgres ругнулся, что такого выражения нет, эта ошибка перехватывается, и
посылается команда “тогда подготовь этот SQL и назначь ему имя foobar”. Далее
все повторяется.&lt;/p&gt;

&lt;p&gt;Подготовленные выражения живут в рамках одного соединения с БД. Это особенно
важно для пула соединений: когда мы выполняем запросы, то заимствуем подключение
из пула, и они могут быть разными. Клиент учитывает это: кэш подготовленных
выражений ведется в разрезе подключения. По мере ротации подключений
подготовленные выражения распространяются по ним.&lt;/p&gt;

&lt;p&gt;Пожалуй, в этом совете нечего взять на заметку, разве что подумать, сколько
работы делают клиентские библиотеки помимо основных обязанностей. Заодно это
плавный переход к теме планов и их анализа.&lt;/p&gt;
</description>
        <pubDate>Sat, 31 Jan 2026 00:00:00 +0000</pubDate>
        <link>https://grishaev.me/tip-018-pg/</link>
        <guid isPermaLink="true">https://grishaev.me/tip-018-pg/</guid>
        
        <category>postgres</category>
        
        <category>sql</category>
        
        
      </item>
    
      <item>
        <title>Совет дня №17</title>
        <description>&lt;p&gt;Наверное, вы много слышали про UUID версии 7. Дело в том, что недавно его
утвердили окончательно, и он появился в Postgres 18. До этого люди писали свой
UUID на коленке, но теперь все официально.&lt;/p&gt;

&lt;p&gt;В целом UUID – это контейнер из 128 бит. Первые несколько содержат информацию о
версии, остальное заполняется в зависимости от нее. Самый популярный стандарт
сегодня – UUID 4, который полностью случаен. Есть и другие алгоритмы: например,
1 и 2 использовали MAC-адрес машины, на которой генерировались, и таким образом
раскрывали системную инфу.&lt;/p&gt;

&lt;p&gt;Чем хороша версия 7? Ее первые 48 битов содержат время Unix в миллисекундах. Это
значит, такие ключи сохраняют порядок в разрезе тысячной секунды. Кроме того, из
него можно извлечь дату и время, а также получить UUID из времени, зная, как
заполнить хвост. Уникальность плюс время, два в одном.&lt;/p&gt;

&lt;p&gt;Существует три подмножества UUID v7. Они различаются тем, как заполняется
хвост. Версия a) хвост заполняется рандомом. Версия b) используется возрастающий
счетчик из 12 бит, остальное – рандом. Версия c) используется два счетчика: 12 и
10 бит (могу быть неточным, читал спеку давно).&lt;/p&gt;

&lt;p&gt;Счетчики в хвосте важны для супер-точных систем, например трейдинга. Лично я не
сталкивался с ситуацией, когда порядка в рамках тысячной секунды недостаточно.&lt;/p&gt;

&lt;p&gt;Как я уже рассказывал, UUID v7 отлично подходит на роль первичного
ключа. Во-первых, если у вас числовые айдишки, вы становитесь заложником
базы. Только она назначает айдишки сущностям. Это становится проблемой в
распределенных системах, очередях задач. Пока сущность не вставилась в базу,
можно наначить ей временный айди, но это лишнее усложнение.&lt;/p&gt;

&lt;p&gt;Наоборот, в случае UUID можно присвоить сущности ключ до вставки в базу,
например:&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;# python
user_id = generate_uuid()
profile_id = generate_uuid()

# sql
insert into users (id) values (user_id)

insert into profiles (id, user_id)
values (profile_id, user_id)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Оба запроса можно выполнить, не обращаясь к полю &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;generated_id&lt;/code&gt; результата.&lt;/p&gt;

&lt;p&gt;Говорят: UUID длиней числовой айдишки, это раздувание таблицы. Вспомним, что
UUIDv7 = id + created_at. Так или иначе нужно хранить время создания
сущности. Типы timestamp(tz) занимают 8 байт, тип biginteger/bigserial – тоже 8
байт. В сумме 16, а это столько же, сколько занимает UUID. Все сходится.&lt;/p&gt;

&lt;p&gt;Главное: UUIDv4 (полностью случайный) не очень подходит для индексов btree. Если
добавлять в него значения возрастанию, они будут наполнять бакеты
последовательно. Примерно как фигурки в Тетрисе, когда профессиональные игроки
укладывают их слева направо. Если данные случайны, фигурки падают в разные
бакеты. Из-за этого выше фрагментация, а кроме того, часто срабатывает
перераспределение узлов.&lt;/p&gt;

&lt;p&gt;Похожая беда с обходом индекса. Когда мы обходим btree-индекс с числами, то
порядок блоков почти совпадает с порядком ключей, например:&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;id   block
1 -&amp;gt; 1001
2 -&amp;gt; 1001
3 -&amp;gt; 1002
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Если у нас уиды, маппинг будет такой:&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;id                           block
b7b0547b-...-148889ec0602 -&amp;gt; 51451
d1897a03-...-520566c5c9a7 -&amp;gt; 13
ef0093d3-...-26e923a66fe4 -&amp;gt; 7232
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Будет много случайного доступа к файлу, и обход такого индекса медленней, чем с
числовыми айдишками.&lt;/p&gt;

&lt;p&gt;К счастью, седьмой уид сводит проблему на нет, так что пользуйтесь!&lt;/p&gt;
</description>
        <pubDate>Sat, 31 Jan 2026 00:00:00 +0000</pubDate>
        <link>https://grishaev.me/tip-017-pg/</link>
        <guid isPermaLink="true">https://grishaev.me/tip-017-pg/</guid>
        
        <category>postgres</category>
        
        <category>sql</category>
        
        <category>uuid</category>
        
        
      </item>
    
      <item>
        <title>Совет дня №16</title>
        <description>&lt;p&gt;Разберем ситуацию, когда индекс есть, но не используется.&lt;/p&gt;

&lt;p&gt;Предположим, в системе есть пользователи, и каждый указывает город:&lt;/p&gt;

&lt;div class=&quot;language-sql highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;create&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;table&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;users&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;id&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;uuid&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;primary&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;name&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;text&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;city&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;text&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;В реальности поле &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;city&lt;/code&gt; ссылается на таблицу городов, но сейчас это не важно –
оставим текст. Разработчик создал индекс на город:&lt;/p&gt;

&lt;div class=&quot;language-sql highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;create&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;index&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;idx_user_city&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;using&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;btree&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;on&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;users&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;city&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Выполняет запрос, чтобы найти пользователей из Москвы:&lt;/p&gt;

&lt;div class=&quot;language-sql highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;select&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;users&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;where&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;city&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'Moscow'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Смотрит план, а там full scan. Почему?&lt;/p&gt;

&lt;p&gt;Самое важное: Postgres никогда не использует индекс просто потому, что он
есть. Индекс берется в работу, только если а) по нему собрана статистика и б)
она показывает, что индекс дешевле full scan.&lt;/p&gt;

&lt;p&gt;Разберем оба условия. Статистика по каждой таблице, ее колонке и индексу
хранится в каталоге &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pg_statistics&lt;/code&gt;. Это очень низкоуровневые данные. В них
записаны наиболее частые значения колонок, их гистограмма, приблизительные
диапазоны значений. Поверх &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pg_statistics&lt;/code&gt; создана вьюха &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pg_stats&lt;/code&gt;, которая
удобнее в работе и проверяет права на таблицы.&lt;/p&gt;

&lt;p&gt;Статистику собирает специальный процесс по расписанию. Если вы только что
создали индекс, статистики для него нет. Обновите ее командой:&lt;/p&gt;

&lt;div class=&quot;language-sql highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;analyze&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;users&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;В идеале &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;analyze&lt;/code&gt; нужно добавлять в миграции, которые создают или меняют
индексы, чтобы сразу после их применения индекс подхватился.&lt;/p&gt;

&lt;p&gt;Итак, статистика есть, но индекс “мигает” – то используется, то нет. Например,
ищем пользователей из Читы, попадаем в индекс:&lt;/p&gt;

&lt;div class=&quot;language-sql highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;explain&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;select&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;users&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;where&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;city&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'Chita'&lt;/span&gt;
&lt;span class=&quot;cm&quot;&gt;/* index scan on idx_user_city ... */&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;А если из Москвы, будет full scan:&lt;/p&gt;

&lt;div class=&quot;language-sql highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;explain&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;select&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;users&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;where&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;city&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'Moscow'&lt;/span&gt;
&lt;span class=&quot;cm&quot;&gt;/* full seq scan on users ... */&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;В чем дело? Ответ – селективность, она же избирательность. Так называют долю
найденных записей относительно их общего числа. С селективностью есть путаница:
разные учебники по-разному трактуют этот показатель. Одни говорят: если процент
малый (1-5%), то селективность высокая, а если большой (30 и выше) – то
низкая. Другие наоборот: малый процент – низкая селективность, высокий —
большая. Я предпочитаю первый (обратный) вариант: чем меньше записей охватывает
условие, тем точнее (выше) селективность.&lt;/p&gt;

&lt;p&gt;Если проверить, сколько в базе пользователей из какого города, то окажется
следующее. Из Читы – три человека (высокая селективность), а из Москвы – триста
тысяч (крайне низкая). Москва окажется в каталоге &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pg_statistics&lt;/code&gt; в колонке
“частые значения” – сигнал к тому, чтобы не брать индекс в работу. При анализе
запроса Postgres это проверит и возьмет full scan. Москвичи пролетают!&lt;/p&gt;

&lt;p&gt;Какова должна быть селективность значения? Реальность такова, что Postgres берет
в работу индекс, если селективность условия не превышает 5-10 процентов. Это
довольно мало! Именно поэтому нет смысла создавать индексы на логические флаги
(true/false), статусы (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;active&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pending&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;done&lt;/code&gt;) и другие значения с малым
разбросом. Их селективность слишком низкая (высокая доля выборки).&lt;/p&gt;

&lt;p&gt;Для таких значений используйте частичные индексы &lt;a href=&quot;/tip-005-pg/&quot;&gt;(см. прошлый
совет)&lt;/a&gt;. Например, только активные заказы, только москвичи и так далее. И
потом ищите уже внутри этого подмножества.&lt;/p&gt;

&lt;p&gt;Почему Postgres предпочитает full scan для условия, которое покрывает 30%
таблицы? Ведь прочитать треть таблицы быстрее, чем всю? Дело в том, что обход
индекса возвращает не сами строки, а номера блоков, где они находятся. Блоки
идут не один за другим, а разбросаны по всему файлу. Даже если отсортировать
номера по возрастанию, между ними могут быть большие промежутки. В результате
будет много дисковых операций. На крутящихся жестких дисках это приводило к
частому переносу головки.&lt;/p&gt;

&lt;p&gt;Напротив, чтение всех блоков подряд относительно дешево. При таком методе
Postgres читает сразу много блоков, а не по одному.&lt;/p&gt;

&lt;p&gt;Итого: выполняйте &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;analyze&lt;/code&gt;, проверяйте селективность условия.&lt;/p&gt;
</description>
        <pubDate>Sat, 31 Jan 2026 00:00:00 +0000</pubDate>
        <link>https://grishaev.me/tip-016-pg/</link>
        <guid isPermaLink="true">https://grishaev.me/tip-016-pg/</guid>
        
        <category>postgres</category>
        
        <category>sql</category>
        
        
      </item>
    
      <item>
        <title>Глава 1. Введение в документы</title>
        <description>
&lt;h2&gt;

    Содержание

&lt;/h2&gt;

&lt;ul id=&quot;toc-item-pg-book-json-ch01&quot;&gt;
  &lt;li&gt;&lt;a href=&quot;#табличное-мышление&quot; id=&quot;toc-item-pg-book-json-ch01-табличное-мышление&quot;&gt;Табличное мышление&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#подход-nosql&quot; id=&quot;toc-item-pg-book-json-ch01-подход-nosql&quot;&gt;Подход NoSQL&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#знакомство-с-документами&quot; id=&quot;toc-item-pg-book-json-ch01-знакомство-с-документами&quot;&gt;Знакомство с документами&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#доводы-в-пользу-документов&quot; id=&quot;toc-item-pg-book-json-ch01-доводы-в-пользу-документов&quot;&gt;Доводы в пользу документов&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#задачи-которые-решают-документы&quot; id=&quot;toc-item-pg-book-json-ch01-задачи-которые-решают-документы&quot;&gt;Задачи, которые решают документы&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#слабые-стороны-документов&quot; id=&quot;toc-item-pg-book-json-ch01-слабые-стороны-документов&quot;&gt;Слабые стороны документов&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#замечание-о-переоценке&quot; id=&quot;toc-item-pg-book-json-ch01-замечание-о-переоценке&quot;&gt;Замечание о переоценке&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#почему-postgres&quot; id=&quot;toc-item-pg-book-json-ch01-почему-postgres&quot;&gt;Почему Postgres&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#тайное-преимущество-sql&quot; id=&quot;toc-item-pg-book-json-ch01-тайное-преимущество-sql&quot;&gt;Тайное преимущество SQL&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;В первой главе мы рассмотрим реляционные и документо-ориентированные базы
данных. Мы обсудим их сильные и слабые стороны, сценарии, когда одному виду
предпочитают другой и наоборот. Здесь же мы коснемся понятия документа:
структуры данных, которая ввиду сложности плохо ложится на реляционные
таблицы. Мы узнаем, для каких задач выбирают документы и чем может быть полезен
Postgres.&lt;/em&gt;&lt;/p&gt;

&lt;h2 id=&quot;табличное-мышление&quot;&gt;Табличное мышление&lt;/h2&gt;

&lt;p&gt;Если вы бэкенд-разработчик, то скорее всего работали с реляционными базами
данных. Наиболее известные их представители — это MySQL, ее форк MariaDB,
PostgreSQL, Microsoft SQL Server, Oracle DB. Реляционные базы называются так
из-за лежащей в их основе &lt;a href=&quot;https://ru.wikipedia.org/wiki/%D0%A0%D0%B5%D0%BB%D1%8F%D1%86%D0%B8%D0%BE%D0%BD%D0%BD%D0%B0%D1%8F_%D0%B0%D0%BB%D0%B3%D0%B5%D0%B1%D1%80%D0%B0&quot;&gt;реляционной алгебры&lt;/a&gt; (она же алгебра
отношений). Это математический аппарат, который строится на отношениях
(множествах кортежей) и операций над ними (проекция, объединение, пересечение и
другие).&lt;/p&gt;

&lt;p&gt;Чтобы работать с базой на прикладном уровне, реляционная алгебра не
требуется. Гораздо важнее следствия математической модели:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;любой массив данных является таблицей;&lt;/li&gt;
  &lt;li&gt;любая операция над таблицей порождает таблицу.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;В каждой базе найдется техника, которая не подходит под эти правила. Однако
общую картину это не меняет: в основе реляционных баз лежат таблицы и операции
над ними.&lt;/p&gt;

&lt;!-- more --&gt;

&lt;p&gt;Предположим, имеется таблица &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;users&lt;/code&gt; с тремя полями: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;id&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;name&lt;/code&gt; и &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;age&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;language-sql highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;create&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;table&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;users&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;id&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;integer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;name&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;text&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;age&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;integer&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Добавим трех пользователей:&lt;/p&gt;

&lt;div class=&quot;language-sql highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;insert&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;into&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;users&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;values&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'Ivan'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;14&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'John'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;34&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;3&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'Juan'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;51&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Выборка всех полей без условий вернет таблицу (отношение):&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;select id, name, age from users;

┌────┬──────┬─────┐
│ id │ name │ age │
├────┼──────┼─────┤
│  1 │ Ivan │  14 │
│  2 │ John │  34 │
│  3 │ Juan │  51 │
└────┴──────┴─────┘
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Если выбрать только два столбца, получим таблицу, усеченную по горизонтали. В
реляционной алгебре это называется проекцией:&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;select id, age from users;

┌────┬─────┐
│ id │ age │
├────┼─────┤
│  1 │  14 │
│  2 │  34 │
│  3 │  51 │
└────┴─────┘
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Оператор &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;WHERE&lt;/code&gt; накладывает условие на строки. С ним мы отбросим часть из них и
получим таблицу, уменьшенную по вертикали. В терминах реляционной алгебры мы
фильтруем отношение. Совместим проекцию и фильтр в одном запросе:&lt;/p&gt;

&lt;div class=&quot;language-sql highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;select&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;name&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;users&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;where&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;age&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;between&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;18&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;and&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;49&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Операторы SELECT и WHERE срезали данные с разных сторон, но итоговый результат —
таблица:&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;┌────┬──────┐
│ id │ name │
├────┼──────┤
│  2 │ John │
└────┴──────┘
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Оператор JOIN (соединение), напротив, расширяет таблицу по горизонтали. Ниже мы
соединяем пользователей с их профилями по ссылочному полю.&lt;/p&gt;

&lt;div class=&quot;language-sql highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;create&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;table&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;profiles&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;user_id&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;integer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;job&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;text&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;is_open&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;boolean&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;insert&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;into&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;profiles&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;values&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'teacher'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'programmer'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;3&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'tester'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;select&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;users&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;u&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;join&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;profiles&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;p&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;on&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;u&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;id&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;p&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;user_id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Итоговая таблица содержит поля обеих таблиц:&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;┌────┬──────┬─────┬─────────┬────────────┬─────────┐
│ id │ name │ age │ user_id │    job     │ is_open │
├────┼──────┼─────┼─────────┼────────────┼─────────┤
│  1 │ Ivan │  14 │       1 │ teacher    │ t       │
│  2 │ John │  34 │       2 │ programmer │ f       │
│  3 │ Juan │  51 │       3 │ tester     │ t       │
└────┴──────┴─────┴─────────┴────────────┴─────────┘
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Даже такой примитивный запрос как &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;select 1 as x&lt;/code&gt; возвращает таблицу. В ней одна
колонка с именем &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;x&lt;/code&gt; и одна запись с кортежем &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;(1, )&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;select 1 as x;

┌───┐
│ x │
├───┤
│ 1 │
└───┘
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Возможно, кому-то этот подход напомнил Matlab, где любое значение считается
матрицей, пусть даже размера 1x1.&lt;/p&gt;

&lt;p&gt;Таблицы и операции над ними образует то, что автор называет табличным
мышлением. Любые данные можно представить таблицей или их набором, а затем
применить проекцию, фильтр или объединение. Операции декларативны: нет
промежуточных переменных, циклов и ветвлений. База данных выводит технические
шаги из описания.&lt;/p&gt;

&lt;p&gt;В прикладных языках вроде Python или Java мы работаем не с таблицами, а
коллекциями. Чаще всего это списки и словари, в случае строгой типизации –
структуры, записи. Следующий тезис прозвучит странно, но все-таки: в обработке
данных коллекции уступают таблицам. Если выразить проекцию и фильтр на
прикладном языке, код окажется многословным, а иной раз – запутанным.&lt;/p&gt;

&lt;p&gt;Причина в том, что Python, Java и аналоги – языки общего назначения. Их задача –
быть удобными во всем, не вдаваясь слишком глубоко в какую-то область. При
работе с коллекциями они удобны, но уступают специальному решению – SQL.&lt;/p&gt;

&lt;p&gt;Предположим, имеется список пользователей. Каждый элемент – словарь с полями id,
name и age. Нужно выбрать пользователей старше 18 лет, оставив при этом код и
имя. Выразим это на разных языках, например императивном Python. Нам понадобится
функция subset, которая вернет подмножество словаря:&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;n&quot;&gt;dеf&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;subset&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;d&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;keys&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;k&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;d&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;k&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;k&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;keys&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;users&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;id&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;name&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;Ivan&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;age&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;14&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;id&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;name&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;John&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;age&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;34&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;id&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;3&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;name&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;Juan&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;age&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;51&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;new_users&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;subset&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;user&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;id&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;name&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;user&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;users&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;user&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;age&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;18&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;

&lt;span class=&quot;p&quot;&gt;[{&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;'id'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;'name'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;'John'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;
 &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;'id'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;3&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;'name'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;'Juan'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;То же самое на Clоjure, функциональном диалекте Лиспа:&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;(dеf users
  [{:id 1 :name &quot;Ivan&quot; :age 14}
   {:id 2 :name &quot;John&quot; :age 34}
   {:id 3 :name &quot;Juan&quot; :age 51}])

(-&amp;gt;&amp;gt; users
     (filter (fn [user]
               (-&amp;gt; user :age (&amp;gt; 18))))
     (map (fn [user]
            (select-keys user [:id :name]))))

({:id 2 :name &quot;John&quot;}
 {:id 3 :name &quot;Juan&quot;})
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;В каждом языке это можно проделать разными способами, но мы выбрали самые
очевидные: list comprehension в Python и map/filter Clоjure. Теперь запишем то
же самое на SQL:&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;select id, name from users where age &amp;gt; 18;

┌────┬──────┐
│ id │ name │
├────┼──────┤
│  2 │ John │
│  3 │ Juan │
└────┴──────┘
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Решение на SQL короче, однако мы усложним пример. Предположим, к пользователям
прилагаются профили. Вдобавок к отбору по возрасту нужно оставить тех
пользователей, в профиле которых указано “открыт к предложениям” (логическое
поле &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;is_open&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;Вот какие трудности это влечет. Во-первых, чтобы найти профиль по номеру
пользователя, нужно составить словарь &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;user_id =&amp;gt; profile&lt;/code&gt;. Во-вторых, на каждом
шаге проверять, есть ли профиль, и если нет, отбрасывать пользователя, даже если
ему больше 18 лет. В-третьих, при наличии профиля проверять флаг is_open. Код на
Python:&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;n&quot;&gt;profiles&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;user_id&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;job&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;teacher&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;is_open&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;True&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;user_id&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;job&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;programmer&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;is_open&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;False&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;user_id&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;3&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;job&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;tester&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;is_open&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;True&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;dеf&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;index_by&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;field&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;rows&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;row&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;field&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;row&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;row&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;rows&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;profile_index&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;index_by&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;user_id&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;profiles&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;result&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[]&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;user&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;users&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;user_id&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;user&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;id&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;profile&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;profile_index&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;user_id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;user&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;age&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;18&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;and&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;profile&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;is_open&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]:&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;row&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;**&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;user&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;**&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;profile&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;result&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;append&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;row&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;p&quot;&gt;[{&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;'id'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;3&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;s&quot;&gt;'name'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;'Juan'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;s&quot;&gt;'age'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;51&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;s&quot;&gt;'user_id'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;3&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;s&quot;&gt;'job'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;'tester'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;s&quot;&gt;'is_open'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;True&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Версия на Clоjure:&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;(dеf profiles
  [{:user-id 1 :job &quot;teacher&quot; :is-open true}
   {:user-id 2 :job &quot;programmer&quot; :is-open false}
   {:user-id 3 :job &quot;tester&quot; :is-open true}])

(dеfn index-by [kw rows]
  (-&amp;gt;&amp;gt; rows
       (map (juxt kw identity))
       (into {})))

(dеf profile-index
  (index-by :user-id profiles))

(-&amp;gt;&amp;gt; users
     (filter (fn [user]
               (-&amp;gt; user :age (&amp;gt; 18))))
     (map (fn [user]
            (let [profile (get profile-index (:id user))]
              (merge user profile))))
     (filter (fn [row]
               (-&amp;gt; row :is-open))))

({:id 3,
  :name &quot;Juan&quot;,
  :age 51,
  :user-id 3,
  :job &quot;tester&quot;,
  :is-open true})
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Решение на SQL:&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;select * from users u
join profiles p on u.id = p.user_id
where u.age &amp;gt; 18 and p.is_open;

┌────┬──────┬─────┬─────────┬────────┬─────────┐
│ id │ name │ age │ user_id │ job    │ is_open │
├────┼──────┼─────┼─────────┼────────┼─────────┤
│  3 │ Juan │  51 │       3 │ tester │ t       │
└────┴──────┴─────┴─────────┴────────┴─────────┘
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Третий вариант — снова самый короткий, но преимущество не только в этом. SQL
декларативен: он не указывает порядок обхода таблиц, не требует циклов и
проверок if/else. Для его исполнения не нужен интерпретатор, установленный
локально. Код на SQL выполнит сервер, а мы получим результат.&lt;/p&gt;

&lt;p&gt;Автор видел примеры того, как целые экраны кода можно было заменить небольшим
запросом. Единственное ограничение в том, что данные не всегда находятся в базе:
в эпоху микросервисов их все чаще извлекают по сети. Сущности получают из разных
источников и соединяют в приложении: строят индексы id =&amp;gt; entity, обходят их и
отбрасывают часть данных. Эти действия легко заменить шагами:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;загрузить данные в реляционные таблицы;&lt;/li&gt;
  &lt;li&gt;выполнить запросы с нужными WHERE, JOIN, GROUP BY и так далее.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Недостаток в том, что данные нужно загрузить в базу – а ведь мы уже потратили
время на обращение к сервисам. Это увеличивает трафик и замедляет
обработку. Однако сегодня для каждого языка найдется in-memory SQL engine –
движок SQL, который работает в одном процессе с приложением. Прежде всего это
SQLite и DuckDB: они написаны на C/C++ и вызываются через внешний интерфейс
(foreign function call). В Java доступны библиотеки H2 и HyperSQL, написанные на
ней самой. Поэтому работа с данными сводится к шагам:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;собрать ингредиенты из сетевых сервисов;&lt;/li&gt;
  &lt;li&gt;перенести их в SQL-движок, запущенный в памяти;&lt;/li&gt;
  &lt;li&gt;выполнить запросы и прочитать результат.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Часто разработчики выступают против SQL и заменяют абстракцией: ORM (Object
Relational Mapping) или потоками (LinQ в C#, Streams в Java). Однако практика
показывает: на долгой дистанции SQL проще и удобней. Реляционная модель
позволяет строить любые проекции данных и при этом:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;
    &lt;p&gt;исходные данные не изменяются: выборка двух столбцов не удалит исходные
столбцы. Это касается императивных языков, где коллекции изменяемы, и удаление
элемента скажется на всем коде.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;Не нужно писать императивный код с обходом коллекций. SQL выражает намерение,
а не реализацию задумки.&lt;/p&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Не нужна машина, на которой установлены Python, Java и другие языки. Если данные
хранятся в базе, для работы с ними хватит минимального клиента вроде psql.&lt;/p&gt;

&lt;h2 id=&quot;подход-nosql&quot;&gt;Подход NoSQL&lt;/h2&gt;

&lt;p&gt;На рубеже нулевых и десятых годов возникли другие подходы к хранению данных. Их
ключевая черта – отказ от таблиц и языка SQL. Совокупно эти решения назвали
&lt;a href=&quot;https://ru.wikipedia.org/wiki/NoSQL&quot;&gt;NoSQL&lt;/a&gt;, причем No означает не английское “нет”, а not only — не только
SQL. Некоторые NoSQL-базы все-таки предлагают похожий на SQL язык и концепцию
таблиц (например, Cassandra).&lt;/p&gt;

&lt;p&gt;Решения NoSQL крайне разнообразны. Прежде всего это простые хранилища ключей и
значений. Свои скромные возможности они компенсируют скоростью доступа и
репликацией по многим узлам.&lt;/p&gt;

&lt;p&gt;Некоторые базы рассчитаны на хранение документов. От хранилищ “ключ-значение”
они отличаются тем, что значение, во-первых, объемно (занимает килобайты и
мегабайты), а во-вторых – структурировано. Документ хранится не строкой, а в
виде дерева или набора “путь-значение”. Рассмотрим следующий документ:&lt;/p&gt;

&lt;div class=&quot;language-json highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;id&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;123456&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;title&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;Some Document&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;attrs&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;code&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;662234&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;created_at&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;2025-01-22&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;users&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;id&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;email&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;user1@test.com&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;id&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;email&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;user2@test.com&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;При вставке он раскладывается на пары “путь-&amp;gt;значение”:&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;id                  -&amp;gt; &quot;123456&quot;
title               -&amp;gt; &quot;Some Document&quot;
attrs.code          -&amp;gt; 662234
attrs.created_at    -&amp;gt; 2025-01-22
attrs.users.0.id    -&amp;gt; 1
attrs.users.0.email -&amp;gt; user1@test.com
attrs.users.1.id    -&amp;gt; 2
attrs.users.1.email -&amp;gt; user2@test.com
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Чтобы найти документы по атрибутам, для некоторых путей строят индекс. В
запросах используется либо доменный язык (DSL), либо выражение пути, аналог
XPath для XML. Все это мы рассмотрим в последующих главах.&lt;/p&gt;

&lt;p&gt;База данных Datomic и аналоги (например, XTDB) хранят документы в виде кортежей
EAV – entity, attribute, value, что означает “сущность”, “атрибут”,
“значение”. В реляционной модели мы бы выразили EAV таблицей:&lt;/p&gt;

&lt;div class=&quot;language-sql highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;create&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;table&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;eav&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;e&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;integer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;a&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;text&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;v&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;text&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Достоинство модели EAV в том, ей можно описать что угодно: пользователей,
профили, товары, заказчиков и так далее. Все это поместиться в одной таблице!
Подобно двоичной системе счисления, EAV — своего рода рубеж: далее упростить
модель невозможно.&lt;/p&gt;

&lt;p&gt;Именно в минимализме EAV проявляется красота этой тройки. Ниже мы заполняем
таблицу данными: это пользователи и профили из задачи выше. Каждая группа
кортежей относятся к одной сущности:&lt;/p&gt;

&lt;div class=&quot;language-sql highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;insert&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;into&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;eav&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;values&lt;/span&gt;

    &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;10001&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'user/name'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'Ivan'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;10001&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'user/age'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'14'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;

    &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;10002&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'user/name'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'John'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;10002&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'user/age'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'34'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;

    &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;10003&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'user/name'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'Juan'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;10003&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'user/age'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'51'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;

    &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;10004&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'profile/user-ref'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'10001'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;10004&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'profile/job'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'teacher'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;10004&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'profile/is-open'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'true'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;

    &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;10005&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'profile/user-ref'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'10002'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;10005&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'profile/job'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'programmer'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;10005&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'profile/is-open'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'false'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;

    &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;10006&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'profile/user-ref'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'10003'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;10006&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'profile/job'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'tester'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;10006&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'profile/is-open'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'true'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Пользователи и профили хранятся в одной таблице и не конфликтуют друг с
другом. Однако для SQL подобный минимализм неудобен: он усложняет запросы. Чтобы
выбрать пользователей старше 18 лет, открытых к предложениям, понадобится каскад
подзапросов и соединений. Маловероятно, что запрос ниже понятен без погружения в
контекст:&lt;/p&gt;

&lt;div class=&quot;language-sql highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;select&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;eav&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;where&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;e&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;select&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;v&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;integer&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;eav&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;where&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;a&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'profile/user-ref'&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;and&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;e&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
            &lt;span class=&quot;k&quot;&gt;select&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;e&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;eav&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;where&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;a&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'profile/is-open'&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;and&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;v&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'true'&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;intersect&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;select&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;e&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;eav&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;where&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;a&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'user/age'&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;and&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;v&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;integer&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;18&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Напротив, в реляционной модели он удивительно прост:&lt;/p&gt;

&lt;div class=&quot;language-sql highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;select&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;users&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;u&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;join&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;profiles&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;p&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;on&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;u&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;id&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;p&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;user_id&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;where&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;u&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;age&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;18&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;and&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;p&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;is_open&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Для работы с EAV служит особый язык запросов, основанный на Datalog. В свою
очередь &lt;a href=&quot;https://ru.wikipedia.org/wiki/Datalog&quot;&gt;Datalog&lt;/a&gt; – подмножество языка Prolog, созданного для
логических задач. Язык опирается на сопоставления и правила. Приведем код на
Clоjure, где мы работаем с Datomic. Для начала запишем пользователей с
профилями:&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;(dеf data
  [{:db/id &quot;user1&quot; :user/name &quot;Ivan&quot; :user/age 14}
   {:db/id &quot;user2&quot; :user/name &quot;John&quot; :user/age 34}
   {:db/id &quot;user3&quot; :user/name &quot;Juan&quot; :user/age 51}
   {:profile/user-ref &quot;user1&quot; :profile/job &quot;teacher&quot;    :profile/is-open true}
   {:profile/user-ref &quot;user2&quot; :profile/job &quot;programmer&quot; :profile/is-open false}
   {:profile/user-ref &quot;user3&quot; :profile/job &quot;tester&quot;     :profile/is-open true}])

(d/transact conn {:tx-data data})
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Выберем пользователей старше 18 лет, открытых к предложениям:&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;(dеf query
  '[:find (pull ?u [*])
	:in $
	:where
	[?u :user/age ?age]
	[(&amp;gt; ?age 18)]
	[?p :profile/user-ref ?u]
	[?p :profile/is-open true]])

(d/q query (d/db conn))

[[{:db/id 96757023244368, :user/name &quot;Juan&quot;, :user/age 51}]]
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Как и в случае с SQL, запрос на Datalog занял меньше строк, чем код на
Clоjure. Специализированное решение – всегда более емкое.&lt;/p&gt;

&lt;p&gt;Переход с SQL на EAV требует серьезной перестройки мышления. И хотя Datomic не
стал популярен как реляционные базы, он вызывал своего рода ренессанс
Datalog. Появились его различные клоны: XTDB, Datascript, Datalevin и другие,
каждый со своими особенностями.&lt;/p&gt;

&lt;p&gt;Некоторые решения NoSQL абстрагируются от хранения данных. Скажем, Datomic
использует Postgres или MySQL для чтения и записи EAV (наряду с этой тройкой он
хранит два других атрибута, которые нас не интересуют). Упрощая, можно сказать,
что Datomic — это огромное Btree-дерево, части которого хранятся в реляционной
таблице. Также Datomic использует кластер Memcached, а его облачная версия —
хранилище S3. Похоже устроена база XTDB: данные могут хранится в реляционных
базах, файлах S3 и даже кластере Kafka.&lt;/p&gt;

&lt;p&gt;Из всего разнообразия NoSQL нас интересуют базы, ориентированные на
документы. Перечислим те, которые пользователь может установить на свой сервер
(т.н. self-hosted решения):&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;a href=&quot;https://www.mongodb.com/&quot;&gt;MongoDB&lt;/a&gt;: производительная база данных, написанная на C++. Известна
своей масштабируемостью и репликацией. Очень популярна в мире Node.js.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;a href=&quot;https://couchdb.apache.org/&quot;&gt;CouchDB&lt;/a&gt;: хранилище документов, написанное на Erlang. CouchDB
интересен концепцией представлений (view). Последние устроены как комбинация
шагов Map и Reduce — первичная выборка и ее свертка. Разработка CouchDB
опирается на публикации Google о технологиях BigTable и MapReduce.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;a href=&quot;https://cassandra.apache.org/_/index.html&quot;&gt;Cassandra&lt;/a&gt;: распределенная колоночная база на языке
Java. Изначально нацелена на работу в кластере. Предлагает таблицы и
SQL-подобный язык, хотя и более ограниченный, чем в реляционных аналогах.&lt;/p&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;К облачным решениям относятся:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;a href=&quot;https://aws.amazon.com/dynamodb/&quot;&gt;DynamoDB&lt;/a&gt;: один из главных сервисов AWS, хранилище документов по
ключу. Отличается высокой скоростью и масштабированием. Работает по протоколу
REST JSON, что позволяет общаться с ней через обычный HTTP-клиент.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;a href=&quot;https://opensearch.org/&quot;&gt;OpenSearch&lt;/a&gt;: еще одно решение AWS для индексации
документов. Создан на базе ElasticSearch после того, как последний сменил
лицензию. Формально OpenSearch не является облачным сервисом, но сложность его
развертки столь высока, что в основном им пользуются как услугой облачных
провайдеров.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;a href=&quot;https://ydb.tech/ru/&quot;&gt;YandexDB (YDB)&lt;/a&gt;: база данных, созданная в Яндексе. Изначально
использовалась для внутренних нужд, в настоящее время доступна как платный
сервис. Работает по протоколу gRPC, а также поддерживает API
DynamoDB. Последний фактор облегчает переезд из AWS в Яндекс.&lt;/p&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Это неполный список, однако мы не ставим цель перечислить все решения для
хранения документов. Важнее другое: что представляют собой документы? Где они
встречаются и почему их выбирают вместо реляционных таблиц?&lt;/p&gt;

&lt;h2 id=&quot;знакомство-с-документами&quot;&gt;Знакомство с документами&lt;/h2&gt;

&lt;p&gt;Документ — это набор фактов о сущности. Договор с поставщиком, заявка на кредит,
история болезни – все это документы. Приведем пример в формате JSON:&lt;/p&gt;

&lt;div class=&quot;language-json highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;id&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;123456&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;title&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;Some Document&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;attrs&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;code&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;662234&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;created_at&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;2025-01-22&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;users&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;id&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;email&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;user1@test.com&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;id&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;email&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;user2@test.com&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;У документа есть уникальный ключ, чаще всего — UUID (уникальный
идентификатор). Для генерации UUID служат разные алгоритмы; позже мы обсудим,
какой из них предпочесть. Важно, что документы плохо совместимы с нарастающим
счетчиком, принятым в реляционных базах. Это связано с тем, что номер документа
должен быть глобальным в рамках всей базы, а не только в разрезе сущности.&lt;/p&gt;

&lt;p&gt;Предположим, мы знаем номер документа — 123. Этого недостаточно, чтобы его
извлечь. Нужно знать сущность: users, orders и так далее. Ключ документа
становится парой (users, 123), и эту пару нужно где-то хранить. По своей природе
пара сложнее, чем один элемент.&lt;/p&gt;

&lt;p&gt;Другой довод в пользу UUID в том, что его производят разные участники. Когда вы
создаете документ, UUID может произвести Postgres, приложение (Java, Python и
так далее), очередь задач или другой узел инфраструктуры. Нужно лишь, чтобы
везде использовался одинаковый алгоритм, например UUID4 или
&lt;a href=&quot;https://uuid7.com/&quot;&gt;UUID7&lt;/a&gt;. Особенности последнего мы обсудим чуть позже.&lt;/p&gt;

&lt;p&gt;Числовые ключи, напротив, лишены описанного преимущества. За их производство
отвечает строго один узел – база данных. Другие участники не могут назначить
документу номер, потому что нет гарантии, что он не появился в промежутке между
проверкой и генерацией. Как мы упоминали, документные базы работают на
нескольких узлах в кластере. В таких условиях сложно производить уникальные
возрастающие числа.&lt;/p&gt;

&lt;p&gt;Мы уделили ключам столько внимания, чтобы читатель запомнил: при работе с
документами используйте уникальные идентификаторы. У сущности может быть
числовой номер, но чаще всего он служит для второстепенных целей. Например:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;
    &lt;p&gt;историческая: документы перенесли из реляционной базы данных, где у них были
числовые ключи – “айдишники”. В системе остался API, который до сих пор
обращается к данным по этому полю, например &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;GET /users/123&lt;/code&gt;. В этом случае мы
поддерживаем совместимость с API.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;прикладная: в финансах и бухгалтерии приняты свои системы нумерации
документов. Менеджеру удобно, когда у заявок на кредит числовые номера,
уникальные в рамках года. Мы согласны поддерживать такой атрибут обеспечить
его уникальность; однако это именно атрибут, а не первичный ключ.&lt;/p&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Кроме ключа, у документа есть метаданные: даты создания и последнего изменения,
версия, автор, принадлежность к какому-либо домену, набор тегов.&lt;/p&gt;

&lt;p&gt;Тело документа содержит атрибуты – пары “путь -&amp;gt; значение”. Значения могут быть
вложенными, например словарем внутри словаря или списком словарей, в каждом из
которых другой список.&lt;/p&gt;

&lt;p&gt;Приведем несколько документов. В первом метаданные и атрибуты собраны вместе, и
не совсем ясно, что есть что:&lt;/p&gt;

&lt;div class=&quot;language-json highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;id&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;c2a44fe0-6840-4916-b6dd-b401f34f9c2a&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;type&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;contract&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;number&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;523552&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;created_at&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;2025-01-01&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;created_by&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;id&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;099f2886-9772-4128-a5cd-b648369ce3f0&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;email&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;some.user@test.com&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;tags&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;risk&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;contract&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;acme&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;],&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;expires_at&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;2027-12-31&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;amount&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;100000000&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Вариант, где метаданные собраны в отдельный словарь, выглядит удобнее:&lt;/p&gt;

&lt;div class=&quot;language-json highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;id&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;c2a44fe0-6840-4916-b6dd-b401f34f9c2a&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;meta&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;type&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;contract&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;created_at&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;2025-01-01&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;created_by&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;id&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;099f2886-9772-4128-a5cd-b648369ce3f0&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;email&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;some.user@test.com&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;tags&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;risk&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;contract&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;acme&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;number&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;523552&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;expires_at&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;2027-12-31&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;amount&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;100000000&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Иногда атрибуты выносят в отдельный словарь. На вершине документа остаются поля
id, meta и attrs:&lt;/p&gt;

&lt;div class=&quot;language-json highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;id&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;c2a44fe0-6840-4916-b6dd-b401f34f9c2a&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;meta&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;type&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;contract&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;created_at&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;2025-01-01&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;created_by&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;id&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;099f2886-9772-4128-a5cd-b648369ce3f0&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;email&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;some.user@test.com&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;tags&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;risk&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;contract&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;acme&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;attrs&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;number&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;523552&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;expires_at&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;2027-12-31&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;amount&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;100000000&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Перейдем к другому вопросу: чем документ отличается от реляционных таблиц? Для
этого рассмотрим документ: договор со ссылкой на контрагента:&lt;/p&gt;

&lt;div class=&quot;language-json highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;id&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;c2a44fe0-6840-4916-b6dd-b401f34f9c2a&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;number&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;523552&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;created_at&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;2025-01-01&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;expires_at&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;2027-12-31&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;contractor&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;id&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;bd6a0da9-fe35-4a37-846c-dff3f32cf0ac&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;title&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;Acme Inc&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;amount&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;100000000&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;currency&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;EUR&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Разложим его на две таблицы: контрагенты и договоры:&lt;/p&gt;

&lt;div class=&quot;language-sql highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;create&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;table&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;contractors&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;id&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;uuid&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;primary&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;title&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;text&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;create&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;table&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;contract&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;id&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;uuid&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;primary&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;number&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;integer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;created_at&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;date&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;expires_at&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;date&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;contractor&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;uuid&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;references&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;contractors&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;amount&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;integer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;currency&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;text&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;insert&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;into&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;contractors&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;values&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;s1&quot;&gt;'bd6a0da9-fe35-4a37-846c-dff3f32cf0ac'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;s1&quot;&gt;'Acme Inc'&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;insert&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;into&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;contract&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;values&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;s1&quot;&gt;'c2a44fe0-6840-4916-b6dd-b401f34f9c2a'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;mi&quot;&gt;523552&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;s1&quot;&gt;'2025-01-01'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;s1&quot;&gt;'2027-12-31'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;s1&quot;&gt;'bd6a0da9-fe35-4a37-846c-dff3f32cf0ac'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;mi&quot;&gt;100000000&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;s1&quot;&gt;'EUR'&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Одним запросом мы получим обе сущности:&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;select * from contract c, contractors cs
where c.contractor = cs.id;

┌──────────────────────────────────────┬────────┬────────────┬────────────┬──────────────────────────────────────┬───────────┬──────────┬──────────────────────────────────────┬──────────┐
│                  id                  │ number │ created_at │ expires_at │              contractor              │  amount   │ currency │                  id                  │  title   │
├──────────────────────────────────────┼────────┼────────────┼────────────┼──────────────────────────────────────┼───────────┼──────────┼──────────────────────────────────────┼──────────┤
│ c2a44fe0-6840-4916-b6dd-b401f34f9c2a │ 523552 │ 2025-01-01 │ 2027-12-31 │ bd6a0da9-fe35-4a37-846c-dff3f32cf0ac │ 100000000 │ EUR      │ bd6a0da9-fe35-4a37-846c-dff3f32cf0ac │ Acme Inc │
└──────────────────────────────────────┴────────┴────────────┴────────────┴──────────────────────────────────────┴───────────┴──────────┴──────────────────────────────────────┴──────────┘
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Так зачем усложнять жизнь документами, если есть таблицы? Причин может быть
несколько, и мы опишем их в отдельном параграфе.&lt;/p&gt;

&lt;h2 id=&quot;доводы-в-пользу-документов&quot;&gt;Доводы в пользу документов&lt;/h2&gt;

&lt;p&gt;Преимущества документов очевидны, если выдвинуть следующие требования.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Неоднородность.&lt;/strong&gt; В реляционных базах каждая таблица имеет структуру, которой
подчиняются все записи. Добавление новой колонки, особенно к большим таблицам,
считается трудоемкой задачей, и этого стараются избегать. Документы, напротив,
обладают нечеткой структурой. Считается нормой, когда у двух документов почти
одинаковый состав полей, но разница все-таки есть.&lt;/p&gt;

&lt;p&gt;Покажем это на примере. Предположим, система ведет договоры между
участниками. Бухгалтерские реалии таковы, что в зависимости от типа участников
структура договора меняется. Договоры между двумя обществами, обществом и ИП, ИП
и самозанятым различаются кардинально. Часть полей будет общей, но будет и
немало других, особых для каждого случая.&lt;/p&gt;

&lt;p&gt;В приложении строят каскад схем. Сперва определяют базовую схему, которой
соответствует любой документ. В ней содержатся поля id, meta и attrs. Запишем ее
на псевдокоде:&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;Type = Enum(user, profile, contract, ...)

Meta = {
    type: Type,
    created_at: Date,
    created_by: UserRef,
    tags: List&amp;lt;String&amp;gt;
}

Attrs = Dict&amp;lt;String, Any&amp;gt;

Document = {
    id: UUID,
    meta: Meta,
    attrs: Attrs
}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;На ее основе определяют договор. Считаем, что условная функция merge рекурсивно
объединяет две схемы:&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;ContractBase = merge(Document, {
    attrs: {
        amount: Integer,
        currency: EnumCurrency,
        contractor: ContractorRef
    }
})
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;От общего договора наследуют конкретный случай: договор между организацией и
индивидуальным предпринимателем:&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;ContractOrgIp = merge(ContractBase, {
    attrs: {
        ip: IPRef,
        tenor: TenorSpec,
        departments: List&amp;lt;DepartmentRef&amp;gt;
    }
})
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Другой случай – договор между двумя организациями. Он включает структуру рисков
и третью сторону, которая берет их на себя:&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;ContractOrgOrg = merge(ContractBase, {
    attrs: {
        org: OrgRef,
        risks: List&amp;lt;RiskTable&amp;gt;,
        risk_taker: RiskTakerRef
    }
})
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;В зависимости от типа участников договор проверяют нужной схемой: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ContractOrgIp&lt;/code&gt;
или &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ContractOrgOrg&lt;/code&gt;. Каждую схему можно расширить: добавить сроки рисков,
комментарии, структуру расчетов.&lt;/p&gt;

&lt;p&gt;Следующая причина, когда документы удобны – &lt;strong&gt;вложенность&lt;/strong&gt;. Пока атрибуты
плоские (не превышают одного уровня вложенности), их можно хранить в таблице. Но
иногда появляются словари и списки словарей. Ниже заявка на кредит хранит
пользователей, которые над ней работали:&lt;/p&gt;

&lt;div class=&quot;language-json highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;id&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;6e394e79-2d71-47ae-a314-534fd9a10719&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;amount&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;10000&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;currency&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;USD&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;users&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;id&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;418d4488-507f-4ad4-86a3-f1c1e43f4b69&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;email&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;ivan@test.com&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;id&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;9f3d6c5a-02b3-45cf-b256-699fe91aab59&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;email&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;anna@test.com&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;id&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;66250983-1a11-421d-86b8-753384bf4845&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;email&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;elena@test.com&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Хранить вложенный массив в плоской таблице неудобно. Обычно поступают так:
таблица application хранит скалярные (отличные от коллекций) атрибуты. Коллекции
выносят в смежные таблицы и связывают их таблицами-мостами:&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;┌────────────────────────────────────────────────────────────────────────────────────────────┐
│┌──────────────────────────┐                        ┌──────────────────────────────────────┐│
││       applications       │                        │                users                 ││
│├────────┬────────┬────────┤                        ├────────┬───────────────┬─────────────┤│
││   id   │ amount │currency│                        │   id   │     email     │  full_name  ││
│├────────┼────────┼────────┤                        ├────────┼───────────────┼─────────────┤│
││...10719│   10000│     USD│                     ┌─▶│...3f4b6│  ivan@test.com│  Ivan Testov││
│└────────┴────────┴────────┘                     │  ├────────┼───────────────┼─────────────┤│
│     ▲                                           │ ▲│...aab59│  anna@test.com│ Anna Testova││
│     │                  ┌─────────────────────┐  │ │├────────┼───────────────┼─────────────┤│
│     │                  │  application_users  │  │ ││...f4845│ elena@test.com│Elena Testova││
│     │                  ├────────────┬────────┤  │ │└────────┴───────────────┴─────────────┘│
│     │                  │document_id │user_id │──┘ │     ▲                                  │
│     │                  ├────────────┼────────┤    │     │                                  │
│     ├──────────────────│    ...10719│...3f4b6│────┘     │                                  │
│     │                  ├────────────┼────────┤          │                                  │
│     ├──────────────────│    ...10719│...aab59│          │                                  │
│     │                  ├────────────┼────────┤          │                                  │
│     └──────────────────│    ...10719│...f4845│──────────┘                                  │
│                        └────────────┴────────┘                                             │
└────────────────────────────────────────────────────────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Проблема в том, что у пользователя тоже могут быть вложенные поля, например
список ролей или отделов, которым он принадлежит. Понадобится таблица
application_users_roles или application_users_departments. Структура базы
усложняется: чем больше вложенность документа, тем сложнее каскад таблиц. Если
на каком-то уровне возникнет ошибка, ее тяжело расследовать.&lt;/p&gt;

&lt;p&gt;Следующей причиной служит &lt;strong&gt;версионирование&lt;/strong&gt;. Схема документа меняется со
временем: сперва поле было числом, но выяснилось, что в нем могут быть символы
алфавита. В другом поле была ссылка на документ, но стало ясно, что вместе с
ссылкой нужно хранить тип сущности. Без типа трудно понять, в какой таблице
искать ссылку. Вот как выглядит документ до изменений:&lt;/p&gt;

&lt;div class=&quot;language-json highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;id&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;...&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;amount&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;10000&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;contractor&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;e093c3c8-54c8-4375-a964-4a5de09ef3d0&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;currency&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;RUB&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;и после:&lt;/p&gt;

&lt;div class=&quot;language-json highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;id&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;...&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;amount&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;10000&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;contractor&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;ref&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;e093c3c8-54c8-4375-a964-4a5de09ef3d0&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;entity&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;contractor&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;currency&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;RUB&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Чтобы работа с документами не превратилась в хаос, вводят версии схем. Версию
хранят в метаданных и используют в операторах if или case при работе с полем. В
примере ниже функция принимает документ и возвращает ссылку на контрагента:&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;(dеfn contractor-ref [doc]
  (let [version (-&amp;gt; doc :meta :version)]
    (cond
      (&amp;gt;= version 2)
      (-&amp;gt; doc :contractor :ref)

      :else
      (-&amp;gt; doc :contractor))))
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Логика сводится к оператору &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;cond&lt;/code&gt;: он находит первое истинное условие и
возвращает следующую за ним форму. Если версия документа больше или равна двум,
путь к ссылке учитывает &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;:ref&lt;/code&gt;. В противном случае сработает форма &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;:else&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Предположим, с версии 5 структура ссылки опять поменялась:&lt;/p&gt;

&lt;div class=&quot;language-json highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
   &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;meta&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;version&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;5&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
   &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
   &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;contractor&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;ref&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
         &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;id&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;...&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
         &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;type&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;:contractor&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
   &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Чтобы это учесть, добавим в &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;cond&lt;/code&gt; новую пару:&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;(dеfn contractor-ref [doc]
  (let [version (-&amp;gt; doc :meta :version)]
    (cond
      (&amp;gt;= version 5)
      (-&amp;gt; doc :contractor :ref :id)

      (&amp;gt;= version 2)
      (-&amp;gt; doc :contractor :ref)

      :else
      (-&amp;gt; doc :contractor))))
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Реляционные базы не позволяют делать то же самое на уровне колонок. Если ссылка
хранится в поле с типом uuid, в нее нельзя записать лишние данные: понадобится
новая колонка.&lt;/p&gt;

&lt;p&gt;Еще одно удобство документных баз в том, что, как правило, они предлагает
удобные &lt;strong&gt;CRUD-операции&lt;/strong&gt;. Реляционные базы, напротив, перекладывают их на ваши
плечи.&lt;/p&gt;

&lt;p&gt;Когда вы подключаетесь к документной базе из приложения, вам доступен объект
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Connection&lt;/code&gt; или &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Client&lt;/code&gt;. Он предлагает методы &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.create&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.update&lt;/code&gt;,
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;get_by_id&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;delete_by_id&lt;/code&gt; и другие. Вот как выглядит жизненный цикл документа
на псевдокоде:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;client = Client.connect(host, port, ...)

doc = {
    meta: {
        created_at: now(),
        created_by: user.id
    },
    attrs: {
        amount: 10000,
        currency: &quot;USD&quot;
    }
}

client.save(doc)
doc.update({&quot;attrs&quot;: {&quot;amount&quot;: 10550}})
doc.delete()
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Выше мы создали документ, обновили вложенное поле и удалили его. Разработчик
ничего не знает про внутреннее устройство базы. Работа с ней сводится к вызову
методов, что очень удобно.&lt;/p&gt;

&lt;p&gt;Реляционные базы не предлагают таких удобств. Разработчику доступен метод
.execute, который ожидает запрос и параметры. Что именно в этом запросе —
остается на ваше усмотрение. Чтобы извлечь платеж по ID, мы пишем SELECT:&lt;/p&gt;

&lt;div class=&quot;language-sql highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;select&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;payments&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;where&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;id&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;?&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Чтобы удалить — DELETE:&lt;/p&gt;

&lt;div class=&quot;language-sql highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;delete&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;payments&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;where&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;id&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;?&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Разработчик должен помнить об инъекциях, избегать конкатенации строк для
построения SQL, помнить операторы и многое другое.&lt;/p&gt;

&lt;p&gt;Выше таблица не может быть параметром. Выражения &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;select * from users&lt;/code&gt; и
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;select * from orders&lt;/code&gt; – это разные запросы. Если разработчик хочет
универсальную функцию, которая принимает имя таблицы и ID, ее пишут отдельно с
учетом небезопасных символов (экранирования). Некоторые таблицы квалифицированы,
то есть включают схему: не просто &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;payments&lt;/code&gt;, а &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;prod_analytics.payments&lt;/code&gt;. В
этом случае обе части экранируются отдельно:&lt;/p&gt;

&lt;div class=&quot;language-sql highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;select&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;&quot;prod_analytics&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;&quot;payments&quot;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Документные базы используют словари для фильтрации. Чтобы выбрать документы по
нескольким признакам, передают словарь атрибут =&amp;gt; значение (по умолчанию они
объединяются оператором AND). Если нужен еще один критерий отбора, в словарь
добавляют поле, и задача решена:&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;client.find(&quot;payments&quot;, {
    amount: 10000,
    currency: &quot;USD&quot;,
    created_by: 153523
})
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;В реляционных базах так не получится. В них фильтрация по двум и трем параметрам
— это разные запросы. Они отличаются даже не числом параметров, а синтаксисом:&lt;/p&gt;

&lt;div class=&quot;language-sql highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;select&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;payments&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;where&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;amount&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;10000&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;select&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;payments&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;where&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;amount&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;10000&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;and&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;currency&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'USD'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;В итоге, даже скачав библиотеку для Postgres, разработчик не может приступить к
работе. Требуется некий “обвес”, вспомогательный код, который берет на себя
вставку данных, выборку, удаление и остальное. Поэтому так популярны ORM: они
служат посредником между базой и приложением. ORM действительно помогает на
старте: легче начать проект, новички быстрее пишут код. У этой медали другая
сторона: иногда ORM посылает неоптимальные или излишние запросы, но разработчик
не в курсе этого.&lt;/p&gt;

&lt;h2 id=&quot;задачи-которые-решают-документы&quot;&gt;Задачи, которые решают документы&lt;/h2&gt;

&lt;p&gt;Разберем теперь, как фирмы приходят к документо-ориентированным базам данных:
какие факторы подталкивают их к тому, чтобы платить за кластер MongoDB или
облачный сервис DynamoDB. Речь пойдет о реальных, а не выдуманных случаях.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Проблема типов.&lt;/strong&gt; Некоторые данные с трудом ложаться на реляционную модель
из-за разнообразия типов. Следующий пример это иллюстрирует.&lt;/p&gt;

&lt;p&gt;Предположим, мы храним товары в Postgresql или MySQL. У товара есть код,
название, цена, ссылка на продавца и другие поля, свойственные всем товарам. Но
как быть с полями, которые относятся к особым видам товаров? Для ноутбука это
диагональ экрана, наличие портов и частота памяти. Для одежды — материалы, длина
рукава, обхват талии. Для спортивного питания — калорийность, доля жиров,
белков, углеводов, масса одной порции, число порций в упаковке и так далее.&lt;/p&gt;

&lt;p&gt;Приведем характеристики случайных товаров с маркетплейсов. Игровой ноутбук:&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;Процессор..................Intel Core 7 240H
Частота процессора.........2500
Ядер процессора............10
Тип памяти.................DDR5
Частота памяти.............5600
Наличие микрофона..........да
Беспроводные интерфейсы....Bluetooth, Wi-Fi
Интерфейсы.................Ethernet, USB 3.2 Gen2 Type A x 2
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Футболка:&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;Цвет товара.........черный
Стиль...............повседневный
Декор...............отсутствует
Размер..............L/XL
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Спортивное питание:&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;Количество в упаковке.....12 штук
Объем.....................1400
Общий вес.................800
Диетические особенности...без глютена, без сахара
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Проблема характеристик в том, что их много. Если вынести каждую их них в
колонку, мы упремся в ограничение базы на их число. (В скобках отметим, что
максимальное число колонок в Postgres — 1600, но на самом деле эта цифра зависит
от состава типов. Если точнее, длина записи не может превышать 8 килобайт –
максимальный размер страницы. Ограничение можно обойти, собрав Postgresql из
исходного кода с заменой константы blocksize, однако маловероятно, что читатель
пойдет на эти меры.)&lt;/p&gt;

&lt;p&gt;Массовые колонки плохи тем, что для большей части товаров они
пустуют. Представим таблицу, где для каждой характеристики создана колонка. Вот
как выглядят данные:&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;|   id | title            | laptop.cpu_count | laptop.mem_freq | cloth.color | cloth.size | food.box_mass | food.diet_note |
|------+------------------+------------------+-----------------+-------------+------------+---------------+----------------|
| 1001 | Razor Flare 18   |               10 |            5600 |             |            |               |                |
| 1002 | MSI Katana 13 XH |               12 |            3200 |             |            |               |                |
| 2001 | T-Shirt White    |                  |                 | white       | L,X,XL     |               |                |
| 2002 | Shirt Pop Blue   |                  |                 | blue        | XXL,S      |               |                |
| 3001 | Mega Snack Plus  |                  |                 |             |            |          1400 | no gluten      |
| 3002 | Protein Bomb     |                  |                 |             |            |          1200 | no sugar       |
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Характеристики ноутбуков для всех товаров, кроме ноутбуков, будут пустыми; то же
самое относится к другим колонкам. Таблица превратилась в разреженную матрицу,
большую часть которой занимает пустота. Хранение пустоты обходится дорого, и мы
должны ее избегать.&lt;/p&gt;

&lt;p&gt;Поскольку рост по горизонтали нам закрыт, пойдем по вертикали. Мы уже знакомы с
моделью EAV, и здесь она весьма кстати. Добавим таблицу &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;item_props&lt;/code&gt; с колонками
item_id, property и value. В них записаны ссылки на товары, имена характеристик
и значения:&lt;/p&gt;

&lt;div class=&quot;language-sql highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;create&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;table&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;items&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;id&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;integer&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;primary&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;title&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;text&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;create&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;table&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;item_props&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;item_id&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;integer&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;references&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;items&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;property&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;text&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;value&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;text&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Подготовим товары: два ноутбука, две футболки и два батончика:&lt;/p&gt;

&lt;div class=&quot;language-sql highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;insert&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;into&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;items&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;values&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1001&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'Razor Flare 18'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1002&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'MSI Katana 13 XH'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;2001&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'T-Shirt White'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;2002&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'Shirt Pop Blue'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;3001&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'Mega Snack Plus'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;3002&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'Protein Bomb'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Назначим характеристики:&lt;/p&gt;

&lt;div class=&quot;language-sql highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;insert&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;into&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;item_props&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;values&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1001&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'laptop.cpu_count'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'10'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1002&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'laptop.mem_freq'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'5600'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1002&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'laptop.cpu_count'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'12'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1002&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'laptop.mem_freq'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'3200'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;2001&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'cloth.color'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'white'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;2001&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'cloth.size'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'L,X,XL'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;2002&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'cloth.color'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'blue'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;2002&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'cloth.size'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'XXL,S'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;3001&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'food.box_mass'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'1400'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;3001&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'food.diet_note'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'no gluten'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;3002&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'food.box_mass'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'1200'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;3002&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'food.diet_note'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'no sugar'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Чтобы выбрать товары с характеристиками, соединим таблицы оператором JOIN:&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;select * from items, item_props
where items.id = item_props.item_id;

┌──────┬──────────────────┬─────────┬──────────────────┬───────────┐
│  id  │      title       │ item_id │     property     │   value   │
├──────┼──────────────────┼─────────┼──────────────────┼───────────┤
│ 1001 │ Razor Flare 18   │    1001 │ laptop.cpu_count │ 10        │
│ 1002 │ MSI Katana 13 XH │    1002 │ laptop.mem_freq  │ 5600      │
│ 1002 │ MSI Katana 13 XH │    1002 │ laptop.cpu_count │ 12        │
│ 1002 │ MSI Katana 13 XH │    1002 │ laptop.mem_freq  │ 3200      │
│ 2001 │ T-Shirt White    │    2001 │ cloth.color      │ white     │
│ 2001 │ T-Shirt White    │    2001 │ cloth.size       │ L,X,XL    │
│ 2002 │ Shirt Pop Blue   │    2002 │ cloth.color      │ blue      │
│ 2002 │ Shirt Pop Blue   │    2002 │ cloth.size       │ XXL,S     │
│ 3001 │ Mega Snack Plus  │    3001 │ food.box_mass    │ 1400      │
│ 3001 │ Mega Snack Plus  │    3001 │ food.diet_note   │ no gluten │
│ 3002 │ Protein Bomb     │    3002 │ food.box_mass    │ 1200      │
│ 3002 │ Protein Bomb     │    3002 │ food.diet_note   │ no sugar  │
└──────┴──────────────────┴─────────┴──────────────────┴───────────┘
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Таблицу &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;item_props&lt;/code&gt; можно заполнять бесконечно, не беспокоясь о числе
характеристик. Если ноутбуку понадобится новое свойство (объем видеопамяти), это
будет очередная строка в таблице:&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;insert into item_props values
    (1001, 'laptop.video_mem', '8 Gb');
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Наш дизайн не учитывает, что у характеристик разные типы. Частота памяти — это
число (2666 герц), размер одежды — перечисление (L, XL), наличие bluetooth —
логический флаг (есть или нет). Иные значения могут быть коллекциями, например
версии кодеков или протоколы — это списки строк или чисел.&lt;/p&gt;

&lt;p&gt;В таблице &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;item_props&lt;/code&gt; колонка &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;value&lt;/code&gt; носит строковый тип. Это компромисс, на
который мы пошли, чтобы хранить в ней любое значение. В целом это неудобно,
особенно если требуется поиск по характеристикам: придется приводить текст к
числу, булеву и другим типам.&lt;/p&gt;

&lt;p&gt;Нам поможет тип jsonb, который вмещает другие типы: строки, числа, а также
коллекции. Назначим полю value тип jsonb, и теперь оно может хранить любые
значения. Проделаем это на новой таблице:&lt;/p&gt;

&lt;div class=&quot;language-sql highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;create&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;table&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;item_props_json&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;item_id&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;integer&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;references&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;items&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;property&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;text&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;value&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;jsonb&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;insert&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;into&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;item_props_json&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;values&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1001&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'laptop.cpu_count'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'10'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1002&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'laptop.mem_freq'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'5600'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1002&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'laptop.cpu_count'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'12'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1002&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'laptop.mem_freq'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'3200'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;2001&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'cloth.color'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'&quot;white&quot;'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;2001&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'cloth.size'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'[&quot;L&quot;,&quot;X&quot;,&quot;XL&quot;]'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;2002&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'cloth.color'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'&quot;blue&quot;'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;2002&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'cloth.size'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'[&quot;XXL&quot;,&quot;S&quot;]'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;3001&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'food.box_mass'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'1400'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;3001&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'food.diet_note'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'&quot;no gluten&quot;'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;3002&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'food.box_mass'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'1200'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;3002&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'food.diet_note'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'&quot;no sugar&quot;'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Обратите внимание, что строки заключаются в двойные кавычки, потому что этого
требует стандарт JSON. Выберем данные еще раз:&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;select * from items, item_props_json
where items.id = item_props_json.item_id;

┌──────┬──────────────────┬─────────┬──────────────────┬──────────────────┐
│  id  │      title       │ item_id │     property     │      value       │
├──────┼──────────────────┼─────────┼──────────────────┼──────────────────┤
│ 1001 │ Razor Flare 18   │    1001 │ laptop.cpu_count │ 10               │
│ 1002 │ MSI Katana 13 XH │    1002 │ laptop.mem_freq  │ 5600             │
│ 1002 │ MSI Katana 13 XH │    1002 │ laptop.cpu_count │ 12               │
│ 1002 │ MSI Katana 13 XH │    1002 │ laptop.mem_freq  │ 3200             │
│ 2001 │ T-Shirt White    │    2001 │ cloth.color      │ &quot;white&quot;          │
│ 2001 │ T-Shirt White    │    2001 │ cloth.size       │ [&quot;L&quot;, &quot;X&quot;, &quot;XL&quot;] │
│ 2002 │ Shirt Pop Blue   │    2002 │ cloth.color      │ &quot;blue&quot;           │
│ 2002 │ Shirt Pop Blue   │    2002 │ cloth.size       │ [&quot;XXL&quot;, &quot;S&quot;]     │
│ 3001 │ Mega Snack Plus  │    3001 │ food.box_mass    │ 1400             │
│ 3001 │ Mega Snack Plus  │    3001 │ food.diet_note   │ &quot;no gluten&quot;      │
│ 3002 │ Protein Bomb     │    3002 │ food.box_mass    │ 1200             │
│ 3002 │ Protein Bomb     │    3002 │ food.diet_note   │ &quot;no sugar&quot;       │
└──────┴──────────────────┴─────────┴──────────────────┴──────────────────┘
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Так выглядят данные в терминале, однако в приложении результат может
отличаться. Библиотеки для работы с базой автоматически парсят значения json(b),
и они становятся нативными типами приложения: числами, строками, словарями и так
далее.&lt;/p&gt;

&lt;p&gt;В некоторых языках, например в Java, нет встроенного JSON-парсера. Драйвер JDBC
не преобразует колонку jsonb в значение JVM. Вместо этого он вернет объект
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PGObject&lt;/code&gt; с JSON-строкой, и как с ней работать – остается на ваше
усмотрение. Различные ORM предлагают &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;JSONField&lt;/code&gt; – класс, который кодирует и
декодирует данные, используя стороннюю библиотеку. В других языках, например
Clоjure, можно расширить типы протоколом – соглашением о том, как упаковать их в
JSON и прочитать обратно.&lt;/p&gt;

&lt;p&gt;Решение с характеристиками можно улучшить. Вместо того, чтобы хранить их по
отдельности, построим словарь вида характеристика -&amp;gt; значение. В этом случае
таблица item_props не понадобится: словарь хранится в самом товаре. Пример:&lt;/p&gt;

&lt;div class=&quot;language-sql highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;drop&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;table&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;item_props&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;drop&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;table&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;item_props_json&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;alter&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;table&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;items&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;add&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;column&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;props&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;jsonb&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;update&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;items&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;set&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;props&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;$$&lt;/span&gt;
    &lt;span class=&quot;err&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;&quot;laptop.cpu_count&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;10&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;&quot;laptop.mem_freq&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;5600&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;err&quot;&gt;$$&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;where&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;id&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1001&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;update&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;items&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;set&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;props&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;$$&lt;/span&gt;
    &lt;span class=&quot;err&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;&quot;cloth.color&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;&quot;white&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;&quot;cloth.size&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;&quot;L&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;&quot;X&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;&quot;XL&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;err&quot;&gt;$$&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;where&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;id&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;2001&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Мы удалили таблицы характеристик, добавили товару колонку props и записали в нее
свойства двух товаров. На практике такие изменения автоматизируют: пишут запрос,
который “схлопывает” характеристики и значения в словари и переносит в товары. В
следующих главах мы рассмотрим эту технику.&lt;/p&gt;

&lt;p&gt;Попутно решается еще одна деталь: объект JSON исключает повторы ключей, поэтому
не получится хранить две характеристики с одинаковым именем. В таблице
item_props мы бы создали уникальный индекс для комбинации (item_id, property).&lt;/p&gt;

&lt;p&gt;Словарь характеристик, хоть и ослабляет структуру базы, оказывается удачным
решением. Да, поле JSON – черный ящик, который хранит что угодно. Да, перед
записью вы обязаны проверить, что props – словарь, а не число или список. Да,
тип jsonb нарушает нормальные формы, которым учат в университетах. Однако
упрощение, которое дает jsonb, в конечном счете перевешивает недостатки. Мы
получили меньше таблиц и меньше кода. Задача решена простым и понятным способом.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Сложная структура.&lt;/strong&gt; Иногда сервис получает уведомления платежных систем:
Paypal, Stripe, App Store. Чтобы проверить покупку, из уведомления берут коды
пользователя и товара, сумму, номер транзакции. Эти поля записывают в базу и
показывают в личном кабинете. Перед обработкой уведомления проверяют, что
транзакция с таким номером еще не встречалась.&lt;/p&gt;

&lt;p&gt;На практике уведомления содержат больше четырех полей. Так, служба &lt;a href=&quot;https://developer.paypal.com/api/nvp-soap/ipn/&quot;&gt;Paypal
IPN&lt;/a&gt; (Instant Payment Notification, мгновенные уведомления о платежах)
передает почти сорок полей, среди которых:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;код уведомления;&lt;/li&gt;
  &lt;li&gt;код транзакции;&lt;/li&gt;
  &lt;li&gt;код покупателя;&lt;/li&gt;
  &lt;li&gt;восемь полей адреса (страна, город, улица и так далее);&lt;/li&gt;
  &lt;li&gt;подробные данные о платеже: валюта цены, валюта оплаты, курс конвертации,
комиссия Paypal, сумма с ней и без нее, отдельная цена доставки;&lt;/li&gt;
  &lt;li&gt;иные служебные поля: тип операции, статус клиента, статус платежа, кодировка,
версия протокола.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;То же самое относится к App Store: сервера Apple присылают огромные документы,
структура которых описана в документации.&lt;/p&gt;

&lt;p&gt;Чаще всего разработчики читают только те поля, что необходимы для проверки
платежа. Остальное отбрасывают за ненадобностью: у разработчика нет времени,
чтобы адаптировать базу данных. В самом деле: чтобы хранить уведомления Paypal в
таблице, понадобится 40 колонок. Много времени уйдет на то, чтобы разложить
документ на поля и правильно записать их. Позже формат уведомлений может
измениться, и мы получим ошибку из-за несовместимых типов. Досадно, когда
уведомление пришло, но не обработалось по нашей вине.&lt;/p&gt;

&lt;p&gt;Решение в том, чтобы выбрать из уведомления только главные поля, а остальные
сохранить в поле jsonb. Этот вариант устойчив к ситуации, когда структура
уведомления меняется.&lt;/p&gt;

&lt;p&gt;Зачем хранить уведомления? Одна из причин в том, что они полезны для отчетов и
аналитики. Например, руководитель хочет знать, какую услугу чаще всего покупали
в такой-то стране в определенный период. Менеджер запросит отчет продаж в
разрезе категорий товаров. Имея данные в базе, пусть даже в виде jsonb, эти
потребности легко удовлетворить.&lt;/p&gt;

&lt;p&gt;Формально платежные сервисы предлагают отчеты в личном кабинете, но их качество
желает лучшего. Сервисы не предлагают группировку, а выгружают только плоские
записи. Ожидается, что их передадут программисту, который сгруппирует должным
образом. Некоторые сервисы поддерживают группировку, но диапазон дат не может
быть больше трех месяцев. Чтобы собрать отчет за прошлый год, понадобятся четыре
запроса: с декабря по февраль, с марта по май и так далее, после чего результаты
объединяют.&lt;/p&gt;

&lt;p&gt;Локальные данные решают эту проблему. Разумеется, время от времени проводят
сверку с платежным сервисом, чтобы выявить расхождения. Однако в плане
отчетности вы не зависите от него: не нужно вызывать API, чтобы показать историю
платежей или продажи в определенной стране за месяц. Технически это решается
хранением документов в поле jsonb.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Стандарты.&lt;/strong&gt; Иногда бизнес опирается на стандарты — соглашения о том, как
обмениваться данными. Одним из примеров служит &lt;a href=&quot;https://www.hl7.org/fhir/&quot;&gt;FHIR&lt;/a&gt; (Fast Healthcare
Interoperability Resources) — стандарт обмена медицинской информацией в США и
других странах. FHIR определяет сущности, принятые в медицинской сфере: пациент,
клиника, анализы, история болезни. Также он описывает спецификацию REST-сервера,
который их хранит и обрабатывает.&lt;/p&gt;

&lt;p&gt;Сущности выглядят как огромные JSON-документы. Приведем &lt;a href=&quot;https://build.fhir.org/patient-examples.html&quot;&gt;пример
пациента&lt;/a&gt; с официального сайта:&lt;/p&gt;

&lt;div class=&quot;language-json highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;resourceType&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;Patient&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;id&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;example&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;identifier&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;use&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;usual&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;type&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;coding&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;system&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;http://terminology.hl7.org/CodeSystem/v2-0203&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;code&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;MR&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}]&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;system&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;urn:oid:1.2.36.146.595.217.0.1&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;value&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;12345&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;period&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;start&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;2001-05-06&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;assigner&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;display&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;Acme Healthcare&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}],&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;active&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;kc&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;name&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;use&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;official&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;family&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;Chalmers&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;given&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;Peter&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;James&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;use&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;usual&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;given&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;Jim&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;use&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;maiden&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;family&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;Windsor&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;given&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;Peter&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;James&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;],&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;period&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;end&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;2002&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}],&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;telecom&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;use&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;home&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;system&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;phone&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;value&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;(03) 5555 6473&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;use&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;work&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;rank&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;system&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;phone&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;value&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;(03) 3410 5613&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;use&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;mobile&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;rank&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;system&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;phone&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;value&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;(03) 5555 8834&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;use&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;old&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;period&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;end&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;2014&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}],&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;gender&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;male&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;birthDate&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;1974-12-25&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;_birthDate&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;extension&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;url&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;http://hl7.org/fhir/StructureDеfinition/patient-birthTime&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;valueDateTime&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;1974-12-25T14:35:45-05:00&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}]&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;deceasedBoolean&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;kc&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;address&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;use&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;home&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;type&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;both&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;text&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;534 Erewhon St&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\n&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;PeasantVille, Rainbow, Vic  3999&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;line&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;534 Erewhon St&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;],&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;city&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;PleasantVille&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;district&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;Rainbow&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;state&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;Vic&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;postalCode&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;3999&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;period&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;start&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;1974-12-25&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}],&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;contact&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;relationship&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;coding&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;system&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;http://terminology.hl7.org/CodeSystem/v2-0131&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;code&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;N&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}]&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}],&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;name&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;family&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;du Marché&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;_family&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;extension&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
          &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;url&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;http://hl7.org/fhir/StructureDеfinition/humanname-own-prefix&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
          &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;valueString&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;VV&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}]&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;given&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;Bénédicte&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;additionalName&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;use&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;nickname&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;given&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;Béné&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}],&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;telecom&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;system&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;phone&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;value&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;+33 (237) 998327&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}],&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;address&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;use&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;home&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;type&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;both&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;line&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;534 Erewhon St&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;],&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;city&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;PleasantVille&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;district&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;Rainbow&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;state&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;Vic&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;postalCode&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;3999&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;period&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;start&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;1974-12-25&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;additionalAddress&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;use&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;work&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;line&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;123 Smart St&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;],&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;city&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;PleasantVille&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;state&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;Vic&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;postalCode&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;3999&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}],&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;gender&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;female&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;period&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;start&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;2012&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}],&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;managingOrganization&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;reference&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;Organization/1&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Опытный разработчик скажет, что документ можно разбить на таблицы. Технически
это возможно; скорее всего, найдется ORM, которая поделит документ на таблицы
patient, address, phone и другие, а при чтении – соберет обратно. Проблема в
том, что подобных сущностей — полторы сотни (на момент написания книги — 157), и
если допустить, что на каждую понадобятся три таблицы…&lt;/p&gt;

&lt;p&gt;Нет смысла считать: ясно, что такая база будет слишком сложной. Почти вся она
будет сгенерирована, и уместить ее в голове одному человеку невозможно. Факт,
что однажды ее станет сложно поддерживать, является лишь вопросом
времени. Поэтому документы хранят как есть — без дробления на таблицы в колонке
jsonb. Скажем, таблица пациентов выглядит так:&lt;/p&gt;

&lt;div class=&quot;language-sql highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;create&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;table&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;patients&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;id&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;uuid&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;primary&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;key&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;d&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;е&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;fault&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;uuid_generate_v4&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(),&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;entity&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;jsonb&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;not&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;created_at&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;timestamptz&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;not&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;null&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;d&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;е&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;fault&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;current_timestamp&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;updated_at&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;timestamptz&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Особо важные свойства пациента можно вынести в колонки, чтобы ускорить к ним
доступ. Однако чаще всего это решается индексами или вычисляемыми столбцами,
которые мы рассмотрим в будущих главах.&lt;/p&gt;

&lt;p&gt;Другая область, где используют документы — &lt;strong&gt;это игры&lt;/strong&gt;. В пошаговых стратегиях,
где ходы делают по очереди, состояние игры описано документом. Он содержит
сведения об игроках, юнитах, их здоровье, статусах, предметах, очередности хода
и многое другое. Состояние игры хранится в одном месте и читается одним
запросом. Когда игрок делает ход, состояние извлекают из поля jsonb. К нему
применяют накопленные ходы, записывают в базу новое состояние и рассылают
игрокам события.&lt;/p&gt;

&lt;p&gt;По такому принципу работала Глобальная Карта компании Wargaming, где когда-то
работал автор. Состояние Карты хранилось в Postgresql в поле jsonb; от игроков
принимались ходы. Каждый час запускалась задача, которая применяла ходы к
состоянию, и таким образом шла игра.&lt;/p&gt;

&lt;p&gt;Наконец, разработчик может столкнуться с документной базой в результате
&lt;strong&gt;расширения фирмы&lt;/strong&gt;. Банки и корпорации чаще покупают компании, чем создают их
с нуля. Может случится так, что банк купил стартап для путешествий, который
хранит данные в MongoDB. Сразу после покупки дают задачу на интеграцию с ним в
других сервисах. Перенос стартапа на другой стек – дело сложное и полное
сюрпризов, поэтому вы долго будете иметь дело с тем, что есть.&lt;/p&gt;

&lt;p&gt;Причины, по которым выбирают документные базы в пользу реляционных, бывают
самыми разными. Самое важное — разработчик не всегда на это влияет, и к этому
следует быть готовым.&lt;/p&gt;

&lt;h2 id=&quot;слабые-стороны-документов&quot;&gt;Слабые стороны документов&lt;/h2&gt;

&lt;p&gt;Мы привели некоторые достоинства документных баз. Пусть читатель не возводит их
в абсолют: не бывает базы данных, хорошей абсолютно во всем. У каждой из них
сильные и слабые стороны, и задача в том, чтобы выбрать решение, сильные стороны
которого совпадают с нуждами проекта. Выбор сводится к торгу: соглашаемся на
одно, терпим другое.&lt;/p&gt;

&lt;p&gt;Перечислим слабые стороны документов, которые заметны в сравнении с реляционной
моделью.&lt;/p&gt;

&lt;p&gt;Первая и самая значимая особенность — &lt;strong&gt;отсутствие оператора JOIN&lt;/strong&gt;. В SQL им
соединяют таблицы по горизонтали. Скажем, если профиль ссылается на
пользователя, можно извлечь обе сущности разом, соединив по ключам (первичному и
внешнему). Это полезно в отчетах, выгрузке данных, а также когда мы работаем не
с одним пользователем, а с тысячами.&lt;/p&gt;

&lt;p&gt;Аргумент насчет тысяч записей особенно важен. Когда нужен один пользователь и
его профиль, обе записи легко выбрать по отдельности функцией &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;get_by_id&lt;/code&gt;. Если
это отчет по всем пользователям, мы выполним два запроса: все пользователи и все
профили. Далее предстоит работа: профили индексируют, обходят пользователей,
получают по словарю профиль, фильтруют данные. Это долго, хрупко, требует кода и
тестирования.&lt;/p&gt;

&lt;p&gt;Другими словами: разработчик делает то, что умеет реляционная база
данных. Оператор JOIN выполнит то же самое, только на порядок быстрее,
безопасней и без экранов кода.&lt;/p&gt;

&lt;p&gt;Иные документные базы не имеют оператора JOIN в принципе. К ним относится
Cassandra – распределенная база, написанная на Java. Проблему соединения в ней
решают денормализацией. Если нужно соединение таблиц users и profiles, заводят
таблицу users_profiles и пишут в нее те же данные. Поскольку данные обновляются
в двух местах, это чревато расхождением и как следствие – странными
результатами.&lt;/p&gt;

&lt;p&gt;В MongoDB аналог JOIN называется &lt;a href=&quot;https://www.mongodb.com/docs/manual/reference/operator/aggregation/lookup/&quot;&gt;$lookup&lt;/a&gt;. Этот оператор выбирает
сущности из смежной коллекции. Предположим, в MongoDB хранятся статьи и
комментарии. Ниже мы выбираем статьи, при этом к каждой из них подтягиваются
комментарии по условию &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;post._id = comment.post_id&lt;/code&gt; (поле id предваряется
подчеркиванием, потому что оно системное).&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;db.posts.aggregate([
  {
    $lookup: {
      from: &quot;comments&quot;,
      localField: &quot;_id&quot;,
      foreignField: &quot;post_id&quot;,
      as: &quot;comments&quot;
    }
  },
  {
    $limit: 100
  }
])
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;В результате у каждой статьи поле &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;comments&lt;/code&gt; с массивом комментариев.&lt;/p&gt;

&lt;p&gt;Определенно, оператор &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;$lookup&lt;/code&gt; — лучше, чем ничего. С ним задача упрощается: не
нужно соединять данные в приложении. Однако &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;$lookup&lt;/code&gt; покрывает только левое
соединение (LEFT JOIN). Чтобы сделать его внутренним (исключить статьи без
комментариев), добавляют условие, что собранный массив не пуст:&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;{$match: {comments: {$ne: []}}}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Для правого, полного, анти- и других соединений операторов нет. Это расстроит
любителей реляционных баз, поскольку они знают: семейство операторов JOIN —
мощный инструмент, которым решают целый пласт задач.&lt;/p&gt;

&lt;p&gt;Похожая проблема встречается в Datomic. Эта база, наоборот, сильна во внутренних
соединениях (INNER JOIN), при котором записи без совпадений отбрасываются. Левые
соединения частично возможны при помощи функции &lt;a href=&quot;https://docs.datomic.com/query/query-data-reference.html#get-else&quot;&gt;get-else&lt;/a&gt;. Полные и
правые, увы, приходится делать в приложении.&lt;/p&gt;

&lt;p&gt;Общий принцип таков: документные базы лишь частично реализуют JOIN. Если вы
мыслите таблицами, понадобится время, чтобы сменить парадигму.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;В плане транзакций&lt;/strong&gt; документные базы тоже уступают реляционным. Например, в
Datomic транзакции выполняются строго одна за другой. Так устроен транзактор –
процесс, который отвечает за изменения в базе. Если транзакция займет длительное
время, она заблокирует другие операции на запись (на чтении это не
скажется). Подобных ситуаций избегают, разбивая крупные изменения на группу
мелких.&lt;/p&gt;

&lt;p&gt;Изначально MongoDB предлагала транзакции в рамках одного
документа. Гарантировалось, что либо все изменения увенчаются успехом, либо не
сработает ни одно. В 2018 году появились ACID-транзакции в разрезе нескольких
документов. MongoDB по-прежнему разделяет эти понятия: легкие транзакции для
одного документа и тяжелые для нескольких. Последние доступны лишь в случае,
когда сервер запущен с поддержкой реплики (флаг &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;--replset&lt;/code&gt;). Это усложняет
локальную разработку, когда MongoDB запущен в Docker и нужно отладить транзакцию
в нескольких документах.&lt;/p&gt;

&lt;p&gt;В Postgres транзакции занимают центральное место. Существуют их разные уровни,
точки сохранения и отката. Многие сущности и переменные живут строго в рамках
транзакции. За счет &lt;a href=&quot;https://postgrespro.ru/docs/postgrespro/current/mvcc-intro&quot;&gt;механизма MVCC&lt;/a&gt; (multi-version concurrency control,
мульти-версионный контроль параллельного доступа) многие транзакции выполняются
параллельно и не мешают друг другу. В критических случаях применяют двухфазные
транзакции, известные как XA или 2 phase commits. С ними атомарные изменения
возможны даже на разных физических серверах.&lt;/p&gt;

&lt;p&gt;Работая с документной базой, обратите внимание на &lt;strong&gt;протокол обмена&lt;/strong&gt;. Если
данные передаются в двоичном виде, это хороший признак: сервер может вернуть
большой объем данных, что необходимо в ряде задач.&lt;/p&gt;

&lt;p&gt;Иные базы общаются с клиентами по REST и JSON (например, OpenSearch). На первый
взгляд это удобно: нужен только HTTP-клиент и JSON-парсер. Как правило, то и
другое есть в поставке языка. Работа с базой становится делом тривиальным: нужно
отправить JSON, проверить статус и прочитать результат. Многие задачи легко
автоматизировать утилитами curl и jq.&lt;/p&gt;

&lt;p&gt;Протокол HTTP позволяет обращаться к базе из браузера при помощи XMLHttpRequest
и Fetch API. Это снижает нагрузку на приложение, потому что оно не служит
посредником между браузером и базой. С другой стороны, это небезопасно:
обращение к базе из браузера означает, что последний хранит ключ
доступа. Подобный доступ используют в закрытых системах, например внутренних
приложениях фирмы, где база доступна только из локальной сети.&lt;/p&gt;

&lt;p&gt;Недостаток JSON том, что он плохо подходит для ленивого чтения. Парсеры читают
его целиком, и на больших данных это проблематично. Растет потребление памяти;
нельзя обработать первую сущность, пока не прочитаны все. Текстовый формат
избыточен: числа записаны неплотно (один байт – один десятичный разряд), строки
экранированы (содержат символы &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;\n&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;\t&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;\uXXXX&lt;/code&gt; и другие). Неоднозначная
ситуация с числами с плавающей запятой. JSON не сообщает, как их читать (Float,
Double, BigDecimal), и выбор остается за вами. Также в JSON нет дат и времени:
эти поля приводят к нужным типам вручную.&lt;/p&gt;

&lt;p&gt;Двоичный формат решает эти проблемы. Числа в нем записаны плотно: скажем,
2 147 483 647 умещается в четыре байта, а не десять. Строки не
экранируются, и парсер не бежит по ним в поисках обратной косой черты. Длина
значений известна заранее. Каждое значение предваряется кодом типа, так что мы
отличим Double от BigDecimal и строку от времени. Двоичный формат удобен для
ленивого чтения.&lt;/p&gt;

&lt;p&gt;Читатель возразит: проблемы форматов решаются библиотеками. Хорошо написанный
клиент скрывает эти шероховатости, и разработчику не важно, как передаются даты,
сколько выделено памяти и так далее. Но практика показывает: чем дольше вы
работаете с базой, тем вероятней эти детали коснутся вас.&lt;/p&gt;

&lt;p&gt;Обратите внимание, насколько легко &lt;strong&gt;забрать из таблицы&lt;/strong&gt; все данные. В Postgres
для этого служит команда COPY. Она выгружает таблицу или запрос в файл, процесс
или поток. В последнем случае клиент принимает поток и направляет в локальный
файл, сеть, функцию обработки. COPY поддерживает три формата: CSV, текстовый и
двоичный, каждый со своими преимуществами и недостатками. Команда COPY полезна
при переезде, в отчетности, тестах, резервном копировании и других
случаях. Копирование работает в обе стороны: от сервера клиенту и
обратно. Большие таблицы копируют параллельно в несколько потоков: каждый
отвечает за свою часть. При этом прирост скорости почти линейный.&lt;/p&gt;

&lt;p&gt;Не все документные базы столь легко делятся данными. Например, OpenSearch
возвращает не более 10 тысяч документов за один запрос. Если в базе миллион
документов, потребуется сто обращений. При этом ваша задача – следить, чтобы
результат каждого запроса не превышал 2 147 483 647 байтов, иначе переполнится
объект ByteArrayOutputStream.&lt;/p&gt;

&lt;p&gt;OpenSearch поддерживает API под названием scroll (аналог курсора в Postgres). Он
гарантирует, что в рамках скролла мы не увидим изменений, сделанных другими
клиентами. Однако scroll подразумевает запросы в цикле и проброс nextToken. Это
влечет написание кода и его отладку. В плане удобства scroll бесконечно далек от
COPY.&lt;/p&gt;

&lt;p&gt;Наконец, документные базы &lt;strong&gt;часто дороги&lt;/strong&gt; в развертке и эксплуатации. Причина в
том, что многие из них устроены как абстракции над другими хранилищами
данных. Выше мы упоминали: Datomic хранит индексы в Postgresql, MySQL или
DynamoDB. Желательно иметь кластер Memcache для снижения нагрузки. XTDB работает
поверх RocksDB, кластера Kafka или сервиса S3. OpenSearch состоит из компонентов
с разной функциональностью: ведущий узел (master node), хранилище (data node),
прием данных (ingest node) и другие.&lt;/p&gt;

&lt;p&gt;Когда в системе много узлов, их трудно развернуть локально. В том числе поэтому
документные базы почти всегда работают в облаке. Для локальной разработки
доступны их упрощенные dev-версии. В случае Datomic это библиотека
com.datomic/local, в которой нет серверного узла: база работает в приложении и
использует файлы или память для хранения.&lt;/p&gt;

&lt;p&gt;Считается очевидным, что такие системы дешевле развернуть в облаке. На первых
порах это действительно так: запуск OpenSearch или Datomic в AWS занимает минуту
с момента нажатия на кнопку. Если купить виртуальный сервер и поручить то же
самое devops-инженеру, он потратит на это дни.&lt;/p&gt;

&lt;p&gt;Однако за все нужно платить, и часто это понимают, когда увязли в облачных
решениях. Автор наблюдал эту ситуацию в стартапах и крупных фирмах. На ранних
этапах команда ни в чем себе не отказывает: внедряет одно облачное решение,
второе, третье. Бдительность усыпляют trial-периоды, когда счета выставляются со
скидкой. Но вот они кончились, и счет за облако съедает почти всю
прибыль. Инвесторы требуют урезать расходы, и разработчикам ставят задачи на
оптимизацию. Теперь они заняты техническими вещами, а не развитием продукта.&lt;/p&gt;

&lt;p&gt;Не начинайте работу с облачной документной базой, пока не рассчитаете
стоимость. Итоговую сумму умножьте на два: во-первых, чтобы не ошибиться в
меньшую сторону и оставить запас бюджета. Во-вторых, учитывайте тестовые
окружения: они понадобятся для обкатки приложения и закрытых показов
заказчику. Тестовые окружения, хоть и потребляют меньше ресурсов, тоже вносят
вклад в ежемесячные счета.&lt;/p&gt;

&lt;h2 id=&quot;замечание-о-переоценке&quot;&gt;Замечание о переоценке&lt;/h2&gt;

&lt;p&gt;Индустрия страдает от еще одного недуга: многие ожидания переоценены. Открывая
стартап, руководство планирует кратный рост пользователей в день. Программисты
пишут сложный кэш, уверенные, что система не выдержит наплыва посетителей. В
качестве базы данных используют что-то облачное, распределенное, ориентированное
на документы. Причина та же: ожидается, что традиционные Postgres и MySQL не
выдержат нагрузки.&lt;/p&gt;

&lt;p&gt;По наблюдению автора, чаще случается иное: мощные решения оказываются
избыточными, а их поддержка – тратой времени и денег. Не переоценивайте мощь
NoSQL. Меняйте парадигму только если исчерпали возможности реляционных баз –
однако и здесь случаются удивительные примеры.&lt;/p&gt;

&lt;p&gt;На конференции PGConf сотрудник OpenAI рассказал о роли Postgres в компании. С
его слов, OpenAI использует один узел Postgres в облаке Azure. Из особых техник
применяется только репликация, чтобы разгрузить чтение. Запись обслуживает один
узел, при этом не используется даже шардирование. А тем временем аудитория
OpenAI приближается к миллиарду пользователей! Согласно опросам, каждый десятый
человек в мире пользуется их услугами – и все это обслуживает один узел Postgres
с несколькими репликами. &lt;a href=&quot;https://pigsty.io/blog/db/openai-pg/&quot;&gt;Подробности доклада&lt;/a&gt; и слайды вы найдете на
странице &lt;a href=&quot;https://www.pgevents.ca/events/pgconfdev2025/schedule/session/433-scaling-postgres-to-the-next-level-at-openai/&quot;&gt;PGConf Dev 2025&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;При всем скепсисе автора к AI нельзя не признать: продукты OpenAI совершили
поворот в индустрии. Появился пример того, как компания с одним узлом Postgres
вышла на уровень гигантов. Выбирая технический стек, помните об этом факте.&lt;/p&gt;

&lt;h2 id=&quot;почему-postgres&quot;&gt;Почему Postgres&lt;/h2&gt;

&lt;p&gt;В этой книге мы решаем следующую задачу: адаптируем Postgres для хранения
документов. На первый взгляд это бессмысленно: зачем Postgres, если существуют
специальные решения? Многие из них мы упоминали: это MongoDB, OpenSearch,
DynamoDB и другие.&lt;/p&gt;

&lt;p&gt;Причина в том, что пока развивались NoSQL-решения, Postgres тоже не стоял на
месте. На смену json пришел jsonb, и с каждой версией его возможности
улучшаются. Мы узнаем, как читать подмножество jsonb, обновлять его отдельные
поля, переводить jsonb в таблицу и обратно, использовать язык JSON Path и другие
техники. Вместе они не уступают возможностям документных баз.&lt;/p&gt;

&lt;p&gt;Кроме типов json(b), Postgres силен в других направлениях. Прежде всего это
операторы JOIN. С их помощью строят отчеты и сложные выборки, сравнивают массивы
данных на предмет совпадений. JOIN развивает табличное мышление, о котором мы
говорили в начале главы. Со временем вы увидите, что почти любая задача решается
соединением: внутренним, левым, полным и другими.&lt;/p&gt;

&lt;p&gt;Транзакции в Postgres раскрываются в полной мере. Они дают гранулярный контроль
над изменениями, управляют параллельным доступом к данным.&lt;/p&gt;

&lt;p&gt;Расширение pg_trgm (триграммы) предлагает поиск на вхождение строки,
сопоставление с регулярным выражением, проверку схожести строк.&lt;/p&gt;

&lt;p&gt;Расширение pg_cron выполняет запросы по расписанию, что снимает вопрос о внешнем
планировщике.&lt;/p&gt;

&lt;p&gt;Обычные и материализованные представления помогают в отчетности. Они снижают
нагрузку на базу, смягчают неудачный дизайн. Это и многое другое мы изучим в
дальнейших главах.&lt;/p&gt;

&lt;p&gt;Postgres открыт и бесплатен в использовании. Он умерен в требованиях и работает
на самом скромном оборудовании. Вы найдете Postgres во всех менеджерах пакетов:
apt, yaml, brew и других. Легко запустить Postgres на виртуальном сервере ценой
в несколько долларов. Это будет настоящий Postgres, а не муляж для локальной
разработки.&lt;/p&gt;

&lt;p&gt;Если вы арендуете базу в облаке, но исчерпали бюджет, возможны десятки вариантов
переезда. Каждый провайдер предлагает услугу забора данных у
конкурента. Предоставьте доступ к базе, и другой провайдер клонирует ее
себе. Так выживают стартапы: первый год они арендуют Postgres в AWS со льготным
периодом. Как только он заканчивается, переезжают со скидкой в Google Cloud,
потом – в Azure и так далее.&lt;/p&gt;

&lt;p&gt;Для Postgres написано множество утилит и расширений. Ваш лучший друг – утилита
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;psql&lt;/code&gt;, интерактивная оболочка для базы данных. Ее возможности крайне обширны:
запросы, импорт и экспорт таблиц, просмотр метаданных, переменные и
псевдонимы. Программы PGAdmin, DBeaver, DataGrip и другие предлагают графический
интерфейс. PGAdmin запускается в браузере и не требует локальной установки.&lt;/p&gt;

&lt;p&gt;Postgresql — база данных с большим объемом учебных материалов. На этом поле с
ней конкурирует разве что MySQL. Документация на 3000 страниц охватывает все
тонкости Postgresql, в том числе низкоуровневые. Компания Postgres Pro
&lt;a href=&quot;https://postgrespro.ru/education/books&quot;&gt;выпускает книги&lt;/a&gt; на русском языке для читателей разного уровня. Все
материалы доступны бесплатно и без регистрации. К книгам прилагаются файлы с
запросами, чтобы не набирать их вручную. В вашем распоряжении учебная база в
нескольких вариантах (малого, среднего, большого, гигантского).&lt;/p&gt;

&lt;h2 id=&quot;тайное-преимущество-sql&quot;&gt;Тайное преимущество SQL&lt;/h2&gt;

&lt;p&gt;Выбирая хранилище данных, имейте в виду: SQL знают не только программисты.&lt;/p&gt;

&lt;p&gt;Сегодня почти любая должность, связанная с цифрами и финансами, подразумевает
знание SQL. Сотрудники банков и телекомов работают в OLAP-системах, которые
агрегируют данные из многих источников. Каждая из этих программ предлагает свой
диалект SQL, и сотрудники изучают их на внутренних курсах. PostgreSQL становится
еще одним диалектом, который учится в короткое время.&lt;/p&gt;

&lt;p&gt;Автор проводил внутренние вебинары на тему как пользоваться Postgres и
PGAdmin. Вопреки ожиданиям, у сотрудников было мало вопросов, и всем было все
понятно. Позже автор видел, как сотрудники писали сложные запросы и передавали
знания друг другу.&lt;/p&gt;

&lt;p&gt;В крупных фирмах SQL становится связующим звеном между бизнесом и
программистами. Он помогает перевести бизнес-жаргон в технические
термины. Например, если аналитик понимает SQL, он объяснит, что значит “дейли
лимиты с апрувом без кепэсити” – какие таблицы и фильтры имеются в виду. Иногда
аналитики пишут сложные запросы, не привлекая программистов, и тем самым берегут
их время.&lt;/p&gt;

&lt;p&gt;Обмен знаниями усложняется, если данные хранятся в системе, отличной от
SQL. Придется думать о том, как предоставить доступ аналитикам. Скорее всего,
понадобятся скрипты, которые переносят данные в реляционную базу. Попытки
переучить сотрудников на что-то иное могут встретить саботаж.&lt;/p&gt;

&lt;p&gt;В некоторых NoSQL-базах наблюдается интересная вещь: их заказчики продавливают
SQL. Например, в Datomic, основанный на языке Datalog, со временем добавили
&lt;a href=&quot;https://docs.datomic.com/analytics/analytics-cli.html&quot;&gt;диалект SQL&lt;/a&gt; – пусть не такой мощный, как в Postgres, но удобный
для аналитиков. Базу XTDB сделали совместимой с протоколом PG Wire, чтобы к ней
можно было обратиться из любого клиента для Postgres.&lt;/p&gt;

&lt;p&gt;Решения, которые противопоставляют себя SQL, все-таки вынуждены с ним
считаться. Это подтверждает тезис: данные нужны всем, а не только
программистам. От того, насколько легко обратиться к данным, зависят процессы и
коммуникации. Опытный программист должен думать не только о себе, но и коллегах,
в том числе тех, кто не умеет программировать. Порой именно реляционная база
оказывается тем решением, которое удобно всем.&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;Теперь когда со вступлениями окончено, перейдем к практической части:
познакомимся с возможностями JSON в Postgres.&lt;/p&gt;
</description>
        <pubDate>Sun, 25 Jan 2026 00:00:00 +0000</pubDate>
        <link>https://grishaev.me/pg-book-json-ch01/</link>
        <guid isPermaLink="true">https://grishaev.me/pg-book-json-ch01/</guid>
        
        <category>postgres</category>
        
        <category>json</category>
        
        <category>book</category>
        
        <category>sql</category>
        
        
      </item>
    
      <item>
        <title>Совет дня №15</title>
        <description>&lt;p&gt;Пагинация, продолжение.&lt;/p&gt;

&lt;p&gt;Вернемся к случаю, когда нужна пагинация по убыванию времени &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;(created_at,
updated_at)&lt;/code&gt; и так далее. Этот критерий встречается так часто, что для него есть
лазейка.&lt;/p&gt;

&lt;p&gt;В прошлый раз мы использовали кортеж &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;(created_at, id)&lt;/code&gt;. Идея в том, что
поскольку &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;created_at&lt;/code&gt; не уникален, он не дает точного положения в таблице. Но
так как id уникален, их комбинация – тоже уникальна.&lt;/p&gt;

&lt;p&gt;От кортежа можно избавиться, если в качестве ID используется UUID v7. Дело в
том, что v7 совмещает в себе эти два свойства: время и уникальность. Существуют
разные подвиды UUID v7, но не зависимо от них пагинация будет:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;точно попадать в границы;&lt;/li&gt;
  &lt;li&gt;использовать btree индекс.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Итак, если у вас API и нужна пагинация, ваши варианты:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;
    &lt;p&gt;limit и offset; убедитесь, что offset не превышает какого-то разумного числа,
например тысячи. Иначе вас будут парсить.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;keyset: комбинация полей &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;(some_field + id)&lt;/code&gt; для уникальности. Требует
отдельного индекса.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;UUIDv7 – если требуется пагинация по &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;created_at&lt;/code&gt;. Это частый случай, поэтому
рассмотрите его.&lt;/p&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Перейдем к пагинации, которая используется не в API, а для служебных
нужд. Например, в миграциях, переносе данных и так далее. Нужно обойти огромную
таблицу, при этом выгрузить ее в память нельзя – слишком большая (например, 100
миллионов записей).&lt;/p&gt;

&lt;p&gt;Один из способов – использовать курсор и FETCH API. Апишка у него довольно
простая:&lt;/p&gt;

&lt;div class=&quot;language-sql highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;BEGIN&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;DECLARE&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;cur_foo&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;CURSOR&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;FOR&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;SELECT&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;FROM&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;items&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;FETCH&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;FORWARD&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;100&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;FROM&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;cur_foo&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;FETCH&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;FORWARD&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;100&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;FROM&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;cur_foo&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;FETCH&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;FORWARD&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;100&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;FROM&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;cur_foo&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;...&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;CLOSE&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;cur_foo&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;COMMIT&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Объявляем курсор, затем в цикле вытягиваем по 100 записей, пока результат не
пустой. В конце закрываем курсор.&lt;/p&gt;

&lt;p&gt;Казалось бы, вот он – святой Грааль. Но беда в том, что курсор требует
транзакции. В момент открытия он запоминает текущий снимок (номер транзакции) и
просматривает записи, версия которых не превышает его. Чтобы пользоваться
курсором, нужно держать транзакцию в течение всего цикла. Для больших таблиц это
дорого, поэтому подходит только для всяких ночных скриптов.&lt;/p&gt;

&lt;p&gt;Кроме того, курсор привязан к конкретному соединению с БД. Использовать их в
HTTP API невозможно.&lt;/p&gt;

&lt;p&gt;Другой способ обойти большую таблицу – сдампить ее в файл при помощи COPY
(см. &lt;a href=&quot;/tip-006-pg/&quot;&gt;прошлый совет&lt;/a&gt;). Чтобы таблица не заняла весь диск, ее
сжимают в gzip. В результате у вас оказывается файл &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;my_table.gzip&lt;/code&gt;, вы
отрываете его и спокойно парсите. Идея в том, чтобы забрать данные из базы как
можно скорее и потом не мучить ее пагинацией. Если скрипт упадет, не придется
насиловать базу снова – у вас уже есть файл.&lt;/p&gt;

&lt;p&gt;Третий способ – использовать драйвер, который позволяет обрабатывать записи в
полете. Например, мой &lt;a href=&quot;https://github.com/igrishaev/pg2&quot;&gt;pg2&lt;/a&gt;. Функция &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;execute&lt;/code&gt; принимает запрос и всякие
опции. Среди прочих можно передать редьюсер – функцию трех тел:&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;(fn
  ([]
   (make-acc ...))
  ([acc row]
   (conj acc row))
  ([acc]
   (finalize acc)))
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Тело без аргументов – инициатор аккумулятора (может быть мутабельным), тело с
одним аргументом – финализатор аккумулятора, с двумя – приращение записи к
аккумулятору. Из коробки доступны разные редьюсеры (см документацию). Легко
написать свой, который будет отправлять каждую строку в сеть, в асинхронный
канал и так далее. И все это – в полете, то есть по мере получения сообщений от
Postgres, без исчерпания всей памяти.&lt;/p&gt;

&lt;p&gt;Полагаю, это все, что можно сказать про пагинацию.&lt;/p&gt;
</description>
        <pubDate>Fri, 23 Jan 2026 00:00:00 +0000</pubDate>
        <link>https://grishaev.me/tip-015-pg/</link>
        <guid isPermaLink="true">https://grishaev.me/tip-015-pg/</guid>
        
        <category>postgres</category>
        
        <category>sql</category>
        
        
      </item>
    
      <item>
        <title>Совет дня №14</title>
        <description>&lt;p&gt;Итак, пагинация. Прежде всего, совет такой: если можете избежать пагинации,
сделайте это. Пагинация — это состояние и его проброс. Лишние заморочки,
пространство для багов.&lt;/p&gt;

&lt;p&gt;Пример, когда пагинации можно избежать — поиск. Как правило, релевантность
поиска резко снижается с продвижением по выдаче. Условно, первые 10 позиций
точные, еще десять — более-менее, остальное — просто чтобы заполнить
выдачу. Вспомните, как давно вы были на пятой странице Гугла? Если нужной
информации нет, лучше отправить другой запрос, чем скроллить страницы. Поэтому
договоритесь: в поиске выдаем 50-100 позиций и никаких пагинаций. Живые люди не
будут ей пользоваться, а вы только откроете ворота различным ботам и парсерам.&lt;/p&gt;

&lt;p&gt;Конечно, иногда пагинация необходима. Простой способ ее добавить — это выражения
LIMIT и OFFSET. Дешево и сердито, но с недостатками. Во-первых, на больших
смещениях выборка линейно замедляется. Это все равно что перематывать
аудиокассету каждый раз с начала. В моей таблице миллион документов, и выражение&lt;/p&gt;

&lt;div class=&quot;language-sql highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;select&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;table&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;limit&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;offset&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;500000&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;занимает 7 секунд. Целая вечность!&lt;/p&gt;

&lt;p&gt;Второй минус — LIMIT-OFFSET неконсистентны. В перерывах между запросами может
добавиться новая запись, и произойдет наслоение. Я видел такое много раз:
листаешь первую страницу, переходишь на вторую и видишь наверху заголовок,
который был внизу первой страницы.&lt;/p&gt;

&lt;p&gt;Дело в том, что пока я мотал первую страницу, добавили новую статью, и окно
сместилось. Для новостного сайта это не страшно, но представьте, что у нас
аналог Твиттера. Пока мы читали первые 20 твитов, кто-то наспамил еще
двадцать. В результате вторая страница может полностью состоять из твитов,
которые мы только что видели на первой! Это никуда не годится.&lt;/p&gt;

&lt;p&gt;Чтобы этого избежать, используют пагинацию по уникальному btree-индексу. Такой
индекс однозначно определяет позицию в таблице: новые записи не сместят окно. У
каждой таблицы есть такой индекс — это первичный ключ (id). Если логика
позволяет листать по id, этим нужно воспользоваться. Пример:&lt;/p&gt;

&lt;div class=&quot;language-sql highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;select&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;items&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;where&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;id&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;?&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;order&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;by&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;id&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;desc&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;limit&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;100&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Id&lt;/code&gt; последней записи запоминается и передается в следующий запрос. Продолжаем
до тех пор, пока выборка не пустая.&lt;/p&gt;

&lt;p&gt;Как быть, если id случаен, и требуется сортировка по дате создания? Задача
усложняется, потому что дата не уникальна. Записи могут быть вставлены импортом
и поэтому иметь одну дату. Граница окна пагинации может попасть на серию записей
с одинаковой датой. Если запомнить дату последней записи и выполнить запрос&lt;/p&gt;

&lt;div class=&quot;language-sql highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;select&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;items&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;where&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;created_at&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;?&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;order&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;by&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;created_at&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;desc&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;limit&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;100&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;, мы пропустим записи, которые не показали.&lt;/p&gt;

&lt;p&gt;Решение в следующем: уникальный атрибут + неуникальный дают уникальное комбо. В
самом деле: если поле &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;created_at&lt;/code&gt; не уникальное, то пара &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;(created_at, id)&lt;/code&gt; —
уникальная. Поэтому заводим составной индекс на пару &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;(created_at desc, id
desc)&lt;/code&gt; и листаем по нему:&lt;/p&gt;

&lt;div class=&quot;language-sql highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;select&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;items&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;where&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;created_at&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;?&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;?&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;order&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;by&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;created_at&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;desc&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;id&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;desc&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;limit&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;100&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Обратите внимание, мы сравниваем кортежи (они сравниваются поэлементно). Общий
принцип такой, что создается уникальный индекс на пару (поле, id). Он называется
keyset, потому что “набор ключей”.&lt;/p&gt;

&lt;p&gt;Каждая сотрировка (возраст, имя, зарплата) требует своей пары, поэтому
договоритесь о них заранее.&lt;/p&gt;
</description>
        <pubDate>Fri, 23 Jan 2026 00:00:00 +0000</pubDate>
        <link>https://grishaev.me/tip-014-pg/</link>
        <guid isPermaLink="true">https://grishaev.me/tip-014-pg/</guid>
        
        <category>postgres</category>
        
        <category>sql</category>
        
        
      </item>
    
      <item>
        <title>Совет дня №13</title>
        <description>&lt;p&gt;Работая с ORM, избегайте проблемы 1 + N. Это когда вы обращаетесь к ссылочным
полям, и база подтягивает сущности штучно, а не разом.&lt;/p&gt;

&lt;p&gt;Пример: магазин товаров, сущность &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Order&lt;/code&gt; ссылается на &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;User&lt;/code&gt; (кто заказал) и
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Item&lt;/code&gt; (что заказали). Модель выглядит так:&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Order&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;model&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Model&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;status&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;EnunField&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;active&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;cancelled&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;pending&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;...)&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;created_at&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;DateTimeFiled&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;now&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;True&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;user&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ForeignField&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;User&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;item&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ForeignField&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Item&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Типичная задача — вывести активные заказы с информацией о клиенте и
товаре. Разработчик делает так:&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;n&quot;&gt;orders&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Orders&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;filter&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;status&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;active&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; \
  &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;order_by&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;created_at&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;desc&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;all&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Затем он строит таблицу:&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;order&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;orders&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;print&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;order&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;order&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;user&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;order&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;item&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;title&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Что произойдет под капотом? Сначала выполнится запрос:&lt;/p&gt;

&lt;div class=&quot;language-sql highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;select&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;orders&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;where&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;status&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'active'&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;order&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;by&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;created_at&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;desc&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Тут все в порядке. Однако в цикле, когда происходит обращение к полям &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;user&lt;/code&gt; и
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;item&lt;/code&gt;, выполняются запросы &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;get-by-id&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;language-sql highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;select&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;users&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;where&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;id&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;select&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;users&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;where&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;id&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;...&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;select&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;items&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;where&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;id&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;100&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;select&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;items&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;where&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;id&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;200&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;В среднем запросов будет 1 + 2N. На практике в одном заказе может быть много
товаров, а кроме того, возможны подгрузки других сущностей. Скажем, товар хранит
ссылку на продавца. Если в таблице должен быть продавец, это будет еще +N
запросов.&lt;/p&gt;

&lt;p&gt;Именно для таких случаев нужны прошлые советы. Во-первых, запросы должны быть
видны в консоли, и разработчик обязан смотреть, что идет в базу. Во-вторых, на
эту логику должен быть тест, который считает запросы.&lt;/p&gt;

&lt;p&gt;Проблема 1 + N лечится разными способами. Первый — ORM может джойнить сущности,
то есть выполнить запрос:&lt;/p&gt;

&lt;div class=&quot;language-sql highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;select&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;orders&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;left&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;join&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;users&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;on&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;orders&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;user_id&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;user&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;id&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;left&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;join&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;items&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;on&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;order&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;item_id&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;item&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;id&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;where&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;status&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'active'&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;order&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;by&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;created_at&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;desc&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Такой джоин может нарушить пагинацию по limit/offset, но это страшно. Можно либо
не использовать ее вообще, либо взять пагинацию по keyset, либо заменить limit
выражением fetch.&lt;/p&gt;

&lt;p&gt;Другой способ — вытянуть записи по слоям на уровне приложения. Первый слой — это
orders. Как только происходит обращение к user, ORM собирает все user_id и
выполнят запрос&lt;/p&gt;

&lt;div class=&quot;language-sql highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;select&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;users&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;where&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;id&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;?&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;?&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;...)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;То же самое с &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;items&lt;/code&gt; — выгребаются уникальные &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;item_id&lt;/code&gt;, и по ним делается
запрос с IN.&lt;/p&gt;

&lt;p&gt;Некоторые ORM действуют тоньше. Если айдишников много, они выгребают смежные
сущности кусками по 10-30. Таким образом, если нужно пройти 100 заказов, мы
совершим 4-10 запросов, что не так страшно.&lt;/p&gt;

&lt;p&gt;Проблема 1 + N — настоящий бич ORM. Сколько подобных ошибок я исправил —
затрудняюсь припомнить (и конечно, совершил сам).&lt;/p&gt;

&lt;p&gt;На мой взгляд, в ORM должна быть опция: кидать исключение, если смежные записи
читаются штучно. Опция должна быть глобальной, чтобы раз и навсегда запретить
подобные вещи. Глядишь, новички стали бы лучше понимать, что вообще происходит.&lt;/p&gt;
</description>
        <pubDate>Fri, 23 Jan 2026 00:00:00 +0000</pubDate>
        <link>https://grishaev.me/tip-013-pg/</link>
        <guid isPermaLink="true">https://grishaev.me/tip-013-pg/</guid>
        
        <category>postgres</category>
        
        <category>sql</category>
        
        
      </item>
    
      <item>
        <title>Совет дня №12</title>
        <description>&lt;p&gt;Дополнение &lt;a href=&quot;/tip-011-pg/&quot;&gt;вчерашней заметки&lt;/a&gt; насчет запросов в базу. Смотреть
запросы, которые выполняются во время тестов – это хорошо, можно поймать много
кривых вещей. Но есть еще одна техника: считать запросы и проверять их
количество. Этим вы защищаете код от ситуации, когда небольшое изменение
накинуло +20 запросов. В ORM подобные вещи случаются часто. В основном они
вызваны проблемой 1 + N, о которой будет следующий совет.&lt;/p&gt;

&lt;p&gt;Фреймворки-гиганты предлагают встроенный метод подсчета запросов. Например, в
Django, если тестовый класс унаследован от &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;TransactionTestCase&lt;/code&gt;, доступен метод
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;assertNumQueries&lt;/code&gt;. В целом подобный тест выглядит так:&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;TestSomeFunc&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;TestCase&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;dеf&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;test_func&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;with&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;assertNumQueries&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;4&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;using&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;db1&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;some_func&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Если кто-то поправит логику, число запросов изменится, и тест не пройдет.&lt;/p&gt;

&lt;p&gt;Если запросов много (10 и больше), посмотрите лог и добавьте комментарий с
распределением запросов. Так логика будет понятна хотя бы в общих чертах:&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;# 1 auth
# 2 permission check
# 4 fetch data
# 2 update data
# 2 send notifications
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;В одном проекте выяснилось: удаление сущности порождало 400 с лишним
запросов. Разумеется, почти все они удалялись поштучно, потому что разработчики
не дружили с БД.&lt;/p&gt;

&lt;p&gt;Для Django написаны в том числе сторонние сборщики запросов, их легко
нагуглить. Полагаю, то же самое есть в Руби, Spring Boot и прочих
комбайнах. Подсчет легко сделать в Кложе: достаточно динамической переменной и
макроса.&lt;/p&gt;

&lt;p&gt;Не обязательно считать запросы во всех апишках: скажем, для &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;get-by-id&lt;/code&gt; подсчет
избыточен. Но удаление сущностей, сложные перемещения из одной таблицы в другую
– очень желательно. Все для этого есть, просто воспользуйтесь.&lt;/p&gt;
</description>
        <pubDate>Fri, 23 Jan 2026 00:00:00 +0000</pubDate>
        <link>https://grishaev.me/tip-012-pg/</link>
        <guid isPermaLink="true">https://grishaev.me/tip-012-pg/</guid>
        
        <category>postgres</category>
        
        <category>sql</category>
        
        
      </item>
    
  </channel>
</rss>
