<?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>Wed, 14 Oct 2026 15:25:14 +0000</pubDate>
    <lastBuildDate>Wed, 14 Oct 2026 15:25:14 +0000</lastBuildDate>
    <generator>Jekyll v4.2.0</generator>
    
      <item>
        <title>Don't be a meat proxy</title>
        <description>
&lt;p&gt;Коротенькая заметка о ребятах, которые на все отвечают выхлопом нейросети:
&lt;a href=&quot;https://gruhn.me/blog/2026-08-03/&quot;&gt;Don’t be a meat proxy&lt;/a&gt;. Хороша тем, что метко вводит новый термин: meat
proxy. Мясной посредник между человеком и нейросетью.&lt;/p&gt;

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

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

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

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

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

&lt;p&gt;Верю, что когда-то это поймут. Я подожду.&lt;/p&gt;
</description>
        <pubDate>Wed, 14 Oct 2026 00:00:00 +0000</pubDate>
        <link>https://grishaev.me/meat-proxy/</link>
        <guid isPermaLink="true">https://grishaev.me/meat-proxy/</guid>
        
        <category>life</category>
        
        <category>ai</category>
        
        
      </item>
    
      <item>
        <title>О Конфуции</title>
        <description>&lt;p&gt;Читаю Конфуция. Вот какой случай напомнил о нем.&lt;/p&gt;

&lt;p&gt;Жил в Америке ультра-патриот. Трамп, МАГА, все дела. Потом женился на русской
эмигрантке, и пара поехала в отпуск. Супруга как раз продлевала визу, и
оказалось, что старый документ уже недействителен, а новый еще не выдали.&lt;/p&gt;

&lt;p&gt;В аэропорту к паре подошли представители ICE – Immigration and Customs
Enforcement (иммиграционная и таможенная служба США). Тут важно понимать
контекст. Формально ICE борется с незаконной миграций, однако при Трампе штат
расширили на порядок, чтобы проводить облавы. На работу в ICE устраивается
отребье вроде бывших военных, таксистов и охранников. Мало того, что эти люди
озлоблены на всех; так еще государство дает им в руки летальное оружие, чтобы
задерживать мигрантов.&lt;/p&gt;

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

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

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

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

&lt;p&gt;При том, что примерно в 680 году ДО нашей эры (то есть 2700 лет назад) Конфуций
вывел формулу человеколюбия: не делай другим того, чего не желаешь
себе. Разумеется, и до Конфуция эту мысль высказывали в том или ином виде, но он
возвел ее в абсолют. На человеколюбии держится все учение Конфуция.&lt;/p&gt;

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

&lt;p&gt;Но когда в жернова попадает близкий человек, все меняется. Товарищ &lt;del&gt;Сталин&lt;/del&gt;
Трамп, произошла чудовищная ошибка…&lt;/p&gt;

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

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

&lt;p&gt;И так далее.&lt;/p&gt;

&lt;p&gt;Глядя на историю людей, иной раз поражаешься. Мысль высказали 2700 лет назад и с
тех пор повторяют во всех вариациях. Понять ее – это ведь не вырастить плавник
или третью руку: тут не нужны тысячелетия. Но судя по текущей обстановке, до
понимания формулы осталось еще очень и очень долго.&lt;/p&gt;
</description>
        <pubDate>Wed, 14 Oct 2026 00:00:00 +0000</pubDate>
        <link>https://grishaev.me/konfu/</link>
        <guid isPermaLink="true">https://grishaev.me/konfu/</guid>
        
        <category>life</category>
        
        <category>maga</category>
        
        
      </item>
    
      <item>
        <title>Чинить или отказаться</title>
        <description>&lt;p&gt;Когда что-то не работает, очевидное решение в том, чтобы починить. Второе
неочевидное – отказаться. Чем старше становлюсь, тем чаще отказываюсь от того,
что ломается. Разумеется, тезис слишком абстрактный и решается по ситуации. Но
часто отказ не рассматривают в принципе и чинят одно и то же годами. Не надо
так: отказ и пересмотр очень важны.&lt;/p&gt;
</description>
        <pubDate>Wed, 14 Oct 2026 00:00:00 +0000</pubDate>
        <link>https://grishaev.me/fix-or-ditch/</link>
        <guid isPermaLink="true">https://grishaev.me/fix-or-ditch/</guid>
        
        <category>life</category>
        
        
      </item>
    
      <item>
        <title>Повторный рендер</title>
        <description>&lt;p&gt;Почти на всех сайтах я замечаю повторный рендер. Это когда контент нарисовался,
а потом Реакт решил, что виртуальное дерево устарело. Поэтому вжух – и контент
рисуется еще раз.&lt;/p&gt;

&lt;p&gt;То же самое во многих мобильных и настольных приложениях вроде Slack или Teams —
которые только называются “приложениями”, а на самом деле оболочка Хрома или
WebView.&lt;/p&gt;

&lt;p&gt;Никто не считает это проблемой. Подумаешь, мигнул контент! В нулевые скорость
была низкая, верстали таблицами, и сайт собирался по 30 секунд. Бывало, уже
давно читаешь текст, и раз – что-то подгрузилось, подпрыгнуло, перестроилось. А
тут – всего-то мигнуло.&lt;/p&gt;

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

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

&lt;p&gt;Давайте договоримся: не важно что у вас под капотом, Реакт-Вуй или что
угодно. Контент не должен перерисовываться только потому, что ему так
показалось. А если эти ваши Реакты-Вуи не починить, то верстайте так, чтобы не
провоцировать повторный рендер.&lt;/p&gt;
</description>
        <pubDate>Wed, 14 Oct 2026 00:00:00 +0000</pubDate>
        <link>https://grishaev.me/double-render/</link>
        <guid isPermaLink="true">https://grishaev.me/double-render/</guid>
        
        <category>ui</category>
        
        
      </item>
    
      <item>
        <title>Confirmation</title>
        <description>&lt;p&gt;&lt;img src=&quot;/assets/static/aws/confirm-what/1.jpeg&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Подтверждать или нет?&lt;/p&gt;

&lt;p&gt;Кстати, лютейше бесят ребята, которые делают кнопку подтверждения слева. Это
неправильно. Продолжение действия должно быть справа, а слева — возврат или
отмена. Разумеется, к тем ребятам, которые не могут показать текст, это не
относится — у них еще все впереди.&lt;/p&gt;
</description>
        <pubDate>Wed, 14 Oct 2026 00:00:00 +0000</pubDate>
        <link>https://grishaev.me/confirm-what/</link>
        <guid isPermaLink="true">https://grishaev.me/confirm-what/</guid>
        
        <category>ui</category>
        
        <category>idiots</category>
        
        
      </item>
    
      <item>
        <title>Кложурный тулинг</title>
        <description>&lt;blockquote&gt;
  &lt;p&gt;Если я правильно понимаю ты писал (пишешь?) на питоне в том числе. Просто я
немного поглядываю что происходит в мире clоjure и вижу, люди как-то
справляются без linter-ов, typechecker-ов и без такого инструмента, как uv
(это менеджер интерпретатора, зависимостей проекта и виртуального окружения) в
мире python. Когда работаешь с чужим кодом и погружаешься в него, то такие
вещи очень сильно помогают. Возможно, в мире clоjure как-то можно обойтись без
них, но тогда мне интересно как?&lt;/p&gt;
&lt;/blockquote&gt;

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

&lt;p&gt;Если отвечать на вопросы буквально, то да: тулинг Кложи не дотягивает до
Питона. Для него созданы десятки линтеров и чекеров, а к каждому — еще столько
же плагинов. Есть Jetbrains с Pycharm, которая выводит типы и предлагает
автокомплит на любой чих. Программирование на Питоне в Pycharm стало как в Джаве
— все типы подхватываются, и возникает ощущение, что в языке строгая
типизация. Что касается uv и uvx, то я не застал их, но знаю, что это мощные
средства.&lt;/p&gt;

&lt;p&gt;До этого изобилия Кложе далеко как до Луны. IDE нет, есть только плагины к
редакторам. Я пользуюсь связкой Emacs + Cider, но понимаю, что освоить ее
новичку трудно. Автокомплит каждый настраивает как может. Проверку синтаксиса в
полете и прочие удобства — тоже. Централизованным решением вроде Jetbrains даже
не пахнет, и вряд ли оно когда-либо появится.&lt;/p&gt;

&lt;p&gt;Не все так плохо с линтерами. Есть весьма развитый clj-kondo, который
отлавливает много ошибок, например неверное число аргументов, необъявленные
переменные и так далее. Суммарно кложурных линтеров наберется с десяток, и если
использовать хотя бы пару из них, это решит некоторое число проблем. Также есть
форматтеры кода по определенным правилам.&lt;/p&gt;

&lt;p&gt;Существует кложурный LSP сервер (Language Server Protocol). Во многих редакторах
LSP-клиент идет из коробки (VS Code, Emacs), так что здесь наблюдается
развитие. С LSP у вас будет автокомплит, переход по коду и остальные базовые
вещи.&lt;/p&gt;

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

&lt;p&gt;Зато REPL позволяет выполнить код в редакторе и отловить подобные
случаи. Кстати, замечаю вот что: все больше джавистов топят за TDD – разработку
от тестов. Парни не понимают, что TDD – это прокисший REPL, своего рода костыль,
чтобы поскорее запустить код. В Джаве нет репла, но есть зеленая кнопочка Run,
которая появляется в Main- и Test-классах. Нажатие этой кнопочки – просто убогий
способ запустить код и ничего больше.&lt;/p&gt;

&lt;p&gt;REPL не отменяет тестов, их пишут – юнит-, интеграционные и так далее.&lt;/p&gt;

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

&lt;p&gt;Затем, чтобы выйти из зоны комфорта, увидеть разнообразие, попробовать себя в
чем-то другом. IDE и тулинг на все случаи жизни — это здорово, но нельзя на них
замыкаться. Не вся работа делается в IDE по подписке. Когда Рич и Кен делали
Unix и Си, разве были у них средства, аналогичные нашим? Они сами написали
линтеры, парсеры, чекеры и прочие обвязки. А как написали IDE без IDE?&lt;/p&gt;

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

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

&lt;p&gt;Так и здесь. Да, чего-то не хватает, но не развалитесь. Можно и на Кложе писать
хороший софт, зарабатывать и получать удовольствие. Да и не только на Кложе: я
бы с удовольствием взялся за коммерческий проект на Common Lisp или Cobol — да
на чем угодно! Нет тулинга? Не беда: либо обойдусь, либо напишу свой.&lt;/p&gt;

&lt;p&gt;С таким настроем легко жить и смотреть в будущее.&lt;/p&gt;
</description>
        <pubDate>Wed, 14 Oct 2026 00:00:00 +0000</pubDate>
        <link>https://grishaev.me/clj-tooling/</link>
        <guid isPermaLink="true">https://grishaev.me/clj-tooling/</guid>
        
        <category>programming</category>
        
        
      </item>
    
      <item>
        <title>On cowardness in Clojure code</title>
        <description>&lt;p&gt;Imagine you have a function accepting a value and doing something with it:&lt;/p&gt;

&lt;div class=&quot;language-clojure 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;k&quot;&gt;defn&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;double-number&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;n&quot;&gt;x&lt;/span&gt;&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;nb&quot;&gt;when&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;x&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;nb&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;x&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;c1&quot;&gt;;; or&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;k&quot;&gt;defn&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;get-user&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;n&quot;&gt;id&lt;/span&gt;&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;nb&quot;&gt;when&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;id&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;nf&quot;&gt;jdbc/execute!&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;*db*&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;s&quot;&gt;&quot;select...&quot;&lt;/span&gt;&lt;span class=&quot;w&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;w&quot;&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;A common pattern which comes all the time is to wrap the entire body with a
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;(when id ...)&lt;/code&gt; form. You don’t want to process nil values so it’s safer to
protect yourself against NPEs. Without &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;(when ...)&lt;/code&gt;, a nil value can ruin the
entire pipeline, cause a null pointer error, trigger DB queries in vain and so
on. These all sound reasonable, yet I’ve got my own term to describe such a kind
of code: “coward style”.&lt;/p&gt;

&lt;p&gt;A person who is wrapping the whole function with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;(when)&lt;/code&gt; isn’t getting one
thing. &lt;strong&gt;If a function had been given nil, it should have never been called
instead&lt;/strong&gt;. Clojure provides a number of macros to call a function conditionally
depending on arguments, for example:&lt;/p&gt;

&lt;div class=&quot;language-clojure 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;nf&quot;&gt;some-&amp;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;nf&quot;&gt;get-user-id&lt;/span&gt;&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;nf&quot;&gt;get-user&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;Should &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;(get-user-id)&lt;/code&gt; return nil, the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;(get-user nil)&lt;/code&gt; form never gets
called. The same applies to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;(cond-&amp;gt;)&lt;/code&gt; and other macros that build an execution
form conditionally.&lt;/p&gt;

&lt;p&gt;In other words: a function must not check its input parameters for nils. But
those people who call this function must.&lt;/p&gt;

&lt;p&gt;Now let me explain the “coward style” I mentioned before. It’s when people write
like this:&lt;/p&gt;

&lt;div class=&quot;language-clojure 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;k&quot;&gt;defn&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;double-number&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;n&quot;&gt;x&lt;/span&gt;&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;nb&quot;&gt;when&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;x&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;nb&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;x&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;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;I don’t know what this code tells you, but to me, it’s clearly this: &lt;em&gt;“Guys, I
don’t want any problems. I don’t want any exceptions to be raised. If you supply
me with a number, I’ll double it but won’t do anything if you pass nil. I cannot
process it but won’t argue on you. Let’s keep it all quiet. Deal?”&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;This is a speech of a typical coward: I don’t want problems. I don’t want stack
traces. I don’t want alerts and investigations. Let’s be quiet. The job is
half-way done in fact as the function works partially. It won’t tell you when
something is not quite right.&lt;/p&gt;

&lt;p&gt;I’ve seen plenty of computation chains like this:&lt;/p&gt;

&lt;div class=&quot;language-clojure 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;nb&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;some-param&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;nf&quot;&gt;parse-param&lt;/span&gt;&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;nf&quot;&gt;pre-process-param&lt;/span&gt;&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;nf&quot;&gt;get-use-id&lt;/span&gt;&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;nf&quot;&gt;get-user-by-id&lt;/span&gt;&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;nf&quot;&gt;send-user-somewhere&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;Now imagine that &lt;strong&gt;every&lt;/strong&gt; function starts with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;(when ...)&lt;/code&gt;, and the final
function crashes with NPE. It will be quite challenging to find who is
guilty. These functions are real cowards: nobody wants to take blame. &lt;em&gt;“I got
nil → I returned nil. Not my business. I washed my hands”.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Thus, stop writing functions starting with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;(when ...)&lt;/code&gt;. If a function silently
swallows a nil value doing nothing, sooner or later you’ll pay for that. Or your
teammates will.&lt;/p&gt;

&lt;p&gt;There is still a way though to protect yourself against nils which I like a
lot. Use built-in pre- and post conditions:&lt;/p&gt;

&lt;div class=&quot;language-clojure 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;k&quot;&gt;defn&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;double-number&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;n&quot;&gt;x&lt;/span&gt;&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;no&quot;&gt;:pre&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;nf&quot;&gt;number?&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;x&lt;/span&gt;&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;nb&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;x&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;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Now if you pass nil, you’ll get a clear error&lt;/p&gt;

&lt;div class=&quot;language-clojure 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;nf&quot;&gt;double-number&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;nil&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;

&lt;/span&gt;&lt;span class=&quot;c1&quot;&gt;;; Execution error (AssertionError) at … (REPL:211).&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;c1&quot;&gt;;; Assert failed: (number? x)&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;Preconditions help a lot with guessing types. Above, they clearly say &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;x&lt;/code&gt; must
be a number and nothing else. In addition to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;:pre&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;:post&lt;/code&gt; forms, the
standard &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;(assert ...)&lt;/code&gt; form might help in the middle of a function to interrupt
execution when you know it makes no sense to go on with a weird value.&lt;/p&gt;

&lt;p&gt;Keen mind that &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;:pre&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;:post&lt;/code&gt;, and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;assert&lt;/code&gt; forms rely on the global &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;*assert*&lt;/code&gt;
variable. It’s a good practice to rely on assertions a lot but wipe them off on
production as they slow down the code. When baking an uberjar, set
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;clojure.core/*assert*&lt;/code&gt; to false. If it’s ClojureScript with a shadow compiler,
pass &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;{:elide-asserts true}&lt;/code&gt; into the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;:compiler-options&lt;/code&gt; map for a production
release.&lt;/p&gt;

&lt;p&gt;I agree that pre/post and assertions take lines of code, and sometimes they make
code a bit noisy. But they will save you hours of debugging. Don’t be a coward
whose main goal is to avoid exceptions. Don’t hide weird things. Be simple and
explicit, and let your code express these two qualities.&lt;/p&gt;
</description>
        <pubDate>Thu, 01 Oct 2026 00:00:00 +0000</pubDate>
        <link>https://grishaev.me/on-clojure-cowardness/</link>
        <guid isPermaLink="true">https://grishaev.me/on-clojure-cowardness/</guid>
        
        <category>programming</category>
        
        <category>clojure</category>
        
        
      </item>
    
      <item>
        <title>Глава 10. Релевантный поиск</title>
        <description>&lt;h2 id=&quot;главы&quot;&gt;Главы&lt;/h2&gt;

&lt;ol&gt;
  &lt;li&gt;&lt;a href=&quot;/pg-book-json-ch01/&quot;&gt;Введение в документы&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;/pg-book-json-ch02/&quot;&gt;Базовые возможности JSON&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;/pg-book-json-ch03/&quot;&gt;JSON в таблицах&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;/pg-book-json-ch04/&quot;&gt;Индексирование JSON&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;/pg-book-json-ch05/&quot;&gt;Ограничения в документах&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;/pg-book-json-ch06/&quot;&gt;Язык путей JSONPath&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;/pg-book-json-ch07/&quot;&gt;Отчеты, функции, расписание&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;/pg-book-json-ch08&quot;&gt;Функции на языке Python&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;/pg-book-json-ch09/&quot;&gt;Версии и патчи&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;/pg-book-json-ch10/&quot;&gt;Релевантный поиск&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;

    Содержание

&lt;/h2&gt;

&lt;ul id=&quot;toc-item-pg-book-json-ch10&quot;&gt;
  &lt;li&gt;&lt;a href=&quot;#главы&quot; id=&quot;toc-item-pg-book-json-ch10-главы&quot;&gt;Главы&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#поиск-в-конкатенации-полей&quot; id=&quot;toc-item-pg-book-json-ch10-поиск-в-конкатенации-полей&quot;&gt;Поиск в конкатенации полей&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#слоеный-поиск&quot; id=&quot;toc-item-pg-book-json-ch10-слоеный-поиск&quot;&gt;Слоеный поиск&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#замечания-к-union&quot; id=&quot;toc-item-pg-book-json-ch10-замечания-к-union&quot;&gt;Замечания к UNION&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#последующая-обработка&quot; id=&quot;toc-item-pg-book-json-ch10-последующая-обработка&quot;&gt;Последующая обработка&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#замена-union-на-full-join&quot; id=&quot;toc-item-pg-book-json-ch10-замена-union-на-full-join&quot;&gt;Замена UNION на FULL JOIN&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#коротко-о-mapreduce&quot; id=&quot;toc-item-pg-book-json-ch10-коротко-о-mapreduce&quot;&gt;Коротко о MapReduce&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#поиск-в-исторической-таблице&quot; id=&quot;toc-item-pg-book-json-ch10-поиск-в-исторической-таблице&quot;&gt;Поиск в исторической таблице&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#о-быстродействии&quot; id=&quot;toc-item-pg-book-json-ch10-о-быстродействии&quot;&gt;О быстродействии&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#дальнейшие-улучшения&quot; id=&quot;toc-item-pg-book-json-ch10-дальнейшие-улучшения&quot;&gt;Дальнейшие улучшения&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#страничная-навигация&quot; id=&quot;toc-item-pg-book-json-ch10-страничная-навигация&quot;&gt;Страничная навигация&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#полнотекстовый-поиск&quot; id=&quot;toc-item-pg-book-json-ch10-полнотекстовый-поиск&quot;&gt;Полнотекстовый поиск&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#объект-запроса&quot; id=&quot;toc-item-pg-book-json-ch10-объект-запроса&quot;&gt;Объект запроса&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#определение-и-хранение-языка&quot; id=&quot;toc-item-pg-book-json-ch10-определение-и-хранение-языка&quot;&gt;Определение и хранение языка&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#полнотекстовый-слой-в-union&quot; id=&quot;toc-item-pg-book-json-ch10-полнотекстовый-слой-в-union&quot;&gt;Полнотекстовый слой в UNION&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#оптимизации-и-доработки&quot; id=&quot;toc-item-pg-book-json-ch10-оптимизации-и-доработки&quot;&gt;Оптимизации и доработки&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#заключение&quot; id=&quot;toc-item-pg-book-json-ch10-заключение&quot;&gt;Заключение&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

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

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

&lt;!-- more --&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;┌───────────────────────────────────────────┐
│                                           │
│   Номер заявки                            │
│  ┌──────────────────────────┐             │
│  │12345                     │             │
│  └──────────────────────────┘             │
│                                           │
│   Название организации                    │
│  ┌──────────────────────────┐             │
│  │Acme                      │             │
│  └──────────────────────────┘             │
│                                           │
│   Сотрудник                               │
│  ┌──────────────────────────┐             │
│  │Maria                     │             │
│  └──────────────────────────┘             │
│                                           │
│   Комментарий                             │
│  ┌──────────────────────────┐             │
│  │reconciliation            │             │
│  └──────────────────────────┘             │
│                                           │
│  ┏━━━━━━━━━━━━━━┓                         │
│  ┃    Найти     ┃                         │
│  ┗━━━━━━━━━━━━━━┛                         │
│                                           │
└───────────────────────────────────────────┘
&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;n&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;doc&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;from&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;applications&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;doc&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;#&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'{application_id}'&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;s1&quot;&gt;'12345'&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;and&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;doc&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;#&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'{organization,short_name}'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;ilike&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'%acme%'&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;and&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;doc&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;#&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'{created_by,name}'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;         &lt;span class=&quot;k&quot;&gt;ilike&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'%maria%'&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;and&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;doc&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;#&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'{comment}'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;                 &lt;span class=&quot;k&quot;&gt;ilike&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'%reconciliation%'&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;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;/p&gt;

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

&lt;h2 id=&quot;поиск-в-конкатенации-полей&quot;&gt;Поиск в конкатенации полей&lt;/h2&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;┌─────────────────────────────────────────────────────────────┐
│                                                             │
│  Номер заявки, ФИО сотрудника, комментарий (см. справку)    │
│ ┌──────────────────────────────────────────────┐┏━━━━━━━━━┓ │
│ │12345                                         │┃  Поиск  ┃ │
│ └──────────────────────────────────────────────┘┗━━━━━━━━━┛ │
│                                                             │
└─────────────────────────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Это выражение проверяется для многих полей с разной логикой. Если пользователь
ввел 123456, мы должны показать заявки, где:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;либо номер заявки включает 123456;&lt;/li&gt;
  &lt;li&gt;либо название организации, подавшей заявку, включает 123456;&lt;/li&gt;
  &lt;li&gt;либо над заявкой работал пользователь, чье имя содержит 1234565;&lt;/li&gt;
  &lt;li&gt;либо в комментарии встречается такая последовательность.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Очевиден шаблон: каждое условие – отбор по определенному полю; условия
объединяются оператором &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;OR&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;doc&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;from&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;applications&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;doc&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;#&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'{application_id}'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;          &lt;span class=&quot;k&quot;&gt;ilike&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'%12398%'&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;or&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;doc&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;#&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'{organization,short_name}'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;ilike&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'%12398%'&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;or&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;doc&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;#&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'{created_by,name}'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;         &lt;span class=&quot;k&quot;&gt;ilike&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'%12398%'&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;or&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;doc&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;#&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'{comment}'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;                 &lt;span class=&quot;k&quot;&gt;ilike&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'%12398%'&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;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Для каждого поля и оператора создан индекс: btree для равенства или триграммный
для вхождения. Если требуется пятый критерий, в часть WHERE добавляется новое
поле, и для него строится индекс.&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;or&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;replace&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;immutable_concat_ws&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&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;variadic&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;returns&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;text&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;language&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;internal&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;immutable&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;parallel&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;safe&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;AS&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'text_concat_ws'&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;or&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;replace&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;application_search_pattern&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;doc&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;returns&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;text&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;language&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;sql&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;immutable&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;strict&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;parallel&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;safe&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;immutable_concat_ws&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;s1&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;n&quot;&gt;doc&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;#&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'{application_id}'&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;doc&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;#&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'{organization,short_name}'&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;doc&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;#&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'{created_by,name}'&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;doc&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;#&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'{comment}'&lt;/span&gt;&lt;span class=&quot;p&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;/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;if&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;not&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;exists&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;idx_applications_application_trgm_pattern&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;on&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;applications&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;using&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;gin&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;((&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;application_search_pattern&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;doc&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;gin_trgm_ops&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;n&quot;&gt;id&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;applications&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;where&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;application_search_pattern&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;doc&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;ilike&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'%12345%'&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;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;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ilike&lt;/code&gt; сообщает, что совпадение
нашлось, но в какой именно части – неизвестно. Наоборот, нам бы хотелось это
знать, потому что поля следуют по убыванию важности. Чем ближе к началу нашлось
совпадение, тем выше должен быть документ в выдаче.&lt;/p&gt;

&lt;p&gt;Предположим, для двух документов функция &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;application_search_pattern&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;code  org name         user name     comment
61312 Global Corp      Suzanne Smith reconciliation started ref #991015123
10151 General Acme Inc John Brown    some long comment about the app
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

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

&lt;h2 id=&quot;слоеный-поиск&quot;&gt;Слоеный поиск&lt;/h2&gt;

&lt;p&gt;Рассмотрим технику, которая определяет приоритет точнее. Идея в следующем:
документы разбивают на группы (слои), которым назначают им число – показатель
релевантности (ранг). В нашем поиске выделим следующие группы:&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;комментарий содержит значение.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Мы не случайно разделяем точность и вхождение. Во-первых, если пользователь ищет
заявку 999, будет странно обнаружить ее на десятом месте после 9991, 3999 и
других чисел с тремя девятками. Во-вторых, точное совпадение опирается на индекс
btree, который быстрее триграммного.&lt;/p&gt;

&lt;p&gt;Назначим группам нарастающие числа: 10, 20, 30 и так далее. Чем меньше этот
показатель, тем раньше в выдаче идет строка. Интервал 10 подобран с умыслом: с
ним между группами можно вставить еще одну, например с рангом 15. Если же
интервал равен единице, придется сдвигать остальные ранги или переходить к
вещественным числам: 1.0, 1.5, 2.0.&lt;/p&gt;

&lt;p&gt;Для каждой группы пишут отдельный 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;n&quot;&gt;id&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;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;rank&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;applications&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;doc&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;#&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'{application_id}'&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;s1&quot;&gt;'12398'&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                  │ rank │
├──────────────────────────────────────┼──────┤
│ 00000000-0000-0000-0000-000000012398 │   10 │
└──────────────────────────────────────┴──────┘
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Чтобы поиск по равенству был быстрым, понадобится btree-индекс. А поскольку
номер заявки уникален, добавим директиву &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;unique&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;unique&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;index&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;idx_doc_application_id&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;on&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;applications&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;((&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;doc&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;#&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'{application_id}'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;nulls&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;not&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;distinct&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;n&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;20&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;rank&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;applications&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;doc&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;#&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'{organization.code}'&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;s1&quot;&gt;'12398'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Конкретно для 12398 результат будет пустым, потому что в базе нет организаций с
таким кодом.&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;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;mi&quot;&gt;30&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;rank&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;applications&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;doc&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;#&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'{application_id}'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;ilike&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'%12398%'&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                  │ rank │
├──────────────────────────────────────┼──────┤
│ 00000000-0000-0000-0000-000000912398 │   30 │
│ 00000000-0000-0000-0000-000000012398 │   30 │
│ 00000000-0000-0000-0000-000000112398 │   30 │
│ 00000000-0000-0000-0000-000000123980 │   30 │
│ 00000000-0000-0000-0000-000000123981 │   30 │
│ 00000000-0000-0000-0000-000000123982 │   30 │
│ 00000000-0000-0000-0000-000000123983 │   30 │
│ 00000000-0000-0000-0000-000000123984 │   30 │
│ 00000000-0000-0000-0000-000000123985 │   30 │
&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;n&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;40&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;rank&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;applications&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;doc&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;#&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'{organization.code}'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;ilike&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'%12398%'&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;n&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;50&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;rank&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;applications&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;doc&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;#&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'{comment}'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;ilike&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'%12398%'&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                  │ rank │
├──────────────────────────────────────┼──────┤
│ 00000000-0000-0000-0000-000000912398 │   50 │
│ 00000000-0000-0000-0000-000000012398 │   50 │
│ 00000000-0000-0000-0000-000000112398 │   50 │
│ 00000000-0000-0000-0000-000000123980 │   50 │
│ 00000000-0000-0000-0000-000000123981 │   50 │
│ 00000000-0000-0000-0000-000000123982 │   50 │
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

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

&lt;p&gt;Имея пять отдельных запросов, объединим их оператором UNION ALL. Слово ALL
необходимо, чтобы Postgres не пытался удалить дубликаты строк – их все равно не
будет. Даже если один и тот же документ встречается в разных выборках, у них
будет разный ранг, и записи будут отличаться.&lt;/p&gt;

&lt;p&gt;Предположим, пользователь ввел строку 12398 нажал ввод. Вот каким окажется
промежуточный запрос:&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;mi&quot;&gt;10&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;rank&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;applications&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;doc&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;#&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'{application_id}'&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;s1&quot;&gt;'12398'&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;union&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;all&lt;/span&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;mi&quot;&gt;20&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;rank&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;applications&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;doc&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;#&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'{organization.code}'&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;s1&quot;&gt;'12398'&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;union&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;all&lt;/span&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;mi&quot;&gt;30&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;rank&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;applications&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;doc&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;#&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'{application_id}'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;ilike&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'%12398%'&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;union&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;all&lt;/span&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;mi&quot;&gt;40&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;rank&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;applications&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;doc&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;#&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'{organization.code}'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;ilike&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'%12398%'&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;union&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;all&lt;/span&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;mi&quot;&gt;50&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;rank&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;applications&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;doc&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;#&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'{comment}'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;ilike&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'%12398%'&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;limit&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;Если выполнить этот запрос, получим таблицу ниже. Изучите ее, а в следующем
параграфе мы обсудим, какие доработки ей требуются.&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                  │ rank │
├──────────────────────────────────────┼──────┤
│ 00000000-0000-0000-0000-000000012398 │   10 │
│ 00000000-0000-0000-0000-000000812398 │   30 │
│ 00000000-0000-0000-0000-000000712398 │   30 │
│ 00000000-0000-0000-0000-000000312398 │   30 │
│ 00000000-0000-0000-0000-000000912398 │   30 │
│ 00000000-0000-0000-0000-000000012398 │   30 │
│ 00000000-0000-0000-0000-000000112398 │   30 │
│ 00000000-0000-0000-0000-000000123980 │   30 │
│ 00000000-0000-0000-0000-000000123981 │   30 │
│ 00000000-0000-0000-0000-000000123982 │   30 │
│ 00000000-0000-0000-0000-000000123983 │   30 │
│ 00000000-0000-0000-0000-000000123984 │   30 │
│ 00000000-0000-0000-0000-000000123985 │   30 │
│ 00000000-0000-0000-0000-000000123986 │   30 │
│ 00000000-0000-0000-0000-000000123987 │   30 │
│ 00000000-0000-0000-0000-000000123988 │   30 │
│ 00000000-0000-0000-0000-000000123989 │   30 │
│ 00000000-0000-0000-0000-000000212398 │   30 │
│ 00000000-0000-0000-0000-000000412398 │   30 │
│ 00000000-0000-0000-0000-000000512398 │   30 │
│ 00000000-0000-0000-0000-000000612398 │   30 │
│ 00000000-0000-0000-0000-000000812398 │   50 │
│ 00000000-0000-0000-0000-000000012398 │   50 │
│ 00000000-0000-0000-0000-000000112398 │   50 │
│ 00000000-0000-0000-0000-000000123980 │   50 │
│ 00000000-0000-0000-0000-000000123981 │   50 │
│ 00000000-0000-0000-0000-000000123982 │   50 │
│ 00000000-0000-0000-0000-000000123983 │   50 │
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;замечания-к-union&quot;&gt;Замечания к UNION&lt;/h2&gt;

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

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

&lt;p&gt;Третье важное замечание – один и тот же документ может быть в разных слоях. Выше
мы привели пример, где последовательность 12345 встречалась в коде заявки и
комментарии. Если присмотреться, наша выборка тоже содержит повторы: документ
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;00000000-0000-0000-0000-000000012398&lt;/code&gt; встречается в трех слоях с рангами 10, 30
и 50. Показать его трижды в выдаче значит запутать пользователей.&lt;/p&gt;

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

&lt;h2 id=&quot;последующая-обработка&quot;&gt;Последующая обработка&lt;/h2&gt;

&lt;p&gt;Задача решается разными способами. Один из них в том, чтобы вынести &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;UNION&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;MIN&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;with&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;layers&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&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;id&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;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;rank&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;applications&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;doc&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;#&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'{application_id}'&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;s1&quot;&gt;'12398'&lt;/span&gt;

  &lt;span class=&quot;k&quot;&gt;union&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;all&lt;/span&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;mi&quot;&gt;20&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;rank&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;applications&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;doc&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;#&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'{organization.code}'&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;s1&quot;&gt;'12398'&lt;/span&gt;

  &lt;span class=&quot;k&quot;&gt;union&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;all&lt;/span&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;mi&quot;&gt;30&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;rank&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;applications&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;doc&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;#&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'{application_id}'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;ilike&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'%12398%'&lt;/span&gt;

  &lt;span class=&quot;k&quot;&gt;union&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;all&lt;/span&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;mi&quot;&gt;40&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;rank&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;applications&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;doc&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;#&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'{organization.code}'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;ilike&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'%12398%'&lt;/span&gt;

  &lt;span class=&quot;k&quot;&gt;union&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;all&lt;/span&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;mi&quot;&gt;50&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;rank&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;applications&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;doc&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;#&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'{comment}'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;ilike&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'%12398%'&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;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;min&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;rank&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;rank&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;from&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;layers&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;group&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;k&quot;&gt;order&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;by&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;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Выражение &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;order by 2&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;┌──────────────────────────────────────┬──────┐
│                  id                  │ rank │
├──────────────────────────────────────┼──────┤
│ 00000000-0000-0000-0000-000000012398 │   10 │
│ 00000000-0000-0000-0000-000000112398 │   30 │
│ 00000000-0000-0000-0000-000000123980 │   30 │
│ 00000000-0000-0000-0000-000000123981 │   30 │
│ 00000000-0000-0000-0000-000000123982 │   30 │
│ 00000000-0000-0000-0000-000000123983 │   30 │
│ 00000000-0000-0000-0000-000000123984 │   30 │
│ 00000000-0000-0000-0000-000000123985 │   30 │
│ 00000000-0000-0000-0000-000000123986 │   30 │
│ 00000000-0000-0000-0000-000000123987 │   30 │
│ 00000000-0000-0000-0000-000000123988 │   30 │
│ 00000000-0000-0000-0000-000000123989 │   30 │
│ 00000000-0000-0000-0000-000000212398 │   30 │
│ 00000000-0000-0000-0000-000000312398 │   30 │
│ 00000000-0000-0000-0000-000000412398 │   30 │
│ 00000000-0000-0000-0000-000000512398 │   30 │
│ 00000000-0000-0000-0000-000000612398 │   30 │
│ 00000000-0000-0000-0000-000000712398 │   30 │
│ 00000000-0000-0000-0000-000000812398 │   30 │
│ 00000000-0000-0000-0000-000000912398 │   30 │
└──────────────────────────────────────┴──────┘
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

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

&lt;p&gt;Теперь добавим к полученной выборке документы. Поместим ее в табличное выражение
с именем &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;grouped&lt;/code&gt; и свяжем с таблицей &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;applications&lt;/code&gt; по полю &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;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;with&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;layers&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&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;p&quot;&gt;),&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;grouped&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&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;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;min&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;rank&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;rank&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt;
      &lt;span class=&quot;n&quot;&gt;layers&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;group&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;k&quot;&gt;order&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;by&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;k&quot;&gt;g&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;a&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;doc&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;grouped&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;g&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;join&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;applications&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;a&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;on&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;g&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;a&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;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Полученный запрос и есть то, к чему мы стремились:&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;/ul&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, 10     │──┐                                           │
│└───────────────┘  │                                           │
│                   │                                           │
│┌───────────────┐  │                                           │
││    id, 20     │──┤                                           │
│└───────────────┘  │   ┌──────────────────┐    ┌──────────────┐│
│                   │   │                  │    │              ││
│┌───────────────┐  │   │remove duplicates │    │    fetch     ││
││    id, 30     │──┼──▶│   sort by rank   │───▶│  documents   ││
│└───────────────┘  │   │                  │    │              ││
│                   │   └──────────────────┘    └──────────────┘│
│┌───────────────┐  │                                           │
││    id, 40     │──┤                                           │
│└───────────────┘  │                                           │
│                   │                                           │
│┌───────────────┐  │                                           │
││    id, 50     │──┘                                           │
│└───────────────┘                                              │
└───────────────────────────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

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

&lt;h2 id=&quot;замена-union-на-full-join&quot;&gt;Замена UNION на FULL JOIN&lt;/h2&gt;

&lt;p&gt;Запрос можно переписать так, чтобы он не использовал &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;UNION&lt;/code&gt;. Для этого
результаты собирают не по вертикали, а по горизонтали при помощи &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;FULL
JOIN&lt;/code&gt;. Первый слой выступает точкой опоры, а последующие “цепляются” к
результату, просматривая ключи слева в поисках первого отличного от NULL. Для
ясности приведем схему:&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;┌─────────────────┬─────────────────┬─────────────────┬─────────────────┬─────────────────┐
│   app.code =    │   org.code =    │ app.code ilike  │ org.code ilike  │  comment ilike  │
├────────┬────────┼────────┬────────┼────────┬────────┼────────┬────────┼────────┬────────┤
│   id   │  rank  │   id   │  rank  │   id   │  rank  │   id   │  rank  │   id   │  rank  │
├────────┴────────┼────────┼────────┼────────┴────────┼────────┴────────┼────────┴────────┤
                  │  1002  │   20   │
│                 ├────────┼────────┼────────┬────────┤                 │                 │
                  │  1003  │   20   │  1003  │   30   │
│                 ├────────┼────────┼────────┼────────┤                 │                 │
                  │  1004  │   20   │  1004  │   30   │
├────────┬────────┼────────┼────────┼────────┼────────┤                 │                 │
│  1005  │   10   │  1005  │   20   │  1005  │   30   │
├────────┼────────┼────────┴────────┼────────┼────────┤                 │                 │
│  1006  │   10   │                 │  1006  │   30   │
├────────┼────────┤                 ├────────┼────────┤                 │                 │
│  1007  │   10   │                 │  1007  │   30   │
├────────┼────────┤                 ├────────┴────────┤                 │                 │
│  1008  │   10   │
├────────┴────────┤                 │                 ├────────┬────────┤                 │
                                                      │  1009  │   40   │
│                 │                 │                 ├────────┴────────┼────────┬────────┤
                                                                        │  1010  │   50   │
└ ─ ─ ─ ─ ─ ─ ─ ─ ┴ ─ ─ ─ ─ ─ ─ ─ ─ ┴ ─ ─ ─ ─ ─ ─ ─ ─ ┴ ─ ─ ─ ─ ─ ─ ─ ─ ┴────────┴────────┘
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

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

&lt;p&gt;Полученную матрицу нужно свернуть, то есть получить финальные ключ и ранг. Для
обоих полей алгоритм одинаков: движемся слева направо и берем первое отличное от
NULL значение. Поскольку ранги слоев нарастают тоже слева направо, первый
непустой ранг будет минимальным. Вот что вернет описанный алгоритм:&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   │  rank  │
├────────┼────────┤
│  1002  │   20   │
├────────┼────────┤
│  1003  │   20   │
├────────┼────────┤
│  1004  │   20   │
├────────┼────────┤
│  1005  │   10   │
├────────┼────────┤
│  1006  │   10   │
├────────┼────────┤
│  1007  │   10   │
├────────┼────────┤
│  1008  │   10   │
├────────┼────────┤
│  1009  │   40   │
├────────┼────────┤
│  1010  │   50   │
└────────┴────────┘
&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   │  rank  │
├────────┼────────┤
│  1005  │   10   │
├────────┼────────┤
│  1006  │   10   │
├────────┼────────┤
│  1007  │   10   │
├────────┼────────┤
│  1008  │   10   │
├────────┼────────┤
│  1002  │   20   │
├────────┼────────┤
│  1003  │   20   │
├────────┼────────┤
│  1004  │   20   │
├────────┼────────┤
│  1009  │   40   │
├────────┼────────┤
│  1010  │   50   │
└────────┴────────┘
&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;with&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;matrix&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&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;coalesce&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;sub1&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;sub2&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;sub3&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;sub4&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;sub5&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;k&quot;&gt;as&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;coalesce&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;sub1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;rank&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;sub2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;rank&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;sub3&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;rank&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;sub4&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;rank&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;sub5&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;rank&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;rank&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;from&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;id&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;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;rank&lt;/span&gt;
      &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;applications&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;app&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;doc&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;#&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'{application_id}'&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;s1&quot;&gt;'12398'&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;sub1&lt;/span&gt;

  &lt;span class=&quot;k&quot;&gt;full&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;join&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;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;20&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;rank&lt;/span&gt;
      &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;applications&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;doc&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;#&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'{organization.code}'&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;s1&quot;&gt;'12398'&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;sub2&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;on&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;coalesce&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;sub1&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;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;sub2&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;full&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;join&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;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;30&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;rank&lt;/span&gt;
      &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;applications&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;doc&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;#&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'{application_id}'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;ilike&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'%12398%'&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;sub3&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;on&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;coalesce&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;sub1&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;sub2&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;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;sub3&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;full&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;join&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;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;40&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;rank&lt;/span&gt;
      &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;applications&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;doc&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;#&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'{organization.code}'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;ilike&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'%12398%'&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;sub4&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;on&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;coalesce&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;sub1&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;sub2&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;sub3&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;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;sub4&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;full&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;join&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;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;50&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;rank&lt;/span&gt;
      &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;applications&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;doc&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;#&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'{comment}'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;ilike&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'%12398%'&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;sub5&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;on&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;coalesce&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;sub1&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;sub2&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;sub3&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;sub4&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;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;sub5&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;order&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;by&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;n&quot;&gt;matrix&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;app&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;doc&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;from&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;matrix&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;join&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;applications&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;app&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;on&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;matrix&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;app&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;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;В запросе часто встречается выражение coalesce. Напомним, что это не функция, а
особая форма, которая принимает произвольное аргументы и возвращает первый
отличный от NULL. Аргументы вычисляются лениво по мере того как Postgres
перебирает их. Конструкция &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;coalesce(sub1.id, sub2.id, sub3.id, ...)&lt;/code&gt; сканирует
столбцы слева направо и возвращает первый не пустой. В случае с рангами форма
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;coalesce(sub1.rank, sub2.rank, ...)&lt;/code&gt; работает как функция &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;LEAST&lt;/code&gt;, потому что
мы специально расположили ранги по нарастанию.&lt;/p&gt;

&lt;p&gt;Какой именно способ предпочесть – по вертикали с &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;UNION&lt;/code&gt; или горизонтали с &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;FULL
JOIN&lt;/code&gt;, – зависит от предпочтений. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;UNION&lt;/code&gt; несколько проще в понимании и
удобстве: можно переставить слои местами, и это не повлияет на дальнейшие
шаги. Для &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;FULL JOIN&lt;/code&gt; это не так, потому что перестановка нарушит выражения
coalesce, по которым мы соединяем слои. Текст ниже подразумевает, что мы
остановили выбор на UNION.&lt;/p&gt;

&lt;h2 id=&quot;коротко-о-mapreduce&quot;&gt;Коротко о MapReduce&lt;/h2&gt;

&lt;p&gt;Поиск, что мы написали, отдаленно напоминает технику
&lt;a href=&quot;https://en.wikipedia.org/wiki/MapReduce&quot;&gt;MapReduce&lt;/a&gt;. Согласно ей, поиск информации проходит в два этапа. На
первом этапе (map – назначить, разметить) приложение собирает данные из разных
источников. На втором этапе (reduce – свернуть, понизить) из каждого фрагмента
берутся лучшие экземпляры. Вместе они образуют результат поиска.&lt;/p&gt;

&lt;p&gt;MapReduce предполагает, что данные приходят не только из реляционных таблиц, но
и вообще отовсюду, включая файлы, сетевые хранилища, сервисы. В нашей книге мы
работаем с SQL и не можем выйти за пределы реляционной базы. Технически возможно
обратиться из Postgres к другой базе данных (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;postgres_fdw&lt;/code&gt;) или HTTP-сервису
(&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pgsql-http&lt;/code&gt;), однако такие трюки мы не рассматриваем.&lt;/p&gt;

&lt;h2 id=&quot;поиск-в-исторической-таблице&quot;&gt;Поиск в исторической таблице&lt;/h2&gt;

&lt;p&gt;Доработаем поиск так, чтобы наряду с таблицей &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;applications&lt;/code&gt; он учитывал и
другие. Так, в прошлой главе мы рассмотрели версии документов и таблицу
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;history&lt;/code&gt;. Коротко рассмотрим, чем она может быть полезна.&lt;/p&gt;

&lt;p&gt;Предположим, организация сменила название с “Acme Inc” на “Global
Corp”. Пользователь, который еще об этом не знает, вводит “Acme” и ничего не
находит – в таблице &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;applications&lt;/code&gt; нет заявки с &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;organization.short_name&lt;/code&gt;,
равным Acme. Но если выполнить поиск в &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;history&lt;/code&gt;, мы найдем версию документа с
прежним названием. По полю &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pk&lt;/code&gt; мы присоединим актуальный документ, и он
окажется в выдаче.&lt;/p&gt;

&lt;p&gt;Задача сводится к тому, чтобы добавить в подзапрос &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;layers&lt;/code&gt; новый слой. Его ранг
еще ниже – 60, потому что исторические данные интересуют нас в последнюю
очередь. Приведем запрос с учетом истории:&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;with&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;layers&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&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;id&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;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;rank&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;applications&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;doc&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;#&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'{application_id}'&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;s1&quot;&gt;'12398'&lt;/span&gt;

  &lt;span class=&quot;k&quot;&gt;union&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;all&lt;/span&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;mi&quot;&gt;20&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;rank&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;applications&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;doc&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;#&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'{organization.code}'&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;s1&quot;&gt;'12398'&lt;/span&gt;

  &lt;span class=&quot;k&quot;&gt;union&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;all&lt;/span&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;mi&quot;&gt;30&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;rank&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;applications&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;doc&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;#&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'{application_id}'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;ilike&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'%12398%'&lt;/span&gt;

  &lt;span class=&quot;k&quot;&gt;union&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;all&lt;/span&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;mi&quot;&gt;40&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;rank&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;applications&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;doc&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;#&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'{organization.code}'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;ilike&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'%12398%'&lt;/span&gt;

  &lt;span class=&quot;k&quot;&gt;union&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;all&lt;/span&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;mi&quot;&gt;50&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;rank&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;applications&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;doc&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;#&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'{comment}'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;ilike&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'%12398%'&lt;/span&gt;

  &lt;span class=&quot;k&quot;&gt;union&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;all&lt;/span&gt;

  &lt;span class=&quot;k&quot;&gt;select&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;pk&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;60&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;rank&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;history&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;where&lt;/span&gt;
      &lt;span class=&quot;n&quot;&gt;entity&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'application'&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;and&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;created_at&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;now&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;interval&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'1 week'&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;and&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;doc&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;#&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'{organization,short_name}'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;ilike&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'%12398%'&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;grouped&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&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;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;min&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;rank&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;rank&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt;
      &lt;span class=&quot;n&quot;&gt;layers&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;group&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;k&quot;&gt;order&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;by&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;k&quot;&gt;g&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;a&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;doc&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;grouped&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;g&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;join&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;applications&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;a&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;on&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;g&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;a&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;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Последний слой (ранг 60) интересен следующим. Во-первых, мы выбираем поле &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pk&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;id&lt;/code&gt; – код версии, а нам нужен код документа
(&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pk&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;entity&lt;/code&gt; – историческая таблица хранит
все сущности, и нам не хотелось бы искать что-либо, отличное от заявок.&lt;/p&gt;

&lt;p&gt;Убедитесь, что исторический запрос попадает в индекс. Скопируйте часть &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;select
pk, 60 as rank...&lt;/code&gt;, предварите ее командой &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;explain analyze&lt;/code&gt; и выполните c разными
параметрами.&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;Когда запускается поиск с UNION, выполняются SELECT-запросы слоев: точное
равенство, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ilike&lt;/code&gt; и другие. Если условие попадает в индекс, выборка
производительна. Предполагается, что мы создали индексы для каждого слоя и все
они “быстрые”. В UNION запросы могут выполняться параллельно – при условии, что
это позволяет окружение и планировщик выбрал такой алгоритм. Из этих соображений
можно сделать вывод, что часть UNION тоже “быстрая”.&lt;/p&gt;

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

&lt;p&gt;Последний шаг, где мы присоединяем документы, тоже хорошо
прогнозируется. Условие JOIN опирается на первичный ключ id таблицы
applications, который индексирован. Соединение двухсот документов по ключам тоже
сработает “быстро”. Итого – если каждый шаг “быстрый” (опирается на индекс),
запросом можно пользоваться, не боясь деградации базы.&lt;/p&gt;

&lt;p&gt;Привести полные планы запросов мы не сможем, потому что они слишком велики;
ограничимся лишь финальными материками. На ноутбуке Macbook M4 Pro 48G поиск с
UNION ALL занял в среднем 12 миллисекунд (Execution Time), cost верхнего узла
равен 2951..4621. Запрос с FULL JOIN оказался эффективней: 3 миллисекунды
(Execution Time), стоимость верхнего узла – 3795..4636. Полные планы вы найдете
&lt;a href=&quot;https://github.com/igrishaev/pg-json-book-code/blob/master/ch10/sql.sql&quot;&gt;в репозитории&lt;/a&gt; с исходным кодом глав.&lt;/p&gt;

&lt;h2 id=&quot;дальнейшие-улучшения&quot;&gt;Дальнейшие улучшения&lt;/h2&gt;

&lt;p&gt;Изучив метрики, подумаем, как можно улучшить поиск. Одна из доработок в
следующем: перед тем как выполнить запрос, проверьте поисковое выражение на
разные критерии. Уже на этом этапе можно определить, что некоторые слои не
понадобятся, и не включать их в построитель SQL.&lt;/p&gt;

&lt;p&gt;Очевидный случай – пустая строка или содержащая только незначащие символы
(пробелы, перенос строки, табуляция). Такие запросы не должны выполняться,
потому что напрасно расходуют ресурсы. Другой вариант – проверка на определенные
шаблоны, например, что номер заявки содержит только цифры. Если в поиск ввели
что-то отличное от цифр, поиск по номеру будет заведомо пустым, и этот слой
исключают из UNION. Обратная ситуация – имя пользователя: если на вход поступили
только цифры, поиск по такому “имени” ничего не даст.&lt;/p&gt;

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

&lt;p&gt;Другая доработка касается длины выборки. На текущий момент каждый слой UNION
неограничен – мы получим столько записей, сколько подошло условию. Для точных
совпадений это не проблема: мы получим одну или несколько строк. Однако неточные
совпадения порой дают сотни записей. Например, для выражения 123 мы получим
документы 551235, 9123, 123512 и так далее. Если предположить, что код заявки
меняется от единицы до миллиона, то кодов, содержащих “123”, окажется много.&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;select&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;count&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;from&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;generate_series&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;mi&quot;&gt;1000000&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;_&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;x&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;where&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;x&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;text&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;like&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'%123%'&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;-- 3999&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;По поиску “123” мы получим без одного четыре тысячи документов! Недостаток
такого слоя в том, что он нарушает технику MapReduce. Согласно ей, из каждого
слоя выбирается часть записей, а не все подряд. Клиенту будет неудобно
просматривать столь длинную выдачу. Кроме того, неограниченная выборка может
замедлить запрос даже с учетом индекса.&lt;/p&gt;

&lt;p&gt;Ограничим каждый SELECT оператором LIMIT. Конкретное значение зависит от многих
факторов и подбирается вручную; в случае автора им оказалось число 50. Приведем
фрагмент запроса, где в каждый слой добавлен LIMIT:&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;with&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;layers&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&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;p&quot;&gt;(&lt;/span&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;mi&quot;&gt;10&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;rank&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;applications&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;doc&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;#&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'{application_id}'&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;s1&quot;&gt;'12398'&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;limit&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;50&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

  &lt;span class=&quot;k&quot;&gt;union&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;all&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;p&quot;&gt;(&lt;/span&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;mi&quot;&gt;20&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;rank&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;applications&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;doc&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;#&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'{organization.code}'&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;s1&quot;&gt;'12398'&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;limit&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;50&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;Обратите внимание: мы обернули каждый SELECT в подзапрос и выбираем из
него. Дело в том, что если добавить LIMIT 250 в конец UNION (250 мы получили,
умножив 50 на число слоев), оператор действует на объединение в целом, а не
слои. Мы словно говорим Postgres: выбери из нескольких запросов не более 250
записей, а в какой пропорции – решай сам. Может оказаться, что все записи пришли
из одного слоя, а до остальных даже не дошло. Чтобы избежать диспропорции,
каждый слой оборачивается в подзапрос с персональным LIMIT, в нашем случае 50.&lt;/p&gt;

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

&lt;h2 id=&quot;страничная-навигация&quot;&gt;Страничная навигация&lt;/h2&gt;

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

&lt;p&gt;На первый взгляд ответ очевиден: разумеется да! В каждой поисковой системе есть
навигатор, с помощью которого мы переходим на вторую, третью и другие
страницы. Иной раз число страниц поражает воображение. Так, по слову “python”
Гугл находит почти 400 миллионов ссылок (если точнее, 394,000,000). Если
предположить, что одна страница вмещает десять ссылок, всего страниц окажется 40
миллионов. Одному человеку физически не под силу их разобрать.&lt;/p&gt;

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

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

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

&lt;h2 id=&quot;полнотекстовый-поиск&quot;&gt;Полнотекстовый поиск&lt;/h2&gt;

&lt;p&gt;Напоследок рассмотрим еще один вопрос, связанный с качеством поиска. До сих мы
писали запросы с точным совпадением (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;=&lt;/code&gt;) и соответствием шаблону
(&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ilike&lt;/code&gt;). Postgres предлагает полнотекстовый поиск с богатыми
возможностями. Среди них — поддержка многих языков, включая русский, позиция
слов, расстояние между ними, словари синонимов и многое другое.&lt;/p&gt;

&lt;p&gt;Чтобы начать работу с поиском, не требуется расширений: все доступно по
умолчанию. Однако желательно, чтобы перед этим разработчик изучил
&lt;a href=&quot;https://postgrespro.ru/docs/postgrespro/current/textsearch&quot;&gt;документацию&lt;/a&gt;. На сайте Postgres Pro она занимает одиннадцать
разделов; каждый из них рассматривает свой аспект поиска с примерами.&lt;/p&gt;

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

&lt;p&gt;Поиск в Postgres опирается на понятие “документ” — специальный вектор,
полученный из текста. Просим не путать его с JSON-документом, о котором мы
говорим всю книгу. Чтобы избежать путаницы, по возможности будем писать
“поисковой документ” и “JSON-документ”.&lt;/p&gt;

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

&lt;ul&gt;
  &lt;li&gt;все символы приводятся к нижнему регистру.&lt;/li&gt;
  &lt;li&gt;Удаляются незначащие (стоп-) слова: союзы, предлоги, артикли (английские a, an, the, французские le, la).&lt;/li&gt;
  &lt;li&gt;Удаляются окончания слов, чтобы падеж и число не влияли на результат. Усеченные слова называют лексемами.&lt;/li&gt;
  &lt;li&gt;Каждая лексема знает позицию в исходном тексте. Это необходимо для расчета
близости слов: чем она меньше, тем выше релевантность. Каждая лексема хранится
в одном экземпляре. Если в тексте их несколько, перечислены все позиции.&lt;/li&gt;
  &lt;li&gt;Каждой лексеме можно назначить вес, который позже учитывается в ранге.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Для хранения поискового документа служит особый тип &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;tsvector&lt;/code&gt;, а для запроса к
нему – &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;tsquery&lt;/code&gt;. В обоих случаях префикс &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ts_&lt;/code&gt; означает text search. С него
начинаются все типы и функции, имеющих отношение к поиску.&lt;/p&gt;

&lt;p&gt;Чтобы получить документ из текста, вызовите функцию &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;to_tsvector&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;to_tsvector&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;'russian'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&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;/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;┌──────────────────────────────────────────────┐
│                 to_tsvector                  │
├──────────────────────────────────────────────┤
│ 'друг':8 'животн':2,6 'некотор':5 'равн':3,7 │
└──────────────────────────────────────────────┘
&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;to_tsquery&lt;/code&gt; можно вызвать без языка (первого аргумента). В этом случае
сработает язык по умолчанию; узнать его можно командой &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;show&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;show&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;default_text_search_config&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
 &lt;span class=&quot;n&quot;&gt;default_text_search_config&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;----------------------------&lt;/span&gt;
 &lt;span class=&quot;n&quot;&gt;pg_catalog&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;english&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;to_tsquery&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;to_tsvector&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s1&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;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;┌─────────────────────────────────────────────────────────────────────────────┐
│                                 to_tsvector                                 │
├─────────────────────────────────────────────────────────────────────────────┤
│ 'все':1 'других':8 'животные':2,6 'некоторые':5 'но':4 'равнее':7 'равны':3 │
└─────────────────────────────────────────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

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

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

&lt;h2 id=&quot;объект-запроса&quot;&gt;Объект запроса&lt;/h2&gt;

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

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

&lt;p&gt;Важно, что в запросе не может быть пробелов между словами. Вместо них стоят
операторы, которые означают, как обрабатывать слова. Наиболее частый оператор
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;amp;&lt;/code&gt; означает, что слова встречаются в документе без учета порядка. Оператор
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;-&amp;gt;&lt;/code&gt; строже: совпадение случится, если в поисковом документе слова идут в том
же порядке, что и в запросе. Палочка &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;|&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;to_tsquery&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;'russian'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'все &amp;amp; животные &amp;amp; равны'&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;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;┌───────────────────┐
│    to_tsquery     │
├───────────────────┤
│ 'животн' &amp;amp; 'равн' │
└───────────────────┘
&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;tsquery&lt;/code&gt; служит целое семейство функций, каждая со своей
особенностью. Среди прочих нас интересует &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;plainto_tsquery&lt;/code&gt;, которая подходит
под самый востребованный случай. Она принимает обычный текст, находит лексемы и
объединяет их оператором &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;amp;&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;plainto_tsquery&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;'russian'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&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;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;┌───────────────────┐
│  plainto_tsquery  │
├───────────────────┤
│ 'животн' &amp;amp; 'равн' │
└───────────────────┘
&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;Postgres не предлагает способа определить язык, и как это сделать – остается на
ваше усмотрение. Один из способов в том, чтобы вызвать библиотеку
&lt;a href=&quot;https://github.com/Mimino666/langdetect&quot;&gt;langdetect&lt;/a&gt; на &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Python&lt;/code&gt; через расширение plpython3u. Приведем
минимальный пример:&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;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;langdetect&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;langdetect&lt;/span&gt; &lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;detect&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;detect_langs&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;text&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;Все животные равны, но некоторые животные равнее других&quot;&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;print&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;detect&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;text&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;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;detect_langs&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;text&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# ru
# [ru:0.9999961866373268]
&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;detect&lt;/code&gt; возвращает наиболее вероятный язык, а &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;detect_langs&lt;/code&gt; – их
список с вероятностью каждого, если подходят несколько вариантов.&lt;/p&gt;

&lt;p&gt;Для Perl написано семейство модулей Lingua, &lt;a href=&quot;https://metacpan.org/pod/Lingua::Identify&quot;&gt;один из которых&lt;/a&gt; служит
для определения текста. Для вызова кода на Perl установите расширение
plperl. Автор уверен, что после главы об интерпретаторах читатель справится с
этой задачей.&lt;/p&gt;

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

&lt;p&gt;Язык, что мы передаем в функции, на самом деле не строка, а более сложный тип —
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;regconfig&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;cfgname&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;pg_ts_config&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;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;┌────────────┐
│  cfgname   │
├────────────┤
│ simple     │
│ arabic     │
│ armenian   │
│ basque     │
│ catalan    │
│ danish     │
│ dutch      │
│ english    │
│ finnish    │
│ french     │
│ german     │
│ greek      │
│ hindi      │
│ hungarian  │
│ indonesian │
│ irish      │
│ italian    │
│ lithuanian │
│ nepali     │
│ norwegian  │
│ portuguese │
│ romanian   │
│ russian    │
│ serbian    │
│ spanish    │
│ swedish    │
│ tamil      │
│ turkish    │
│ yiddish    │
└────────────┘
&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;simple&lt;/code&gt; — самый простой, примитивный
анализатор текста. Постарайтесь не использовать его в боевом запуске.&lt;/p&gt;

&lt;p&gt;Один из способов добиться того, чтобы язык &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;tsvector&lt;/code&gt; и &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;tsquery&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;alter&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;table&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;applications&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;lang&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;regconfig&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;k&quot;&gt;update&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;applications&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;set&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;lang&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'english'&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;comment&lt;/code&gt;
передается в функцию &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;detect&lt;/code&gt; модуля &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;langdetect&lt;/code&gt;. Далее ответ &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;detect&lt;/code&gt;
приводится к одному из значений &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;regconfig&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;or&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;replace&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;app_detect_lang&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;doc&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;returns&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;regconfig&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;transform&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;jsonb&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;language&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;plpython3u&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;immutable&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;strict&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;parallel&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;safe&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;$$&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;langdetect&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;detect&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;lang&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;detect&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;doc&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;&quot;comment&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;])&lt;/span&gt;

    &lt;span class=&quot;n&quot;&gt;mapping&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;nv&quot;&gt;&quot;en&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;&quot;english&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;nv&quot;&gt;&quot;ru&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;&quot;russian&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;nv&quot;&gt;&quot;fr&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;&quot;french&quot;&lt;/span&gt;
    &lt;span class=&quot;err&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;n&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;lang&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;mapping&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;n&quot;&gt;mapping&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;lang&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;else&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;s1&quot;&gt;'simple'&lt;/span&gt;
&lt;span class=&quot;err&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;pg_cron&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;update&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;applications&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;set&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;lang&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;app_detect_lang&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;doc&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;where&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;lang&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;is&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;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;полнотекстовый-слой-в-union&quot;&gt;Полнотекстовый слой в UNION&lt;/h2&gt;

&lt;p&gt;Теперь когда у нас есть поисковой документ и запрос, проверим их на
соответствие. Для этого служит оператор &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@@&lt;/code&gt;, с которым мы познакомились в главе
про JSON Path. У него схожая семантика: документ слева проверяется на
соответствие запросу справа. Оператор возвращает истину или ложь:&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;to_tsvector&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;'russian'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&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;plainto_tsquery&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;'russian'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'животные равны'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;res&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;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;┌─────┐
│ res │
├─────┤
│ t   │
└─────┘
&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;n&quot;&gt;to_tsvector&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;'russian'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&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;plainto_tsquery&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;'english'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'животные равны'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;res&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;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;┌─────┐
│ res │
├─────┤
│ f   │
└─────┘
&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;UNION&lt;/code&gt; на другой,
основанный на tsvector. Новый слой выглядит так:&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;p&quot;&gt;(&lt;/span&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;mi&quot;&gt;50&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;rank&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;applications&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;where&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;to_tsvector&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;'english'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;doc&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;'comment'&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;to_tsquery&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;'english'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'12398'&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;50&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;sub5&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;index&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;idx_application_comment_tsvector&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;on&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;applications&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;using&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;gin&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;to_tsvector&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;'english'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;doc&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;'comment'&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;'english'&lt;/code&gt; заменяют на
колонку &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;lang&lt;/code&gt;, где хранится язык.&lt;/p&gt;

&lt;h2 id=&quot;оптимизации-и-доработки&quot;&gt;Оптимизации и доработки&lt;/h2&gt;

&lt;p&gt;К полнотекстовому поиску применяются все оптимизации, что мы рассмотрели для
триграммного. Можно индексировать не одно поле, а несколько за счет конкатенации
строк. Объединяет строки оператор &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;||&lt;/code&gt; или &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;функция concat_ws&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;where&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;to_tsvector&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;lang&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;concat_ws&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;s1&quot;&gt;', '&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;doc&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;#&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'{comment}'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;doc&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;#&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'{description}'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;doc&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;#&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'{path,to,field}'&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;plainto_tsquery&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;lang&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'12398'&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;tsvector&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;or&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;replace&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;app_tsvector&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;doc&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;n&quot;&gt;lang&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;regconfig&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;returns&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;tsvector&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;language&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;sql&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;immutable&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;strict&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;parallel&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;safe&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;/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;index&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;idx_application_comment_tsvector&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;on&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;applications&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;using&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;gin&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;app_tsvector&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;doc&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;lang&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;n&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;doc&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;applications&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;where&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;app_tsvector&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;doc&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;lang&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;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;query&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;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;alter&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;table&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;applications&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;_ts_vect&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;tsvector&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;generated&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;always&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;app_tsvector&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;doc&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;lang&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;stored&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;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;doc&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;applications&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;where&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;_ts_vect&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;@@&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;query&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;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;applications_tsvector&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;app_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;k&quot;&gt;references&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;applications&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;tsvect&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;tsvector&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;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Чтобы найти документ, сперва мы фильтруем таблицу &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;applications_tsvector&lt;/code&gt; по
полю tsvect, а затем JOIN-им документ по &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;app_id&lt;/code&gt;. Это позволяет индексировать
документ, не затрагивая таблицу &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;applications&lt;/code&gt;. Тем самым достигается разделение
ответственности: сущности – отдельно, поисковой индекс – отдельно.&lt;/p&gt;

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

&lt;p&gt;Вектор можно улучшить по многим критериям. Один из них в том, что иногда
документ содержит фрагменты на разных языках, например основная часть изложена
на русском, а цитаты – на английском. Предположим, мы написали скрипт, который
делит документ на языковые блоки. Для каждого блока вычисляется
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;tsvector&lt;/code&gt;. Полученные векторы объединяются оператором &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;||&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;select&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;to_tsvector&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;s1&quot;&gt;'russian'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;s1&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;to_tsvector&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;s1&quot;&gt;'english'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;s1&quot;&gt;'All animals are equal, but some animals are more equal than others'&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;vec&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;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;┌────────────────────────────────────────────────────────────────────────────────────┐
│                                        vec                                         │
├────────────────────────────────────────────────────────────────────────────────────┤
│ 'anim':10,15 'equal':12,18 'other':20 'друг':8 'животн':2,6 'некотор':5 'равн':3,7 │
└────────────────────────────────────────────────────────────────────────────────────┘
&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;&amp;lt;res&amp;gt;&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;&amp;lt;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;res&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;@@&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;plainto_tsquery&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;'russian'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'животные равны'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;res&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;-- true&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;select&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;res&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;@@&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;plainto_tsquery&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;'english'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'animal equal'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;res&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;-- false&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Другие возможные улучшения – подключить словари стоп-слов и синонимов,
специфичных для вашей отрасли. Скажем, можно задать правило, чтобы при разборе
текста слово “application” сокращалось до “app”. В этом случае запрос “app 123”
найдет документ, где в исходном тексте встречается “Application 123”. То же
самое касается слова “customer”, которое часто сокращают до “cust”. Устойчивые
сочетания слов можно свести к аббревиатурам, например “country of risk” →
“cor”, “know your customer” → “kyc” и другие. В разделе &lt;a href=&quot;https://postgrespro.ru/docs/enterprise/18/textsearch-dictionaries&quot;&gt;“Textsearch
Dictionaries”&lt;/a&gt; документации Postgres эта техника
описана подробно.&lt;/p&gt;

&lt;p&gt;Еще больше сведений, связанных с поиском и релевантностью, вы найдете в
англоязычной статье &lt;a href=&quot;https://anyblockers.com/posts/postgres-as-a-search-engine&quot;&gt;“Postgres as a search engine”&lt;/a&gt;. На
Хабре доступен &lt;a href=&quot;https://habr.com/ru/companies/sravni/articles/888534/&quot;&gt;ее перевод&lt;/a&gt; на русский язык.&lt;/p&gt;

&lt;h2 id=&quot;заключение&quot;&gt;Заключение&lt;/h2&gt;

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

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

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

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

&lt;p&gt;Работая над поиском, не спешите внедрять сторонние решения вроде ElasticSearch,
OpenSearch и аналогов. Даже если их результаты точнее, затраты на ввод в
эксплуатацию и поддержку окажутся значительными. Идите на этот шаг только если
полностью исчерпали потенциал Postgres.&lt;/p&gt;

&lt;p&gt;Не существует универсального рецепта поиска. В каждой фирме используют свой
подход, отталкиваясь от конкретных задач. Надеемся, мы привели достаточно
примеров и техник, из которых читатель составит поиск под свои нужды.&lt;/p&gt;
</description>
        <pubDate>Fri, 18 Sep 2026 00:00:00 +0000</pubDate>
        <link>https://grishaev.me/pg-book-json-ch10/</link>
        <guid isPermaLink="true">https://grishaev.me/pg-book-json-ch10/</guid>
        
        <category>postgres</category>
        
        <category>json</category>
        
        <category>book</category>
        
        <category>sql</category>
        
        
      </item>
    
      <item>
        <title>Глава 9. Версии и патчи</title>
        <description>&lt;h2 id=&quot;главы&quot;&gt;Главы&lt;/h2&gt;

&lt;ol&gt;
  &lt;li&gt;&lt;a href=&quot;/pg-book-json-ch01/&quot;&gt;Введение в документы&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;/pg-book-json-ch02/&quot;&gt;Базовые возможности JSON&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;/pg-book-json-ch03/&quot;&gt;JSON в таблицах&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;/pg-book-json-ch04/&quot;&gt;Индексирование JSON&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;/pg-book-json-ch05/&quot;&gt;Ограничения в документах&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;/pg-book-json-ch06/&quot;&gt;Язык путей JSONPath&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;/pg-book-json-ch07/&quot;&gt;Отчеты, функции, расписание&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;/pg-book-json-ch08&quot;&gt;Функции на языке Python&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;/pg-book-json-ch09/&quot;&gt;Версии и патчи&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;/pg-book-json-ch10/&quot;&gt;Релевантный поиск&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;

    Содержание

&lt;/h2&gt;

&lt;ul id=&quot;toc-item-pg-book-json-ch09&quot;&gt;
  &lt;li&gt;&lt;a href=&quot;#главы&quot; id=&quot;toc-item-pg-book-json-ch09-главы&quot;&gt;Главы&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#историческая-таблица&quot; id=&quot;toc-item-pg-book-json-ch09-историческая-таблица&quot;&gt;Историческая таблица&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#демонстрация&quot; id=&quot;toc-item-pg-book-json-ch09-демонстрация&quot;&gt;Демонстрация&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#создание-версий&quot; id=&quot;toc-item-pg-book-json-ch09-создание-версий&quot;&gt;Создание версий&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#версии-на-триггерах&quot; id=&quot;toc-item-pg-book-json-ch09-версии-на-триггерах&quot;&gt;Версии на триггерах&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#доводы-против-триггеров&quot; id=&quot;toc-item-pg-book-json-ch09-доводы-против-триггеров&quot;&gt;Доводы против триггеров&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#версии-в-приложении&quot; id=&quot;toc-item-pg-book-json-ch09-версии-в-приложении&quot;&gt;Версии в приложении&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#версии-в-postgres&quot; id=&quot;toc-item-pg-book-json-ch09-версии-в-postgres&quot;&gt;Версии в Postgres&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#ограничение-по-времени&quot; id=&quot;toc-item-pg-book-json-ch09-ограничение-по-времени&quot;&gt;Ограничение по времени&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#восстановление-версии&quot; id=&quot;toc-item-pg-book-json-ch09-восстановление-версии&quot;&gt;Восстановление версии&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#архивация-версий&quot; id=&quot;toc-item-pg-book-json-ch09-архивация-версий&quot;&gt;Архивация версий&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#стандарт-json-patch&quot; id=&quot;toc-item-pg-book-json-ch09-стандарт-json-patch&quot;&gt;Стандарт JSON Patch&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#патчи-в-приложении&quot; id=&quot;toc-item-pg-book-json-ch09-патчи-в-приложении&quot;&gt;Патчи в приложении&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#патчи-в-базе-данных&quot; id=&quot;toc-item-pg-book-json-ch09-патчи-в-базе-данных&quot;&gt;Патчи в базе данных&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#версии-с-патчами&quot; id=&quot;toc-item-pg-book-json-ch09-версии-с-патчами&quot;&gt;Версии с патчами&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#прием-патчей-от-клиента&quot; id=&quot;toc-item-pg-book-json-ch09-прием-патчей-от-клиента&quot;&gt;Прием патчей от клиента&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#коротко-об-event-sourcing&quot; id=&quot;toc-item-pg-book-json-ch09-коротко-об-event-sourcing&quot;&gt;Коротко об event-sourcing&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#сбор-агрегатов-из-событий&quot; id=&quot;toc-item-pg-book-json-ch09-сбор-агрегатов-из-событий&quot;&gt;Сбор агрегатов из событий&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#махинации-с-патчами&quot; id=&quot;toc-item-pg-book-json-ch09-махинации-с-патчами&quot;&gt;Махинации с патчами&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#бесконфликтное-редактирование&quot; id=&quot;toc-item-pg-book-json-ch09-бесконфликтное-редактирование&quot;&gt;Бесконфликтное редактирование&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#решение-конфликтов&quot; id=&quot;toc-item-pg-book-json-ch09-решение-конфликтов&quot;&gt;Решение конфликтов&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#технические-детали&quot; id=&quot;toc-item-pg-book-json-ch09-технические-детали&quot;&gt;Технические детали&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;В этой главе мы поговорим о том, как хранить разные версии одного
документа. Обсудим, как организовать историческую таблицу, перемещать в нее
документы и восстанавливать их. Также мы затронем тему разности JSON и
познакомимся со стандартом JSON Patch (не путать с JSON Path).&lt;/em&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;li&gt;Вести аналитику: смотреть как менялись сущности во времени, строить графики,
отслеживать рост и падение показателей.&lt;/li&gt;
&lt;/ul&gt;

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

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

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

&lt;p&gt;Иногда версии хранятся неограниченно долго, а иногда их “подрезают”. Записи
старше определенного срока считаются устаревшими, и спецальная задача переносит
их в другое место, например хранилище S3. Очистка запускается регулярно по
расписанию.&lt;/p&gt;

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

&lt;h2 id=&quot;историческая-таблица&quot;&gt;Историческая таблица&lt;/h2&gt;

&lt;p&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;pk&lt;/code&gt; или &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;doc_id&lt;/code&gt;. Это поле не будет уникальным, потому
что один и тот же документ может иметь несколько версий.&lt;/p&gt;

&lt;p&gt;Поле doc типом jsonb хранит документ в состоянии до того, как к нему применили
изменения.&lt;/p&gt;

&lt;p&gt;Вспомогательное поле &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;entity&lt;/code&gt; содержит имя таблицы, которой принадлежит
документ: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;application&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;organization&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;pk&lt;/code&gt;. В некоторых случаях (например,
в триггерах) поле &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;entity&lt;/code&gt; заполняется из локальной переменной.&lt;/p&gt;

&lt;p&gt;Колонка &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;operation&lt;/code&gt; пригодится, чтобы знать тип операции над документом:
обновление или удаление. Будем хранить в ней значения “update” и “delete”. Как и
в случае с &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;entity&lt;/code&gt;, иногда имя операции доступно в переменной.&lt;/p&gt;

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

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

&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;history&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;not&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;null&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;default&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;gen_random_uuid&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(),&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;pk&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;uuid&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;entity&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;text&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;k&quot;&gt;operation&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;text&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;doc&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;k&quot;&gt;default&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;user_id&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;uuid&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;k&quot;&gt;comment&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;text&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;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

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

&lt;p&gt;Подумаем теперь, за какой информацией мы будем обращаться к таблице &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;history&lt;/code&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;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;(pk, created_at
desc)&lt;/code&gt;. Поскольку &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pk&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;limit 1&lt;/code&gt;. Уникальность индекса не позволит иметь
несколько версий с одинаковой датой.&lt;/p&gt;

&lt;h2 id=&quot;демонстрация&quot;&gt;Демонстрация&lt;/h2&gt;

&lt;p&gt;Опробуем историческую таблицу в действии. Запишем в нее сто документов по десять
версий на каждый, что в дает тысячу записей. Для этого подготовим функцию для
генерации UUID:&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;or&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;replace&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;gen_uuid&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;x&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;k&quot;&gt;returns&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;uuid&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;language&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;sql&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;immutable&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;strict&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;parallel&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;safe&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;to_char&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;x&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'FM00000000-0000-0000-0000-000000000000'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)::&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;uuid&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;history&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;pk&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;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;operation&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;doc&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;user_id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;comment&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;gen_uuid&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;x&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;p&quot;&gt;),&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;((&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;array&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;'applications'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'organizations'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'users'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'events'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;])[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ceil&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;random&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;mi&quot;&gt;4&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;array&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;'update'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'delete'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;])[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ceil&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;random&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;mi&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)]),&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;jsonb_build_object&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
        &lt;span class=&quot;s1&quot;&gt;'foo'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;x&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;s1&quot;&gt;'bar'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;format&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;'some field %s'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;x&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
        &lt;span class=&quot;s1&quot;&gt;'test'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'hello'&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;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;interval&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'1 year'&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;random&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(),&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;gen_random_uuid&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(),&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;format&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;'comment %s'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;x&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;from&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;generate_series&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;mi&quot;&gt;1000&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;seq&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;x&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;history&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;limit&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;10&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;offset&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;855&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;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;┌─[ RECORD 1 ]────────────────────────────────────────────────────────┐
│ id         │ cc896171-b22e-443e-9f1e-d31cc1bf7d0b                   │
│ pk         │ 00000000-0000-0000-0000-000000000056                   │
│ entity     │ events                                                 │
│ operation  │ delete                                                 │
│ doc        │ {&quot;bar&quot;: &quot;some field 856&quot;, &quot;foo&quot;: 856, &quot;test&quot;: &quot;hello&quot;} │
│ created_at │ 2025-10-03 08:32:13.55007+03                           │
│ user_id    │ 6a7b7910-8baa-4cd3-8b7a-e2337e3430a8                   │
│ comment    │ comment 856                                            │
├─[ RECORD 2 ]────────────────────────────────────────────────────────┤
│ id         │ b878b211-11bf-4ce6-b3ab-dd23246cde6e                   │
│ pk         │ 00000000-0000-0000-0000-000000000057                   │
│ entity     │ organizations                                          │
│ operation  │ delete                                                 │
│ doc        │ {&quot;bar&quot;: &quot;some field 857&quot;, &quot;foo&quot;: 857, &quot;test&quot;: &quot;hello&quot;} │
│ created_at │ 2026-01-09 16:44:54.44607+03                           │
│ user_id    │ 2bec5043-f61e-4dfe-97ad-75fc95588174                   │
│ comment    │ comment 857                                            │
├─[ RECORD 3 ]────────────────────────────────────────────────────────┤
│ id         │ c92ee4a1-f634-4d94-bf59-0b9e727c17c5                   │
│ pk         │ 00000000-0000-0000-0000-000000000058                   │
│ entity     │ events                                                 │
│ operation  │ delete                                                 │
│ doc        │ {&quot;bar&quot;: &quot;some field 858&quot;, &quot;foo&quot;: 858, &quot;test&quot;: &quot;hello&quot;} │
│ created_at │ 2026-03-15 23:42:29.06367+03                           │
│ user_id    │ baa57871-0134-4b66-8c73-bfa6772f2b84                   │
│ comment    │ comment 858                                            │
├─[ RECORD 4 ]────────────────────────────────────────────────────────┤
&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;k&quot;&gt;count&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;from&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;history&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;where&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;pk&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'00000000-0000-0000-0000-000000000056'&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;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;┌───────┐
│ count │
├───────┤
│    10 │
└───────┘
&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;unique&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;index&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;idx_history_pg_created_at&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;on&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;history&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;pk&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;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;/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;created_at&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;history&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;where&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;pk&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'00000000-0000-0000-0000-000000000056'&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;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                  │          created_at          │
├──────────────────────────────────────┼──────────────────────────────┤
│ 13aad2e6-dc8d-4506-a502-4a906f5098ae │ 2026-05-25 13:51:35.42847+03 │
│ 61e69328-cf7d-4c08-b1e4-4348ec90a355 │ 2026-03-06 13:49:14.76927+03 │
│ 2114a3bd-8227-4fc2-8d19-792b67efb24c │ 2026-02-28 22:41:54.82047+03 │
│ 41e96a49-dddc-45c2-acec-440476fcbae7 │ 2026-01-26 15:56:18.70527+03 │
│ 9d0bb1e6-31a5-4c15-b860-56ca36bb2bf1 │ 2026-01-03 04:48:32.26047+03 │
│ 44c8ddf0-0a24-4f98-b05f-7cd920bf06f0 │ 2025-11-27 12:14:12.45567+03 │
│ af7004de-ac56-4263-818f-b1de9d07dcaf │ 2025-11-08 01:02:17.17887+03 │
│ cc896171-b22e-443e-9f1e-d31cc1bf7d0b │ 2025-10-03 08:32:13.55007+03 │
│ 2e903e63-65e8-4b78-89f2-938415d5e762 │ 2025-09-20 21:43:27.49887+03 │
│ 8b6d1d09-568e-481a-a96f-a8e3cd9626d7 │ 2025-07-19 18:00:57.48927+03 │
└──────────────────────────────────────┴──────────────────────────────┘
&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;LIMIT 1&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;created_at&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;history&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;where&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;pk&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'00000000-0000-0000-0000-000000000056'&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;1&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;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                  │          created_at          │
├──────────────────────────────────────┼──────────────────────────────┤
│ 13aad2e6-dc8d-4506-a502-4a906f5098ae │ 2026-05-25 13:51:35.42847+03 │
└──────────────────────────────────────┴──────────────────────────────┘
&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;created_at&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;created_at&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;history&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;where&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;pk&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'00000000-0000-0000-0000-000000000056'&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;and&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;s1&quot;&gt;'2025-11-08 00:00:00+03'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;timestamptz&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;1&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;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                  │          created_at          │
├──────────────────────────────────────┼──────────────────────────────┤
│ cc896171-b22e-443e-9f1e-d31cc1bf7d0b │ 2025-10-03 08:32:13.55007+03 │
└──────────────────────────────────────┴──────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Если предварить запросы командой EXPLAIN, план подтвердит, что мы попали в
индекс.&lt;/p&gt;

&lt;h2 id=&quot;создание-версий&quot;&gt;Создание версий&lt;/h2&gt;

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

&lt;p&gt;Уточним, что версию создают только обновлении или удалении документа. Это
значит, мы заинтересованы в запросах UPDATE и DELETE. При создании документа
(INSERT) порождать версию бессмысленно: она будет точной копией документа.&lt;/p&gt;

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

&lt;p&gt;Хотя автор не поощряет триггеры, наше первое решение будет основано на них.&lt;/p&gt;

&lt;h2 id=&quot;версии-на-триггерах&quot;&gt;Версии на триггерах&lt;/h2&gt;

&lt;p&gt;Чтобы при обновлении и удалении заявки в таблице &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;history&lt;/code&gt; появлялась версия
документа, проделаем следующее. Сперва подготовим триггерную функцию, которая
выполняет вставку в &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;history&lt;/code&gt;. Триггерные функции отличаются от обычных тем, что
им доступны переменные &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;OLD&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;NEW&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;TG_OP&lt;/code&gt; и другие. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;OLD&lt;/code&gt; и NEW Содержат
прежнюю и новую версии строки, связанной с событием. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;TG_OP&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;DELETE&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;or&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;replace&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;fn_applications_history&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;returns&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;trigger&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;$$&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;begin&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;history&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;pk&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;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;operation&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;doc&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;k&quot;&gt;values&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;OLD&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;s1&quot;&gt;'applications'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;TG_OP&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;old&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;doc&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&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;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;OLD&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&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;k&quot;&gt;language&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;plpgsql&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;create trigger&lt;/code&gt; связывает таблицу, событие и функцию, которую следует
вызывать. Вызовем команду дважды для обновления и удаления. Мы заинтересованы в
прошлой версии документа (до события), поэтому указываем &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;before delete/update&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;trigger&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;trg_application_before_delete&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;before&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;delete&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;on&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;applications&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;each&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;row&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;execute&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;fn_applications_history&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;trigger&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;trg_application_before_update&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;before&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;update&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;on&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;applications&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;each&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;row&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;execute&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;fn_applications_history&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;update&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;applications&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;set&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;doc&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;'extra'&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;to_jsonb&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;42&lt;/span&gt;&lt;span class=&quot;p&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;s1&quot;&gt;'00000000-0000-0000-0000-000000123999'&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;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;pk&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;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;operation&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;jsonb_pretty&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;doc&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;doc&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;history&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;where&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;pk&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'00000000-0000-0000-0000-000000123999'&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;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;┌─[ RECORD 1 ]───────────────────────────────────────────────────────────────────┐
│ id         │ 009cc4ce-ea6b-4ea1-87b1-bed7825c8781                              │
│ pk         │ 00000000-0000-0000-0000-000000123999                              │
│ entity     │ applications                                                      │
│ operation  │ UPDATE                                                            │
│ created_at │ 2026-06-21 15:52:54.03302+03                                      │
│ doc        │ {                                                                ↵│
│            │     &quot;id&quot;: &quot;00000000-0000-0000-0000-000000123999&quot;,                ↵│
│            │     &quot;status&quot;: &quot;approved&quot;,                                        ↵│
│            │     &quot;amounts&quot;: [                                                 ↵│
│            │         {                                                        ↵│
│            │             &quot;amount&quot;: 41513124,                                  ↵│
│            │             &quot;period&quot;: {                                          ↵│
│            │                 &quot;d&quot;: 4,                                          ↵│
│            │                 &quot;m&quot;: 8,                                          ↵│
│            │                 &quot;w&quot;: 7,                                          ↵│
│            │                 &quot;y&quot;: 1                                           ↵│
│            │             },                                                   ↵│
│            │             &quot;currency&quot;: &quot;USD&quot;                                    ↵│
&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;delete&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;applications&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;s1&quot;&gt;'00000000-0000-0000-0000-000000012345'&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;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;pk&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;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;operation&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;jsonb_pretty&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;doc&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;doc&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;history&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;where&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;pk&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'00000000-0000-0000-0000-000000012345'&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;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;┌─[ RECORD 1 ]───────────────────────────────────────────────────────────────────┐
│ id         │ e0437f5d-19b6-4850-a2ae-51b21e626a0e                              │
│ pk         │ 00000000-0000-0000-0000-000000012345                              │
│ entity     │ applications                                                      │
│ operation  │ DELETE                                                            │
│ created_at │ 2026-06-21 15:48:55.118652+03                                     │
│ doc        │ {                                                                ↵│
│            │     &quot;id&quot;: &quot;00000000-0000-0000-0000-000000012345&quot;,                ↵│
│            │     &quot;status&quot;: &quot;archived&quot;,                                        ↵│
│            │     &quot;amounts&quot;: [                                                 ↵│
│            │         {                                                        ↵│
│            │             &quot;amount&quot;: 40528181,                                  ↵│
│            │             &quot;period&quot;: {                                          ↵│
│            │                 &quot;d&quot;: 7,                                          ↵│
│            │                 &quot;m&quot;: 7,                                          ↵│
&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;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;UPDATE&lt;/code&gt; и &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DELETE&lt;/code&gt;. У этой строгости недостаток: она не допускает исключений, а
порой они необходимы. Например, при изменении некоторых полей создавать версии
не нужно. Также они не требуются в служебных миграциях: исправлении дат, часовых
поясов, корректировки ссылок. Наоборот, будет хуже, если мы исправили миллион
документов и получили лишний миллион версий.&lt;/p&gt;

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

&lt;p&gt;Можно временно отключить триггеры для тех тестов, которые в них не
нуждаются. Для этого служит команда &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;alter table ... disable trigger&lt;/code&gt; и ее
обратное действие &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;enable&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;alter&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;table&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;applications&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;disable&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;trigger&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;trg_application_before_delete&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;cm&quot;&gt;/* run all tests */&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;applications&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;disable&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;trigger&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;trg_application_before_delete&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;/p&gt;

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

&lt;h2 id=&quot;версии-в-приложении&quot;&gt;Версии в приложении&lt;/h2&gt;

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

&lt;p&gt;Идея в том, чтобы назначить модели событие before update или before delete, а в
обработчике записать документ в историческую таблицу. Приведем пример для ORM из
фреймворка Django:&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;kn&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;django.db.models.signals&lt;/span&gt; &lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;pre_save&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;django.dispatch&lt;/span&gt; &lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;receiver&lt;/span&gt;

&lt;span class=&quot;kn&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;project.models&lt;/span&gt; &lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Application&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;History&lt;/span&gt;

&lt;span class=&quot;o&quot;&gt;@&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;receiver&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;pre_save&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;sender&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Application&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;save_history&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;sender&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;instance&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;kwargs&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;ow&quot;&gt;not&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;instance&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;pk&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;n&quot;&gt;History&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;objects&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;create&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;pk&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;instance&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;entity&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;'application'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;operation&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;'UPDATE'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;doc&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;instance&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;doc&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;receiver&lt;/code&gt; объединяет событие &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pre_save&lt;/code&gt; (до записи в базу), модель
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Application&lt;/code&gt; и обработчик &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;save_history&lt;/code&gt;. Последний сработает до того, как
вызван метод &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.save()&lt;/code&gt; модели. Параметр &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;instance&lt;/code&gt; содержит саму модель
(экземпляр Application), которую мы намерены изменить. Если она новая (первичный
ключ не заполнен), версия не создается.&lt;/p&gt;

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

&lt;p&gt;По аналогии устроена реакция на удаление модели: разница лишь в типе сигнала
(&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pre_delete&lt;/code&gt;).&lt;/p&gt;

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

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

&lt;h2 id=&quot;версии-в-postgres&quot;&gt;Версии в Postgres&lt;/h2&gt;

&lt;p&gt;Опишем удаление документа на SQL. Это подготовленный оператор с одним параметром
– ключом документа. В нем две операции: удаление из основной таблицы и вставка в
историческую. Обе операции выполняются атомарно, им доступен один и тот же
снимок данных.&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;prepare&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;delete_application&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;with&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;OLD&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&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;applications&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;err&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;n&quot;&gt;uuid&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;returning&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;insert&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;into&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;history&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;pk&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;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;operation&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;doc&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;k&quot;&gt;select&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;OLD&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;s1&quot;&gt;'applications'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;s1&quot;&gt;'DELETE'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;old&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;doc&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;current_timestamp&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;from&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;OLD&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;Сперва запрос с псевдонимом OLD удаляет документ из основной таблицы, при этом
удаленные строки возвращаются. В главном запросе OLD выступает источником
данных: строки из него вставляются в таблицу версий. Данные словно перетекают из
одной таблицы в другую. Процесс происходит атомарно и без нашего участия: клиент
ничего не передает через себя, а только командует.&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;execute&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;delete_application&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;'00000000-0000-0000-0000-000000100999'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;uuid&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;id&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;applications&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;s1&quot;&gt;'00000000-0000-0000-0000-000000100999'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;-- (0 rows)&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;history&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;pk&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;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;operation&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;jsonb_pretty&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;doc&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;doc&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;history&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;where&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;pk&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'00000000-0000-0000-0000-000000100999'&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;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;┌─[ RECORD 1 ]───────────────────────────────────────────────────────────────────┐
│ id         │ 78ce995b-c865-489c-8c24-0ab26d62caef                              │
│ pk         │ 00000000-0000-0000-0000-000000100999                              │
│ entity     │ applications                                                      │
│ operation  │ DELETE                                                            │
│ created_at │ 2026-06-21 16:16:23.510292+03                                     │
│ doc        │ {                                                                ↵│
│            │     &quot;id&quot;: &quot;00000000-0000-0000-0000-000000100999&quot;,                ↵│
│            │     &quot;status&quot;: &quot;archived&quot;,                                        ↵│
│            │     &quot;amounts&quot;: [                                                 ↵│
│            │         {                                                        ↵│
│            │             &quot;amount&quot;: 11520867,                                  ↵│
│            │             &quot;period&quot;: {                                          ↵│
│            │                 &quot;d&quot;: 2,                                          ↵│
│            │                 &quot;m&quot;: 8,                                          ↵│
│            │                 &quot;w&quot;: 6,                                          ↵│
│            │                 &quot;y&quot;: 1                                           ↵│
│            │             },                                                   ↵│
│            │             &quot;currency&quot;: &quot;EUR&quot;                                    ↵│
&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;update_application&lt;/code&gt; ниже ожидает два
параметра: ключ документа &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;$1::uuid&lt;/code&gt; и его тело &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;$2::jsonb&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;prepare&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;update_application&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;with&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;OLD&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&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;applications&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;err&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;k&quot;&gt;NEW&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&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;applications&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;set&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;doc&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;mi&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;jsonb&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;err&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;n&quot;&gt;uuid&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;returning&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;insert&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;into&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;history&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;pk&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;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;operation&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;doc&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;k&quot;&gt;select&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;OLD&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;s1&quot;&gt;'applications'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;s1&quot;&gt;'UPDATE'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;OLD&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;doc&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;current_timestamp&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;from&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;OLD&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;ul&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;OLD&lt;/code&gt; читает прежний документ;&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;NEW&lt;/code&gt; обновляет текущий;&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;insert&lt;/code&gt; записывает &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;OLD&lt;/code&gt; в историческую таблицу.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Обновим одну из заявок и убедимся, что в таблице &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;applications&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;execute&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;update_application&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;s1&quot;&gt;'00000000-0000-0000-0000-000000100321'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;uuid&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;nv&quot;&gt;&quot;application_id&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;100321&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;nv&quot;&gt;&quot;some_field&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;&quot;test&quot;&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;p&quot;&gt;::&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;select&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;doc&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;applications&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;s1&quot;&gt;'00000000-0000-0000-0000-000000100321'&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;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;┌─[ RECORD 1 ]───────────────────────────────────────────┐
│ doc │ {&quot;some_field&quot;: &quot;test&quot;, &quot;application_id&quot;: 100321} │
└─────┴──────────────────────────────────────────────────┘
&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;n&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;pk&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;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;operation&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;jsonb_pretty&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;doc&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;doc&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;history&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;where&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;pk&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'00000000-0000-0000-0000-000000100321'&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;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;┌─[ RECORD 1 ]───────────────────────────────────────────────────────────────────┐
│ id         │ 47e6cdba-e1fe-4320-acdb-ef193ef7f3e6                              │
│ pk         │ 00000000-0000-0000-0000-000000100321                              │
│ entity     │ applications                                                      │
│ operation  │ UPDATE                                                            │
│ created_at │ 2026-06-21 16:27:53.384049+03                                     │
│ doc        │ {                                                                ↵│
│            │     &quot;id&quot;: &quot;00000000-0000-0000-0000-000000100321&quot;,                ↵│
│            │     &quot;status&quot;: &quot;archived&quot;,                                        ↵│
│            │     &quot;amounts&quot;: [                                                 ↵│
│            │         {                                                        ↵│
│            │             &quot;amount&quot;: 50222648,                                  ↵│
│            │             &quot;period&quot;: {                                          ↵│
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;ограничение-по-времени&quot;&gt;Ограничение по времени&lt;/h2&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;prepare&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;update_application_throttled&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;with&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;upsert&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&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;applications&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;doc&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&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;err&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;n&quot;&gt;uuid&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;mi&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;::&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;on&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;conflict&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;k&quot;&gt;do&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;update&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;set&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;doc&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;excluded&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;DOC&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;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;now&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;returning&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;NEW&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;k&quot;&gt;OLD&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;doc&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;doc_old&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;history&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;pk&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;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;operation&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;doc&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;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;s1&quot;&gt;'applications'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;s1&quot;&gt;'UPDATE'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;doc_old&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;current_timestamp&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;from&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;upsert&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;where&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;doc_old&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;is&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;k&quot;&gt;and&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;not&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;exists&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;id&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;history&lt;/span&gt;
      &lt;span class=&quot;k&quot;&gt;where&lt;/span&gt;
          &lt;span class=&quot;n&quot;&gt;pk&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;upsert&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;and&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;created_at&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;now&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;interval&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'1 minute'&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;not exists&lt;/code&gt;. Подзапрос ищет в
исторической таблице запись таким же ключом и датой создания позже порога. Если
запись нашлась, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;not exists&lt;/code&gt; вернет ложь. В результате &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;insert&lt;/code&gt; получит пустой
набор записей, и версия не будет создана.&lt;/p&gt;

&lt;p&gt;Еще одно улучшение кроется в части &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;upsert&lt;/code&gt;. Как ясно из названия, она не только
обновляет документ, но и создает в случае отсутствия. При этом возвращается
прежнее значение документа – поле &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;OLD.doc&lt;/code&gt; под именем &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;doc_old&lt;/code&gt;. За счет него
мы определим тип события: вставку или обновление. Значение &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;doc_old&lt;/code&gt; помещается в
историческую таблицу, если оно не пустое. В противном случае документ был
создан, и порождать версию нет смысла.&lt;/p&gt;

&lt;p&gt;Заметим, что в выражении &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;on conflict&lt;/code&gt; переменные &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;OLD&lt;/code&gt; и &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;NEW&lt;/code&gt; доступны с
версии 18. Для младших версий мы бы переписали запрос так, чтобы &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;OLD&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;with&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;OLD&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&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;applications&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;err&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;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Теперь проверим наш код. Для начала вставим простейший документ &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;{&quot;foo&quot;:
&quot;1&quot;}&lt;/code&gt;. Оператор вернул &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;INSERT 0 0&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;execute&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;update_application_throttled&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;s1&quot;&gt;'00000000-0000-0000-0000-000010123123'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;uuid&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;nv&quot;&gt;&quot;foo&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;&quot;1&quot;&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;p&quot;&gt;::&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;c1&quot;&gt;-- INSERT 0 0&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;INSERT 0 1&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;execute&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;update_application_throttled&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;s1&quot;&gt;'00000000-0000-0000-0000-000010123123'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;uuid&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;nv&quot;&gt;&quot;foo&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;&quot;2&quot;&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;p&quot;&gt;::&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;c1&quot;&gt;-- INSERT 0 1&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;execute&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;update_application_throttled&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;s1&quot;&gt;'00000000-0000-0000-0000-000010123123'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;uuid&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;nv&quot;&gt;&quot;foo&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;&quot;3&quot;&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;p&quot;&gt;::&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;c1&quot;&gt;-- INSERT 0 0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Проверим таблицы: в application хранится текущий документ &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;{&quot;foo&quot;: &quot;3&quot;}&lt;/code&gt;, а в
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;history&lt;/code&gt; – версия &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;{&quot;foo&quot;: &quot;1&quot;}&lt;/code&gt;. Версию &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;{&quot;foo&quot;: &quot;2&quot;}&lt;/code&gt; мы пропустили, потому что с
момента &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;{&quot;foo&quot;: &quot;1&quot;}&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;table applications;
┌─[ RECORD 1 ]──────────────────────────────────────┐
│ id         │ 00000000-0000-0000-0000-000010123123 │
│ doc        │ {&quot;foo&quot;: &quot;3&quot;}                         │
│ created_at │ 2026-08-18 09:32:46.966243+03        │
│ updated_at │ 2026-08-18 09:33:00.341051+03        │
└────────────┴──────────────────────────────────────┘

table history;
┌─[ RECORD 1 ]──────────────────────────────────────┐
│ id         │ 8e6b2261-76b1-4aa2-8e4f-5771faaf1e8f │
│ pk         │ 00000000-0000-0000-0000-000010123123 │
│ entity     │ applications                         │
│ operation  │ UPDATE                               │
│ doc        │ {&quot;foo&quot;: &quot;1&quot;}                         │
│ created_at │ 2026-08-18 09:32:54.304452+03        │
│ user_id    │ &amp;lt;null&amp;gt;                               │
│ comment    │ &amp;lt;null&amp;gt;                               │
└────────────┴──────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Подождем 5 минут и обновим документ еще раз:&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;execute&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;update_application_throttled&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;s1&quot;&gt;'00000000-0000-0000-0000-000010123123'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;uuid&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;nv&quot;&gt;&quot;foo&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;&quot;4&quot;&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;p&quot;&gt;::&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;c1&quot;&gt;-- INSERT 0 1&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;{&quot;foo&quot;: &quot;3&quot;}&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;table history;
┌─[ RECORD 1 ]──────────────────────────────────────┐
│ id         │ 8e6b2261-76b1-4aa2-8e4f-5771faaf1e8f │
│ pk         │ 00000000-0000-0000-0000-000010123123 │
│ entity     │ applications                         │
│ operation  │ UPDATE                               │
│ doc        │ {&quot;foo&quot;: &quot;1&quot;}                         │
│ created_at │ 2026-08-18 09:32:54.304452+03        │
│ user_id    │ &amp;lt;null&amp;gt;                               │
│ comment    │ &amp;lt;null&amp;gt;                               │
├─[ RECORD 2 ]──────────────────────────────────────┤
│ id         │ 5fa063d6-5e43-4cb3-b1ef-8d12d82657fa │
│ pk         │ 00000000-0000-0000-0000-000010123123 │
│ entity     │ applications                         │
│ operation  │ UPDATE                               │
│ doc        │ {&quot;foo&quot;: &quot;3&quot;}                         │
│ created_at │ 2026-08-18 09:44:15.368447+03        │
│ user_id    │ &amp;lt;null&amp;gt;                               │
│ comment    │ &amp;lt;null&amp;gt;                               │
└────────────┴──────────────────────────────────────┘
&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;/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;/ul&gt;

&lt;p&gt;Получаются две перестановки: из главной таблицы в историческую и
наоборот. Приведем подготовленный оператор &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;restore_application&lt;/code&gt;, выполняющий эти
действия. Ключ документа передается первым параметром (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;$1::uuid&lt;/code&gt;), версия –
вторым (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;$2::timestamptz&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;prepare&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;restore_application&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;with&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;del_current_doc&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&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;applications&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;err&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;n&quot;&gt;uuid&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;returning&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;create_version&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&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;history&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;pk&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;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;operation&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;doc&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;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;s1&quot;&gt;'applications'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;s1&quot;&gt;'RESTORE'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;doc&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;current_timestamp&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;del_current_doc&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;delete_version&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&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;history&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;where&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;pk&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;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;uuid&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;and&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;err&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;n&quot;&gt;timestamptz&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;returning&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;insert&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;into&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;applications&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;doc&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;p&quot;&gt;)&lt;/span&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;doc&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;current_timestamp&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;from&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;delete_version&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;{&quot;foo&quot;: &quot;4&quot;}&lt;/code&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;ul&gt;
  &lt;li&gt;выбрать документы старше определенного срока;&lt;/li&gt;
  &lt;li&gt;записать их в файл, сжать и передать в долгосрочное хранилище;&lt;/li&gt;
  &lt;li&gt;удалить из базы обработанные документы.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Задача выполняется регулярно, например раз в месяц.&lt;/p&gt;

&lt;p&gt;Архивация решается разными способами, в том числе силами Java, Python и других
языков. Ниже мы рассмотрим, как сделать это в Postgres без привлечения других
технологий. Изучите следующий запрос:&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;copy&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&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;history&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;n&quot;&gt;now&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;interval&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'1 year'&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;returning&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;to&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;program&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'gzip &amp;gt; /path/to/history.csv.gzip'&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;with&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;format&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;csv&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;header&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;on&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;DELETE&lt;/code&gt;). Данные,
что он удалил, возвращаются оператором &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;RETURNING&lt;/code&gt;. Выражение &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;COPY&lt;/code&gt; приводит
записи к формату CSV и передает процессу gzip, а тот сжимает результат в файл.&lt;/p&gt;

&lt;p&gt;Вместо &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;to program&lt;/code&gt; можно указать &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;to stdout&lt;/code&gt;, и тогда данные получит
клиент. Уточним, что в терминах Postgres под &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;stdout&lt;/code&gt; понимается не стандартный
канал Unix, а клиентская сторона. Postgres передает серию сообщений &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;CopyData&lt;/code&gt;;
клиент принимает их и как-то обрабатывает, например записывает в файл.&lt;/p&gt;

&lt;p&gt;Тонкости оператора COPY мы рассмотрели в седьмой главе про
отчетность. Обратитесь к ней, если что-то непонятно.&lt;/p&gt;

&lt;p&gt;Напомним, что CSV-файлы подлежат сжатию. Даже обычные алгоритмы zip и gzip будут
хорошим выбором. Еще большего выигрыша можно добиться, выбрав lz4 (утилита
доступна во всех менеджерах пакетов). Алгоритмы LZMA2 и ZPAQ предлагают своего
рода компромисс: крайне эффективное сжатие при медленной распаковке. Как
правило, именно их выбирают для долгосрочного хранения версий, логов и подобной
информации. Распаковка случается редко, а остальное время мы платим за объем.&lt;/p&gt;

&lt;p&gt;Для полноты картины приведем процедуру &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;truncate_and_dump_history&lt;/code&gt;, которая
записывает удаленные версии в файл. Особенность в том, что выражение copy
строится функцией format и выполняется оператором execute. Форматирование
необходимо, чтобы путь к файлу включал в себя год, месяц и день. Для этого
параметр &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;%s&lt;/code&gt; внутри пути заменяется значением &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;to_char(now(), 'yyyy_mm_dd')&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;or&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;replace&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;procedure&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;truncate_and_dump_history&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;language&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;plpgsql&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;$$&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;begin&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;execute&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;format&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;k&quot;&gt;sql&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;$&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;copy&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&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;history&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;n&quot;&gt;now&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;interval&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'1 year'&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;returning&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;to&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;program&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'gzip &amp;gt; /Users/ivan/work/pg-json-book-code/history_%s.csv.gzip'&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;with&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;format&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;csv&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;header&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;on&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;k&quot;&gt;sql&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;to_char&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;p&quot;&gt;(),&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'yyyy_mm_dd'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;));&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&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;p&quot;&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Мы идем на подобные ухищрения, потому что copy не принимает параметров: все
значения должны находиться в теле запроса. Процедуру легко расширить так, чтобы
срок давности (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;interval '1 year'&lt;/code&gt;) передавался параметром, ровно как и
директория, куда записывать файл.&lt;/p&gt;

&lt;p&gt;Как и в случае отчетами, задачу ставят на расписание при помощи расширения
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pg_cron&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;call&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;truncate_and_dump_history&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;h2 id=&quot;стандарт-json-patch&quot;&gt;Стандарт JSON Patch&lt;/h2&gt;

&lt;p&gt;Последний вопрос этой главы касается стандарта JSON Patch (не путайте с JSON
Path). Прежде чем рассказать, что это такое, рассмотрим, какую задачу мы
надеемся решить с его помощью.&lt;/p&gt;

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

&lt;p&gt;Мы храним версии документа, но не можем ответить на вопрос, что именно в нем
изменилось. Порой это необходимо: клиенты просят, чтобы при просмотре документа
в боковом виджете были недавние изменения. Программистам тоже удобно, когда
система предлагает быстрый способ сравнить документы. Это полезно при
расследовании инцидентов или ошибок операторов. Сравнение документов вручную в
программе &lt;a href=&quot;https://kdiff3.sourceforge.net/&quot;&gt;Kdiff3&lt;/a&gt; и аналогах занимает много времени. Здесь и приходит
на помощь JSON Patch.&lt;/p&gt;

&lt;p&gt;JSON Patch – это соглашение о том, как описать изменения в
JSON-документе. Стандарт доступен на &lt;a href=&quot;https://jsonpatch.com/&quot;&gt;официальном сайте&lt;/a&gt; по
одноименному адресу. JSON Patch – это даже не язык, а данные: массив объектов с
полями op, path, value и некоторые другие. Каждый объект описывает одну операцию
над документом, а массив определяет порядок.&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;a&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;c&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;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;mi&quot;&gt;3&lt;/span&gt;&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;d&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;kc&quot;&gt;true&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;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;b&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;c&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;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;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;mi&quot;&gt;3&lt;/span&gt;&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;Какие действия необходимы, чтобы перейти от первого ко второму? Вот как выглядит
ответ на языке JSON Patch:&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;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;op&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;remove&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;path&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;/d&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;op&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;replace&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;path&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;/c/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;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;op&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;add&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;path&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;/c/2&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;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;3&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;op&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;remove&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;path&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;/a&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;op&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;add&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;path&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;/b&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;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;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

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

&lt;ul&gt;
  &lt;li&gt;Сначала из документа удаляется поле &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;d&lt;/code&gt; (путь &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/d&lt;/code&gt;).&lt;/li&gt;
  &lt;li&gt;В массиве &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&quot;c&quot;&lt;/code&gt; второй элемент меняется с 3 на 2.&lt;/li&gt;
  &lt;li&gt;К списку добавляется элемент 3.&lt;/li&gt;
  &lt;li&gt;Удаляется поле &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;a&lt;/code&gt;.&lt;/li&gt;
  &lt;li&gt;Добавляется поле &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;b&lt;/code&gt; со значением 2.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;Не путайте JSON Patch с JSON Path (англ. patch – “заплатка”, path – “путь”). Это
разные стандарты, причем первый описывает данные, а второй – язык. В разговорной
речи JSON Patch называют патчем, заплаткой или “дифом” от английского difference
– “разница”.&lt;/p&gt;

&lt;p&gt;Из описания видно, что JSON Patch довольно прост. Он подразумевает всего шесть
операций над документом: add, remove, replace, copy, move, test. Имея исходный
документ и список изменений, легко применить их в цикле. Вычисление разницы
между документами – напротив, сложная задача, и чуть ниже мы ее рассмотрим.&lt;/p&gt;

&lt;p&gt;Замечания заслуживает поле “path” – путь к элементу. С этим полем связан еще
один стандарт – Jsonpointer, который определяет, как строится путь к
произвольному JSON-элементу. Стандарт имеет код &lt;a href=&quot;https://datatracker.ietf.org/doc/html/rfc6901&quot;&gt;RFC 6901&lt;/a&gt; и может быть
прочитан на сайте ietf.org.&lt;/p&gt;

&lt;p&gt;Теперь когда мы знакомы с JSON Patch, объясним, как он поможет в версионировании
документов. Если пользователи часто смотрят историю изменений, их можно
материализовать – хранить в отдельной колонке. Для этого добавим в таблицу
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;history&lt;/code&gt; поле patch с типом 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;alter&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;table&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;history&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;patch&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;jsonb&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;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Ожидается, что в нем хранится патч – JSON-массив объектов. Если клиент хочет
знать, какие изменения произошли в указанной версии, он передает ключ документа
и версию. В ответ ему высылают содержимое колонки patch, например:&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;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;op&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;add&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;path&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;/status&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;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;pending&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;nl&quot;&gt;&quot;op&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;add&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;path&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;/accounts&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;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;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;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;mi&quot;&gt;3&lt;/span&gt;&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;nl&quot;&gt;&quot;op&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;replace&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;path&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_field&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;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;foo&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;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

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

&lt;h2 id=&quot;патчи-в-приложении&quot;&gt;Патчи в приложении&lt;/h2&gt;

&lt;p&gt;Подумаем теперь, как заполнить колонку &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;patch&lt;/code&gt;. Можно сделать это двумя способами:
в приложении и в базе данных. Если приложение написано на Python, для работы с
патчами понадобится пакет &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;jsonpatch&lt;/code&gt;. Установите его командой:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;pip3 &lt;span class=&quot;nb&quot;&gt;install &lt;/span&gt;jsonpatch
&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;JsonPatch.from_diff&lt;/code&gt; принимает две
структуры данных и возвращает экземпляр &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;JsonPatch&lt;/code&gt;. Приведение его к строке дает
патч в виде JSON. Метод apply принимает документ и применяет к нему патч,
возвращая новый документ. Этих средств достаточно для нашей работы. Небольшая
демонстрация:&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;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;jsonpatch&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# declare two documents
&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;doc_old&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;s&quot;&gt;&quot;a&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;c&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;1&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;d&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;n&quot;&gt;doc_new&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;s&quot;&gt;&quot;c&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;1&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;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;b&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;c1&quot;&gt;# create a patch object
&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;patch&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;jsonpatch&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;JsonPatch&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;from_diff&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;doc_old&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;doc_new&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# get JSON representation of the patch
&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;str&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;patch&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;# [{&quot;op&quot;: &quot;remove&quot;, &quot;path&quot;: &quot;/a&quot;}, {&quot;op&quot;: &quot;remove&quot;, &quot;path&quot;: &quot;/d&quot;}, {&quot;op&quot;: &quot;add&quot;, &quot;path&quot;: &quot;/b&quot;, &quot;value&quot;: 2}, {&quot;op&quot;: &quot;add&quot;, &quot;path&quot;: &quot;/c/1&quot;, &quot;value&quot;: 2}]
&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;# Apply the patch to the old document
&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;doc_new2&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;patch&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;apply&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;doc_old&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# Both new docs are equal
&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;doc_new&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;doc_new2&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;# True
&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;patch&lt;/code&gt;. Необходимы
старый и новый экземпляры документа, чтобы передать их в метод &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;from_diff&lt;/code&gt; и этот
самый патч получить. При этом вопрос, в какой момент вам доступны оба
экземпляра, зависит от фреймворка и библиотек.&lt;/p&gt;

&lt;p&gt;Django ORM не предлагает сигнала, который принимал бы оба экземпляра модели
(условные &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;instance_old&lt;/code&gt; и &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;instance_new&lt;/code&gt;). Вместо этого регистрируют сигнал
pre_save, где instance содержит новый экземпляр модели, а прежний мы получим,
обратившись к базе методом &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.get&lt;/code&gt;. При этом учитываем, что экземпляр может быть
новым (не заполнено поле &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.pk&lt;/code&gt;) либо прежнего документа почему-то не существует
(исключение &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DoesNotExist&lt;/code&gt;). В обоих случаях сигнал прекращает работу. Если все
идет по плану, из полей &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;doc&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;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;jsonpatch&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;django.db.models.signals&lt;/span&gt; &lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;pre_save&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;django.forms.models&lt;/span&gt; &lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;model_to_dict&lt;/span&gt;

&lt;span class=&quot;o&quot;&gt;@&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;receiver&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;pre_save&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;sender&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Application&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;save_patch&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;sender&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;instance&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;kwargs&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;ow&quot;&gt;not&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;instance&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;pk&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;k&quot;&gt;try&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;app_old&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Application&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;objects&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;pk&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;instance&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;pk&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;except&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;sender&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;DoesNotExist&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;n&quot;&gt;patch&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;jsonpatch&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;JsonPatch&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;from_diff&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;app_old&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;doc&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;instance&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;doc&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

    &lt;span class=&quot;n&quot;&gt;History&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;objects&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;create&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;pk&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;instance&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;entity&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;'application'&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;patch&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;patch&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;to_string&lt;/span&gt;&lt;span class=&quot;p&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;Еще одно средство Django – класс &lt;a href=&quot;https://django-model-utils.readthedocs.io/en/latest/utilities.html#field-tracker&quot;&gt;FieldTracker&lt;/a&gt; из пакета Model
Utils. Это служебное поле, которое не сохраняется в базе, а только отслеживает
изменения других полей. Вот как выглядит с ним модель:&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;kn&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;django.db&lt;/span&gt; &lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;models&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;model_utils&lt;/span&gt; &lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;FieldTracker&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Application&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;models&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;doc&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;JSONField&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;tracker&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;FieldTracker&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;


&lt;span class=&quot;n&quot;&gt;app&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Application&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;objects&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;pk&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;app&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;doc&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;extra&quot;&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;mi&quot;&gt;123&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;app&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;save&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;pre_save&lt;/code&gt; мы получим прежнее значение &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;doc&lt;/code&gt; методом previous. За счет
этого не понадобится запрос к базе:&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;o&quot;&gt;@&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;receiver&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;pre_save&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;sender&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Application&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;save_patch&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;sender&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;instance&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;kwargs&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;ow&quot;&gt;not&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;instance&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;pk&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;n&quot;&gt;doc_old&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;instance&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;tracker&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;previous&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;doc&quot;&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;ow&quot;&gt;not&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;doc_old&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;n&quot;&gt;patch&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;jsonpatch&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;JsonPatch&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;from_diff&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;doc_old&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;instance&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;doc&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

    &lt;span class=&quot;n&quot;&gt;History&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;objects&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;create&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;pk&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;instance&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;entity&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;'application'&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;patch&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;patch&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;to_string&lt;/span&gt;&lt;span class=&quot;p&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;На официальном сайте JSON Patch собраны к библиотеки к популярным языкам
программирования. Скорее всего, среди них найдется и тот, на котором написано
ваше приложение.&lt;/p&gt;

&lt;h2 id=&quot;патчи-в-базе-данных&quot;&gt;Патчи в базе данных&lt;/h2&gt;

&lt;p&gt;Рассмотрим другую сторону вопроса: как работать с JSON Patch на уровне базы
данных? По умолчанию Postgres не предлагает для этого никаких
инструментов. Однако мы умеем работать с патчами на языке Python, а последний
поддерживается в Postgres. Отсюда решение: напишем функцию на языке plpython3,
которая сделает все необходимое.&lt;/p&gt;

&lt;p&gt;Для начала установите пакет &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;jsonpatch&lt;/code&gt; в тот интерпретатор, который
используется расширением &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;plpython3&lt;/code&gt;. Напомним, что одна из частых ошибок в том,
что пакет установлен в другой интерпретатор и не находится при запуске кода в
Postgres.&lt;/p&gt;

&lt;p&gt;Создайте и опробуйте функцию &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;py_make_patch&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;or&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;replace&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;py_make_patch&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;src&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;n&quot;&gt;dst&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;returns&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;jsonb&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;language&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;plpython3u&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;strict&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;$$&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;json&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;jsonpatch&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;doc_src&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;json&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;loads&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;src&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;doc_dst&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;json&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;loads&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;dst&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;patch&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;jsonpatch&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;JsonPatch&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;from_diff&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;doc_src&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;doc_dst&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;n&quot;&gt;patch&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;to_string&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;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;transform for type
jsonb&lt;/code&gt;. Дело в том, что приведение класса &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;JsonPatch&lt;/code&gt; к jsonb силами Postgres
влечет некоторые сложности, в которые мы не будем углубляться. Заинтересованному
читателю предлагаем домашнюю работу: добавить в функцию &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;transform for type
jsonb&lt;/code&gt; и избавиться от вызовов &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;json.loads&lt;/code&gt;. Вам понадобится знание о внутреннем
устройстве класса &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;JsonPatch&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;select&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;py_make_patch&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;'{&quot;a&quot;: 1}'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;::&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;s1&quot;&gt;'{&quot;a&quot;: 2}'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;::&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;as&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;patch&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;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;┌───────────────────────────────────────────────┐
│                     patch                     │
├───────────────────────────────────────────────┤
│ [{&quot;op&quot;: &quot;replace&quot;, &quot;path&quot;: &quot;/a&quot;, &quot;value&quot;: 2}] │
└───────────────────────────────────────────────┘
&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;or&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;replace&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;py_apply_patch&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;doc&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;n&quot;&gt;patch&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;returns&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;jsonb&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;transform&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;jsonb&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;language&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;plpython3u&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;immutable&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;strict&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;$$&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;jsonpatch&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;patch_obj&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;jsonpatch&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;JsonPatch&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;patch&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;n&quot;&gt;patch_obj&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;apply&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;doc&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;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;n&quot;&gt;py_apply_patch&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;nv&quot;&gt;&quot;some_field&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;&quot;test&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;&quot;application_id&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;100321&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;p&quot;&gt;::&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;err&quot;&gt;$$&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;nv&quot;&gt;&quot;op&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;&quot;add&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;&quot;path&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;&quot;/status&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;&quot;value&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;&quot;pending&quot;&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;}&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;nv&quot;&gt;&quot;op&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;&quot;add&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;&quot;path&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;&quot;/accounts&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;&quot;value&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;1&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;mi&quot;&gt;3&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;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;err&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;&quot;op&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;&quot;replace&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;&quot;path&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;&quot;/some_field&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;&quot;value&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;&quot;foo&quot;&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;}&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;p&quot;&gt;::&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;as&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;doc_new&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;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;┌─────────────────────────────────────────────────────────────────────────────────────────────┐
│                                           doc_new                                           │
├─────────────────────────────────────────────────────────────────────────────────────────────┤
│ {&quot;status&quot;: &quot;pending&quot;, &quot;accounts&quot;: [1, 2, 3], &quot;some_field&quot;: &quot;foo&quot;, &quot;application_id&quot;: 100321} │
└─────────────────────────────────────────────────────────────────────────────────────────────┘
&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;Теперь когда Postgres работает с патчами, перепишем версионирование. Вот как
выглядит оператор, который создает или обновляет документ; во втором случае
создается версия документа с патчем:&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;prepare&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;update_application&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;with&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;upsert&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&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;applications&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;doc&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&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;err&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;n&quot;&gt;uuid&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;mi&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;::&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;on&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;conflict&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;k&quot;&gt;do&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;update&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;set&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;doc&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;excluded&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;DOC&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;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;now&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;returning&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;NEW&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;k&quot;&gt;OLD&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;doc&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;doc_old&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;NEW&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;doc&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;doc_new&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;history&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;pk&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;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;operation&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;doc&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;patch&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;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;s1&quot;&gt;'applications'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;s1&quot;&gt;'UPDATE'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;doc_old&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;py_make_patch&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;doc_old&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;doc_new&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;current_timestamp&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;from&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;upsert&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;where&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;doc_old&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;is&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;/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;nl&quot;&gt;&quot;some_field&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;test&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;application_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;100321&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;execute&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;update_application&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;s1&quot;&gt;'00000000-0000-0000-0000-000000100321'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;uuid&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;nv&quot;&gt;&quot;application_id&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;100321&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;nv&quot;&gt;&quot;status&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;&quot;pending&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;nv&quot;&gt;&quot;some_field&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;&quot;foo&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;nv&quot;&gt;&quot;accounts&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;1&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;mi&quot;&gt;3&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;p&quot;&gt;::&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;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;pk&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;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;doc&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;jsonb_pretty&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;patch&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;patch&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;from&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;history&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;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;┌─[ RECORD 1 ]──────────────────────────────────────────────┐
│ id     │ dcbb3da3-5f2c-4ae7-861c-5c401c550905             │
│ pk     │ 00000000-0000-0000-0000-000000100321             │
│ entity │ applications                                     │
│ doc    │ {&quot;some_field&quot;: &quot;test&quot;, &quot;application_id&quot;: 100321} │
│ patch  │ [                                               ↵│
│        │     {                                           ↵│
│        │         &quot;op&quot;: &quot;add&quot;,                            ↵│
│        │         &quot;path&quot;: &quot;/status&quot;,                      ↵│
│        │         &quot;value&quot;: &quot;pending&quot;                      ↵│
│        │     },                                          ↵│
│        │     {                                           ↵│
│        │         &quot;op&quot;: &quot;add&quot;,                            ↵│
│        │         &quot;path&quot;: &quot;/accounts&quot;,                    ↵│
│        │         &quot;value&quot;: [                              ↵│
│        │             1,                                  ↵│
│        │             2,                                  ↵│
│        │             3                                   ↵│
│        │         ]                                       ↵│
│        │     },                                          ↵│
│        │     {                                           ↵│
│        │         &quot;op&quot;: &quot;replace&quot;,                        ↵│
│        │         &quot;path&quot;: &quot;/some_field&quot;,                  ↵│
│        │         &quot;value&quot;: &quot;foo&quot;                          ↵│
│        │     }                                           ↵│
│        │ ]                                                │
└────────┴──────────────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;В колонке patch оказались следующие операции:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;{&quot;op&quot;: &quot;add&quot;, &quot;path&quot;: &quot;/status&quot;, &quot;value&quot;: &quot;pending&quot;}&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;{&quot;op&quot;: &quot;add&quot;, &quot;path&quot;: &quot;/accounts&quot;, &quot;value&quot;: [1, 2, 3]}&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;{&quot;op&quot;: &quot;replace&quot;, &quot;path&quot;: &quot;/some_field&quot;, &quot;value&quot;: &quot;foo&quot;}&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

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

&lt;h2 id=&quot;прием-патчей-от-клиента&quot;&gt;Прием патчей от клиента&lt;/h2&gt;

&lt;p&gt;На базе JSON Patch можно построить еще одно решение. Если клиент хочет изменить
документ, можно принять от него только патч. Его применяют к текущему документу,
и дополнительно мы создаем версию. Вычислять патч при этом не нужно, потому что
он уже есть. Подход оправдывает себя на крупных документах, которые дорого
передавать по сети. Патч практически всегда меньше документа.&lt;/p&gt;

&lt;p&gt;Напомним, мы уже написали функцию &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;py_apply_patch&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;or&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;replace&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;py_apply_patch&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;doc&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;n&quot;&gt;patch&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;returns&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;jsonb&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;transform&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;jsonb&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;language&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;plpython3u&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;immutable&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;strict&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;$$&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;jsonpatch&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;patch_obj&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;jsonpatch&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;JsonPatch&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;patch&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;n&quot;&gt;patch_obj&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;apply&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;doc&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;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;patch_application_with_history&lt;/code&gt; от двух параметров: ключа
документа (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;$1::uuid&lt;/code&gt;) и патча (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;$2::jsonb&lt;/code&gt;). Стадия &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;step_old&lt;/code&gt; запоминает
документ до того, как мы наложим на него патч. Стадия &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;step_update&lt;/code&gt; обновляет
документ, задействуя функцию &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;py_apply_patch&lt;/code&gt;. В основной части мы записываем
версию документа, используя старое значение из &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;step_old&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;prepare&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;patch_application_with_history&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;with&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;step_old&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&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;applications&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;err&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;n&quot;&gt;uuid&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;step_update&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&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;applications&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;set&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;doc&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;py_apply_patch&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;doc&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;mi&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;::&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;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;err&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;n&quot;&gt;uuid&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;history&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;pk&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;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;operation&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;doc&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;patch&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;k&quot;&gt;select&lt;/span&gt;
      &lt;span class=&quot;n&quot;&gt;step_old&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;s1&quot;&gt;'applications'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;s1&quot;&gt;'UPDATE'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;n&quot;&gt;step_old&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;doc&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;mi&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;::&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;current_timestamp&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt;
      &lt;span class=&quot;n&quot;&gt;step_old&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;execute&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;patch_application_with_history&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;s1&quot;&gt;'00000000-0000-0000-0000-000000100321'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;uuid&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;p&quot;&gt;[&lt;/span&gt;
        &lt;span class=&quot;err&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;&quot;op&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;&quot;add&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;&quot;path&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;&quot;/comment&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;&quot;value&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;&quot;updated using a JSON patch&quot;&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;}&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;p&quot;&gt;::&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;/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;n&quot;&gt;jsonb_pretty&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;doc&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;doc&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;jsonb_pretty&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;patch&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;patch&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;from&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;history&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;where&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;pk&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'00000000-0000-0000-0000-000000100321'&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;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;┌──────────────────────────────┬───────────────────────────────────────────────┐
│             doc              │                     patch                     │
├──────────────────────────────┼───────────────────────────────────────────────┤
│ {                           ↵│ [                                            ↵│
│     &quot;some_field&quot;: &quot;test&quot;,   ↵│     {                                        ↵│
│     &quot;application_id&quot;: 100321↵│         &quot;op&quot;: &quot;add&quot;,                         ↵│
│ }                            │         &quot;path&quot;: &quot;/comment&quot;,                  ↵│
│                              │         &quot;value&quot;: &quot;updated using a JSON patch&quot;↵│
│                              │     }                                        ↵│
│                              │ ]                                             │
└──────────────────────────────┴───────────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

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

&lt;h2 id=&quot;коротко-об-event-sourcing&quot;&gt;Коротко об event-sourcing&lt;/h2&gt;

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

&lt;p&gt;Когда сущность строится из событий, это дает интересные возможности. В любой
момент ее можно “переиграть” – собрать с нуля. Если ограничить события датой
(например, не позже 2026 года), то получим версию документа в прошлом. В редких
случаях событие можно удалить и собрать агрегат без него. События полезны в
отладке и по-своему заменяют логи.&lt;/p&gt;

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

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

&lt;p&gt;Простым примером event-sourcing служит история банковского счета. Когда клиент
вносит деньги на счет, мы не просто увеличиваем баланс. Пополнение счета
записывается в таблицу операций и хранится вместе с другими фактами пополнения,
снятия, переводов, комиссий и других. Таким образом баланс – это сумма всех
операций. Баланс материализуют (хранят в отдельном поле), потому что
рассчитывать его каждый раз дорого. Однако важно понимать, что совокупность
событий важнее результата. Если отдельную операцию признали ошибочной или
незаконной, баланс пересчитают.&lt;/p&gt;

&lt;p&gt;Еще одно уточнение касается слова “сумма”. Под ним мы имеем в виду результат
применения операций. Какие именно действия выполняет событие – зависит от его
типа. В случае с банковским счетом баланс нельзя вычислить арифметически, потому
что иные события включают процент на остаток или конвертацию валют.&lt;/p&gt;

&lt;p&gt;В прошлом параграфе мы научились хранить патч для каждого изменения. Легко
заметить, что получился своего рода event-sourcing. Действительно: если взять
исходный документ и применить к нему серию патчей, получим текущий (последний)
документ. Если результаты отличаются, то имеет место нарушение: либо события
теряются, либо кто-то исправил документ в обход нашей системы.&lt;/p&gt;

&lt;h2 id=&quot;сбор-агрегатов-из-событий&quot;&gt;Сбор агрегатов из событий&lt;/h2&gt;

&lt;p&gt;Рассмотрим, как применить набор патчей к исходному документу. У нас уже
достаточный опыт работы с jsonpatch, поэтому приведем минимальные примеры. Если
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;doc&lt;/code&gt; – начальный документ, а &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;patches&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;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;jsonpatch&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;doc&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;patches&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;op&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;add&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;path&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;/a&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;value&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;p&quot;&gt;[{&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;op&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;add&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;path&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;/b&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;value&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;p&quot;&gt;]&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;patch&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;patches&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;p&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;jsonpatch&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;JsonPatch&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;patch&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;doc&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;nb&quot;&gt;apply&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;doc&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;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;doc&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;# {'a': 1, 'b': 2}
&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;fold&lt;/code&gt; или &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;reduce&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;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;jsonpatch&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;functools&lt;/span&gt; &lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;reduce&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;step_fn&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;acc&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;patch&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;p&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;jsonpatch&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;JsonPatch&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;patch&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;n&quot;&gt;p&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;apply&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;acc&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;doc&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;reduce&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;step_fn&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;patches&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;print&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;doc&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;# {'a': 1, 'b': 2}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

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

&lt;p&gt;Объявим агрегатную функцию &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;json_patch_agg&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;aggregate&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;json_patch_agg&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&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;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;sfunc&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;py_apply_patch&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;stype&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&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;n&quot;&gt;initcond&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s1&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;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sfunc&lt;/code&gt; (state function): функция приращения. Принимает два параметра, где
первый – аккумулятор, который накапливает значение. Второй аргумент функции –
очередное значение колонки. Функция возвращает новый аккумулятор.&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;stype&lt;/code&gt;: тип, с которым работает агрегатная функция. В нашем случае это &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;jsonb&lt;/code&gt;.&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;initcond&lt;/code&gt;: начальный (пустой) элемент. Используется в качестве аккумулятора
при первом вызове функции приращения.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Напомним, что функцию приращения мы уже объявляли – это &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;py_apply_patch&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;or&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;replace&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;py_apply_patch&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;doc&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;n&quot;&gt;patch&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;returns&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;jsonb&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;transform&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;jsonb&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;language&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;plpython3u&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;immutable&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;strict&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;$$&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;jsonpatch&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;patch_obj&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;jsonpatch&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;JsonPatch&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;patch&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;n&quot;&gt;patch_obj&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;apply&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;doc&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;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;n&quot;&gt;json_patch_agg&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;patch&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;doc&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&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;'[{&quot;op&quot;: &quot;add&quot;, &quot;path&quot;: &quot;/a&quot;, &quot;value&quot;: 1}]'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;::&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;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;'[{&quot;op&quot;: &quot;add&quot;, &quot;path&quot;: &quot;/b&quot;, &quot;value&quot;: 2}]'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;::&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;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;'[{&quot;op&quot;: &quot;replace&quot;, &quot;path&quot;: &quot;/a&quot;, &quot;value&quot;: 3}]'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;::&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;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;vals&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;patch&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;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;┌─[ RECORD 1 ]───────────┐
│ doc │ {&quot;a&quot;: 3, &quot;b&quot;: 2} │
└─────┴──────────────────┘
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

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

&lt;h2 id=&quot;махинации-с-патчами&quot;&gt;Махинации с патчами&lt;/h2&gt;

&lt;p&gt;Чем дольше вы работаете с JSON Patch, тем больше откроете трюков и техник. В
этом параграфе мы расскажем о двух из них.&lt;/p&gt;

&lt;p&gt;Несколько патчей можно свести к одному, банально объединих их. Действительно:
если патч &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;p1&lt;/code&gt; содержит операции &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;o1&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;o2&lt;/code&gt; и &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;o3&lt;/code&gt;, а &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;p2&lt;/code&gt; – &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;o4&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;o5&lt;/code&gt; и &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;o6&lt;/code&gt;,
то легко получить патч &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;p3&lt;/code&gt; с операциями &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;o1&lt;/code&gt; – &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;o6&lt;/code&gt;, и они применятся в том же
порядке. Напомним, что в Python списки объединяются оператором &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;+&lt;/code&gt; (плюс), а в
Postgres – &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;||&lt;/code&gt; (палочки). Полученный патч называют супер- или пакетным патчем.&lt;/p&gt;

&lt;p&gt;В примере ниже оба патча применяются как один, при этом порядок сохраняется: в
итоговом списке числа идут в верной последовательности. Дефис на конце пути
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&quot;/nums/-&quot;&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;n&quot;&gt;p1&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;op&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;add&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;path&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;/nums/-&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;value&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;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;op&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;add&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;path&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;/nums/-&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;value&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;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;op&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;add&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;path&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;/nums/-&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;value&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;p&quot;&gt;]&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;p2&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;op&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;add&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;path&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;/nums/-&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;value&quot;&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;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;op&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;add&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;path&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;/nums/-&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;value&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;p&quot;&gt;},&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;op&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;add&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;path&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;/nums/-&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;value&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;6&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;doc&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;s&quot;&gt;&quot;nums&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;k&quot;&gt;print&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;jsonpatch&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;JsonPatch&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;p1&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;p2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;apply&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;doc&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;# {'nums': [1, 2, 3, 4, 5, 6]}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Перенесемся в Postgres. Предположим, имеется таблица событий с колонкой
patch. Как получить пакетный патч всех событий? Для этого напишем агрегатную
функцию, которая объединяет 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;aggregate&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;jsonb_concat_agg&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&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;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;sfunc&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;jsonb_concat&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;stype&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&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;n&quot;&gt;initcond&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s1&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;/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;jsonb_pretty&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;jsonb_concat_agg&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;patch&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;doc&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&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;'[{&quot;op&quot;: &quot;add&quot;, &quot;path&quot;: &quot;/a&quot;, &quot;value&quot;: 1}]'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;::&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;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;'[{&quot;op&quot;: &quot;add&quot;, &quot;path&quot;: &quot;/b&quot;, &quot;value&quot;: 2}]'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;::&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;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;'[{&quot;op&quot;: &quot;add&quot;, &quot;path&quot;: &quot;/c&quot;, &quot;value&quot;: 3}]'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;::&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;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;vals&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;patch&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;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;┌─[ RECORD 1 ]────────────────┐
│ doc │ [                    ↵│
│     │     {                ↵│
│     │         &quot;op&quot;: &quot;add&quot;, ↵│
│     │         &quot;path&quot;: &quot;/a&quot;,↵│
│     │         &quot;value&quot;: 1   ↵│
│     │     },               ↵│
│     │     {                ↵│
│     │         &quot;op&quot;: &quot;add&quot;, ↵│
│     │         &quot;path&quot;: &quot;/b&quot;,↵│
│     │         &quot;value&quot;: 2   ↵│
│     │     },               ↵│
│     │     {                ↵│
│     │         &quot;op&quot;: &quot;add&quot;, ↵│
│     │         &quot;path&quot;: &quot;/c&quot;,↵│
│     │         &quot;value&quot;: 3   ↵│
│     │     }                ↵│
│     │ ]                     │
└─────┴───────────────────────┘
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;По умолчанию JSON Patch предлагает шесть типов операций: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;add&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;replace&lt;/code&gt; и
другие. Однажды вы обнаружите, что для комфортной работы их не
хватает. Например, чтобы описать пополнение счета, необходима операция
приращения (increase):&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;nl&quot;&gt;&quot;op&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;inc&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;path&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;/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;nl&quot;&gt;&quot;value&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;200&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;replace&lt;/code&gt; для этой цели будет ошибкой. Если до операции на счету
было 1000 рублей и клиент добавил 500, патч с &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;replace&lt;/code&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;nl&quot;&gt;&quot;op&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;replace&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;path&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;/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;nl&quot;&gt;&quot;value&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;1500&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;Нарушается важный принцип: событие не должно знать о состоянии документа, потому
что иначе его нельзя удалить без последствий. Например, если клиент внес 200 и
300 рублей подряд, события выглядят как в примере ниже:&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;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;op&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;replace&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;path&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;/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;nl&quot;&gt;&quot;value&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;1200&lt;/span&gt;&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;nl&quot;&gt;&quot;op&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;replace&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;path&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;/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;nl&quot;&gt;&quot;value&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;1500&lt;/span&gt;&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;Если удалить первое событие (200 рублей), то после второго на счету снова
окажется окажется 1500 рублей, что неверно. Предположим теперь, что события
созданы при помощи операции inc:&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;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;op&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;inc&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;path&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;/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;nl&quot;&gt;&quot;value&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;200&lt;/span&gt;&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;nl&quot;&gt;&quot;op&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;inc&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;path&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;/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;nl&quot;&gt;&quot;value&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;300&lt;/span&gt;&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;С ней ошибки не произойдет: если удалить первое событие, то после второго на
счету окажется 1300 рублей. Кроме того, обе операции коммутативны (дают
одинаковый результат в разном порядке), что снижает риски, связанные с
очередностью сообщений.&lt;/p&gt;

&lt;p&gt;Библиотеку jsonpatch легко расширить. Встроенные операции унаследованы от класса
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PatchOperation&lt;/code&gt; и содержатся в приватном словаре. Расширение сводится к двум
шагам:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;на базе &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PatchOperation&lt;/code&gt; определить класс с частным методом &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;apply&lt;/code&gt;;&lt;/li&gt;
  &lt;li&gt;добавить его в словарь операций.&lt;/li&gt;
&lt;/ul&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;kn&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;jsonpatch&lt;/span&gt; &lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;JsonPatch&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;PatchOperation&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;IncrementOperation&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;PatchOperation&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;apply&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;n&quot;&gt;obj&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;subobj&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;part&lt;/span&gt; &lt;span class=&quot;o&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;n&quot;&gt;pointer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;to_last&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;obj&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;try&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;val&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;subobj&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;part&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;except&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;KeyError&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;IndexError&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
            &lt;span class=&quot;k&quot;&gt;raise&lt;/span&gt;

        &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;not&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;isinstance&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;val&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;nb&quot;&gt;int&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;float&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)):&lt;/span&gt;
            &lt;span class=&quot;k&quot;&gt;raise&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;TypeError&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;sa&quot;&gt;f&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;Value is not a number: &lt;/span&gt;&lt;span class=&quot;si&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;val&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

        &lt;span class=&quot;k&quot;&gt;try&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;o&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;n&quot;&gt;operation&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;value&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;except&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;KeyError&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ex&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
            &lt;span class=&quot;k&quot;&gt;raise&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;InvalidJsonPatch&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;The operation does not contain a 'value' member&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

        &lt;span class=&quot;n&quot;&gt;subobj&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;part&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;value&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;obj&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;apply&lt;/code&gt; занимают проверки и внятные сообщения об
ошибках. Регистрация несколько неуклюжа: поле &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;JsonPatch.operations&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;n&quot;&gt;ops&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;JsonPatch&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;operations&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;s&quot;&gt;'inc'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;IncrementOperation&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;JsonPatch&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;operations&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ops&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;doc&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;s&quot;&gt;&quot;amount&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1000&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;p&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;JsonPatch&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;op&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;inc&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;path&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;/amount&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;value&quot;&lt;/span&gt;&lt;span class=&quot;p&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;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;op&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;add&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;path&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;/foo&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;value&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;a&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;s&quot;&gt;&quot;op&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;inc&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;path&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;/amount&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;value&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;300&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;op&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;add&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;path&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;/bar&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;value&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;b&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;k&quot;&gt;print&lt;/span&gt;&lt;span class=&quot;p&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;nb&quot;&gt;apply&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;doc&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;# {'amount': 1500, 'foo': 'a', 'bar': 'b'}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

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

&lt;p&gt;Чтобы инкремент был доступен в Postgres, скопируйте файл &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;json_patch_ops.py&lt;/code&gt; на
сервер. Он должен быть в директории, которую Python просматривает при загрузке
модулей. Простое решение в том, чтобы скопировать файл в директорию
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;site-packages&lt;/code&gt; или указать к нему путь в переменной &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PYTHONPATH&lt;/code&gt;. Более
грамотный подход – оформить код в пакет и устанавливать при помощи
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pip3&lt;/code&gt;. Рассмотрите вариант, чтобы вашу работу включили в официальную
библиотеку, например в виде расширения.&lt;/p&gt;

&lt;h2 id=&quot;бесконфликтное-редактирование&quot;&gt;Бесконфликтное редактирование&lt;/h2&gt;

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

&lt;p&gt;Предположим, сотрудники Иванов и Петров редактируют один и тот же документ. Наша
задача — сделать так, чтобы изменения одного сотрудника не затерли изменения
другого. Например, если Иванов изменил поле amount, а Петров — comment, эти
правки не конфликтуют друг с другом и можно применить их в любом порядке. Если
Петров тоже исправил amount, перед нами классический конфликт.&lt;/p&gt;

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

&lt;p&gt;Можно заблокировать документ на уровне базы командой &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;select for update&lt;/code&gt;. Однако
выражение &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;for update&lt;/code&gt; требует транзакции, и мы не можем держать ее
неограниченно долго.&lt;/p&gt;

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

&lt;p&gt;Техника, что мы рассматриваем, опирается на три версии документа: исходную (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;А&lt;/code&gt;)
и две локальных (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;B&lt;/code&gt; и &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;C&lt;/code&gt;). Имея эти три версии, легко найти патч для пар (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;А&lt;/code&gt;
→ &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;В&lt;/code&gt;) и (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;А&lt;/code&gt; → &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;С&lt;/code&gt;). Далее мы проверяем, есть у патчей общие
поля. Если нет, патчи не конфликтуют, и можно применить их в любом порядке. В
противном случае мы не только уведомим пользователя о конфликте, но и скажем,
какое поле стало его причиной.&lt;/p&gt;

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

&lt;h2 id=&quot;решение-конфликтов&quot;&gt;Решение конфликтов&lt;/h2&gt;

&lt;p&gt;Итак, Иванов и Петров одновременно открыли документ на
редактирование. Изначально документ выглядел как в примере ниже. Назовем его
ревизией &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;А&lt;/code&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;mi&quot;&gt;100500&lt;/span&gt;&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;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;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;100&lt;/span&gt;&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;New document&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;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;amount&lt;/code&gt; на 110 и нажал “Сохранить”. На сервер ушел документ
ревизии &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;B&lt;/code&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;mi&quot;&gt;100500&lt;/span&gt;&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;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;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;110&lt;/span&gt;&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;New document&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;Выше мы указываем версию нарастающим число: 1, 2 и так далее. Это небезопасно:
ничто не мешает злоумышленнику передать версию 999, и серверный алгоритм
сработает с ошибкой. Для версий лучше использовать идентификатор UUID, подобрать
который вручную невозможно. В нашем случае мы используем числа только для
краткости.&lt;/p&gt;

&lt;p&gt;По полю &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;version&lt;/code&gt; сервер определяет, что изменения отталкиваются от
версии 1. Выполняется проверка: 1 — это текущий документ? Для этого мы читаем
версию из таблицы &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;applications&lt;/code&gt;. Если в ней записано 1, это значит, что никто не
успел внести изменения до нас, и мы вправе сделать это сейчас. Замещение
документа происходит в два шага:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;документ А (версии 1, исходный) уходит в историческую таблицу;&lt;/li&gt;
  &lt;li&gt;новый документ В замещает текущий, его версия становится 2.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Изменения Иванова приняты. А что с Петровым? Бедняга просидел весь день на
митингах и вернулся к компьютеру в конце дня. Петров нашел силы только на то,
чтобы изменить заголовок с “New document” на “My document”. На сервер ушел
документ, который мы назовем ревизией &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;C&lt;/code&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;mi&quot;&gt;100500&lt;/span&gt;&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;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;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;100&lt;/span&gt;&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;My document&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;Сервер выполнит те же самые действия, что и в прошлый раз. Он прочитает текущий
документ и выяснит, что его версия не 1, а 2. Это значит, за время отсутствия
Петрова документ изменился. Его нельзя переписать: если сделать текущей версию
Петрова, то изменения Иванова (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;amount&lt;/code&gt; 110) пропадут.&lt;/p&gt;

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

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

&lt;p&gt;Легко заметить, что в нашем примере конфликта нет. Иванов исправил поле amount,
а Петров — &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;title&lt;/code&gt;, поэтому изменения Петрова все-таки можно принять даже с
учетом того, что они опираются на устаревший документа. Вот как проверить
конфликт программным способом:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;найти изменения (патч) между версиями &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;A&lt;/code&gt; и &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;B&lt;/code&gt; (исходной и Иванова);&lt;/li&gt;
  &lt;li&gt;то же самое для &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;A&lt;/code&gt; и &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;C&lt;/code&gt; (исходной и Петрова);&lt;/li&gt;
  &lt;li&gt;определить, есть ли в патчах элементы с одинаковым полем &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;path&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;В нашем случае патчи выглядят следующим образом. От &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;А&lt;/code&gt; к &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;В&lt;/code&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;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;op&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;replace&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;path&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;/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;nl&quot;&gt;&quot;value&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;110&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;А&lt;/code&gt; к &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;С&lt;/code&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;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;op&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;replace&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;path&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;/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;nl&quot;&gt;&quot;value&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;My document&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;Видим, что пути &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/amount&lt;/code&gt; и &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/title&lt;/code&gt; не пересекаются, поэтому изменения не
конфликтуют. Чтобы применить версию Петрова, мы берем последний актуальный
документ (версию Иванова) и накладываем на него второй патч (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;A&lt;/code&gt; →
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;C&lt;/code&gt;). Результат становится новым текущим документом с версией 3.&lt;/p&gt;

&lt;p&gt;Предположим теперь, Петров изменил не только заголовок документа, но и поле
amount. Назовем эту ревизию &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;C2&lt;/code&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;mi&quot;&gt;100500&lt;/span&gt;&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;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;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;115&lt;/span&gt;&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;My document&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;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;А&lt;/code&gt; → &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;C2&lt;/code&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;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;op&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;replace&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;path&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;/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;nl&quot;&gt;&quot;value&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;My document&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;op&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;replace&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;path&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;/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;nl&quot;&gt;&quot;value&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;115&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;А&lt;/code&gt; → &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;В&lt;/code&gt;: в обоих есть элемент с
полем &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/amount&lt;/code&gt;. Решить конфликт программа не в состоянии, потому что только
авторы знают, какое значение верное. Со стороны сервера будет правильным сделать
следующее:&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;/ul&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;op&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;replace&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;path&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;/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;nl&quot;&gt;&quot;value&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;115&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;p&gt;Когда конфликт исправлен (скажем, стороны договорились, что поле &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;amount&lt;/code&gt; должно
быть вообще 120), Петров высылает документ ревизии &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;D&lt;/code&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;mi&quot;&gt;100500&lt;/span&gt;&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;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;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;120&lt;/span&gt;&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;My document&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;Это изменение отталкивается от текущей версии 2. Когда сервер проделает шаги,
что мы уже рассмотрели, новый документ заменит прежний, а последний уйдет в
таблицу версий.&lt;/p&gt;

&lt;p&gt;Интересно, что наш подход работает для множественных изменений. Предположим,
пока Петров сидел на совещании, сразу несколько сотрудников изменили документ. В
результате текущим стал документ версии 9 (обозначим его &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;F&lt;/code&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;mi&quot;&gt;100500&lt;/span&gt;&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;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;9&lt;/span&gt;&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;100&lt;/span&gt;&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;category&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;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;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;john@test.com&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;Customer #123&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;history&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;event&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;edited&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;user&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;92323&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;A&lt;/code&gt; → &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;F&lt;/code&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;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;op&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;replace&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;path&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;/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;nl&quot;&gt;&quot;value&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;Customer #123&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;op&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;replace&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;path&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;/version&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;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;9&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;op&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;add&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;path&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;/category&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;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;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;op&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;add&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;path&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;/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;nl&quot;&gt;&quot;value&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;john@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;op&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;add&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;path&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;/history&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;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;event&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;edited&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;user&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;92323&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;Среди измененных полей нет &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/amount&lt;/code&gt;, следовательно патчи &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;A&lt;/code&gt; → &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;F&lt;/code&gt; и &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;A&lt;/code&gt;
→ &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;C&lt;/code&gt; не пересекаются. Изменения Петрова можно по-прежнему применить к
последнему документу.&lt;/p&gt;

&lt;h2 id=&quot;технические-детали&quot;&gt;Технические детали&lt;/h2&gt;

&lt;p&gt;Рассмотрим функцию сравнения двух патчей. Она принимает два jsonb-документа
(массивы объектов) и определяет, есть ли в них одинаковые поля path. Функция
довольно проста: это пересечение (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;intersect&lt;/code&gt;) двух вызовов
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;jsonb_path_query&lt;/code&gt;. Если пересечение не пустое (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;exists&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;or&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;replace&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;find_conflicts&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;patch1&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;n&quot;&gt;patch2&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;returns&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;boolean&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;language&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;sql&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;immutable&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;strict&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;parallel&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;safe&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;exists&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;jsonb_path_query&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;patch1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'strict $[*].path'&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;jsonb_path_query&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;patch2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'strict $[*].path'&lt;/span&gt;&lt;span class=&quot;p&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;Возможно, вам понадобится не просто логический флаг, а сводная информация: какие
именно поля затронуты и с какими значениями. Подойдет вторая функцию, которая
оперирует таблицами. Каждый патч она приводит к нативной таблице Postgres с
полями &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;op&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;path&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;value&lt;/code&gt; и другими. Далее эти таблицы соединяются оператором
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;JOIN&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;or&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;replace&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;find_conflicts&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;patch1&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;n&quot;&gt;patch2&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;returns&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;table&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;i1&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;op1&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;path1&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;value1&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;n&quot;&gt;from1&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;i2&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;op2&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;path2&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;value2&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;n&quot;&gt;from2&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;language&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;sql&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;immutable&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;strict&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;parallel&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;safe&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;$$&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;with&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;p1&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&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;jt&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;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;json_table&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;patch1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'$[*]'&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;columns&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;i&lt;/span&gt;     &lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;ordinality&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;op&lt;/span&gt;    &lt;span class=&quot;nb&quot;&gt;text&lt;/span&gt;  &lt;span class=&quot;n&quot;&gt;path&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'$.op'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;path&lt;/span&gt;  &lt;span class=&quot;nb&quot;&gt;text&lt;/span&gt;  &lt;span class=&quot;n&quot;&gt;path&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'$.path'&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;n&quot;&gt;path&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'$.value'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;nv&quot;&gt;&quot;from&quot;&lt;/span&gt;  &lt;span class=&quot;n&quot;&gt;jsonb&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;path&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'$.from'&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;))&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;jt&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;p2&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&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;jt&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;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;json_table&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;patch2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'$[*]'&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;columns&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;i&lt;/span&gt;     &lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;ordinality&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;op&lt;/span&gt;    &lt;span class=&quot;nb&quot;&gt;text&lt;/span&gt;  &lt;span class=&quot;n&quot;&gt;path&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'$.op'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;path&lt;/span&gt;  &lt;span class=&quot;nb&quot;&gt;text&lt;/span&gt;  &lt;span class=&quot;n&quot;&gt;path&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'$.path'&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;n&quot;&gt;path&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'$.value'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;nv&quot;&gt;&quot;from&quot;&lt;/span&gt;  &lt;span class=&quot;n&quot;&gt;jsonb&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;path&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'$.from'&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;))&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;jt&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;p1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;p2&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;where&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;p1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;op&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;p2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;op&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;and&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;p1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;path&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;p2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;path&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;and&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;p1&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;o&quot;&gt;!=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;p2&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;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;err&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;/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;find_conflicts&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;p&quot;&gt;[&lt;/span&gt;
    &lt;span class=&quot;err&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;nv&quot;&gt;&quot;op&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;&quot;replace&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;nv&quot;&gt;&quot;path&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;&quot;/amount&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;nv&quot;&gt;&quot;value&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;120&lt;/span&gt;
    &lt;span class=&quot;err&quot;&gt;}&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;nv&quot;&gt;&quot;op&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;&quot;add&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;nv&quot;&gt;&quot;path&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;&quot;/comment&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;nv&quot;&gt;&quot;value&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;&quot;edited&quot;&lt;/span&gt;
    &lt;span class=&quot;err&quot;&gt;}&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;p&quot;&gt;::&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;err&quot;&gt;$$&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;nv&quot;&gt;&quot;op&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;&quot;add&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;nv&quot;&gt;&quot;path&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;&quot;/user_ids/3&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;nv&quot;&gt;&quot;value&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;40&lt;/span&gt;
    &lt;span class=&quot;err&quot;&gt;}&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;nv&quot;&gt;&quot;op&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;&quot;replace&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;nv&quot;&gt;&quot;path&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;&quot;/title&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;nv&quot;&gt;&quot;value&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;&quot;Test 2&quot;&lt;/span&gt;
    &lt;span class=&quot;err&quot;&gt;}&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;p&quot;&gt;::&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;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&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;┌────┬─────┬───────┬────────┬───────┬────┬─────┬───────┬────────┬───────┐
│ i1 │ op1 │ path1 │ value1 │ from1 │ i2 │ op2 │ path2 │ value2 │ from2 │
├────┼─────┼───────┼────────┼───────┼────┼─────┼───────┼────────┼───────┤
└────┴─────┴───────┴────────┴───────┴────┴─────┴───────┴────────┴───────┘
(0 rows)
&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;find_conflicts&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;p&quot;&gt;[&lt;/span&gt;
    &lt;span class=&quot;err&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;nv&quot;&gt;&quot;op&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;&quot;replace&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;nv&quot;&gt;&quot;path&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;&quot;/amount&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;nv&quot;&gt;&quot;value&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;120&lt;/span&gt;
    &lt;span class=&quot;err&quot;&gt;}&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;nv&quot;&gt;&quot;op&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;&quot;add&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;nv&quot;&gt;&quot;path&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;&quot;/comment&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;nv&quot;&gt;&quot;value&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;&quot;edited&quot;&lt;/span&gt;
    &lt;span class=&quot;err&quot;&gt;}&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;p&quot;&gt;::&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;err&quot;&gt;$$&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;nv&quot;&gt;&quot;op&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;&quot;replace&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;nv&quot;&gt;&quot;path&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;&quot;/amount&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;nv&quot;&gt;&quot;value&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;150&lt;/span&gt;
    &lt;span class=&quot;err&quot;&gt;}&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;nv&quot;&gt;&quot;op&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;&quot;replace&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;nv&quot;&gt;&quot;path&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;&quot;/title&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;nv&quot;&gt;&quot;value&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;&quot;Test 2&quot;&lt;/span&gt;
    &lt;span class=&quot;err&quot;&gt;}&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;p&quot;&gt;::&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;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&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;┌────┬─────────┬─────────┬────────┬────────┬────┬─────────┬─────────┬────────┬────────┐
│ i1 │   op1   │  path1  │ value1 │ from1  │ i2 │   op2   │  path2  │ value2 │ from2  │
├────┼─────────┼─────────┼────────┼────────┼────┼─────────┼─────────┼────────┼────────┤
│  1 │ replace │ /amount │ 120    │ &amp;lt;null&amp;gt; │  1 │ replace │ /amount │ 150    │ &amp;lt;null&amp;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;/amount&lt;/code&gt;, значения сторон &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;B&lt;/code&gt; и &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;C&lt;/code&gt; –
120 и 150. Отправим эту информацию клиенту; ожидается, что приложение выделит
проблемные поля и поможет пользователю их исправить.&lt;/p&gt;

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

&lt;p&gt;Важно, что алгоритм полностью реализуется силами базы: роль приложения
минимальна. Базе требуется только функция, которая по двум документам строит
патч. Проще всего делегировать эту задачу расширению &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;plpython3u&lt;/code&gt; и пакету
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;jsonpatch&lt;/code&gt;. Искушенные читатели могут написать код на на Си и оформить его в
расширение Postgres.&lt;/p&gt;

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

&lt;p&gt;Приглашаем читателя к последней главе, где мы закроем нерешенный вопрос с
поиском.&lt;/p&gt;
</description>
        <pubDate>Tue, 18 Aug 2026 00:00:00 +0000</pubDate>
        <link>https://grishaev.me/pg-book-json-ch09/</link>
        <guid isPermaLink="true">https://grishaev.me/pg-book-json-ch09/</guid>
        
        <category>postgres</category>
        
        <category>json</category>
        
        <category>book</category>
        
        <category>sql</category>
        
        
      </item>
    
      <item>
        <title>Русская речь в фильмах</title>
        <description>&lt;p&gt;Иногда русская озвучка бывает, мягко говоря, спорной. Актеры кричат в тех
местах, где персонаж шепчет; интонация странная, пропадают акценты, идиомы, игра
слов. Так что если вам позволяют условия (знаете язык, партнер(ша) не
возражает), потребляйте фильмы и игры в оригинале (или хотя бы ключевые
сцены). У этого правила, однако, есть исключение: иногда в оригинале звучит
русская речь. В таком исполнении, что хочется не слышать ее ни за какие деньги.&lt;/p&gt;

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

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

&lt;p&gt;Когда русский бандит обращается к американцам, проблемы не возникает. Тут даже
переигрывают: “лэт мы спык ынглиш фр-р-ром май хар-р-рт”. Плохой персонаж всегда
будет с рокочущим “р”. Как обезьянничать, так все мастера. А как насчет
нормальной речи?&lt;/p&gt;

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

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

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

&lt;p&gt;Выходит, что инди-студия смогла, а Голливуд не может? Слабо верится.&lt;/p&gt;

&lt;p&gt;Интересно, что в локализации фильмов мы не сталкиваемся с подобным кринжем:
герой говорит с родными на нормальном языке, в то время как в оригинале звучит
тарабарщина. Оригинал, увы, нет-нет да подкинет подобный сюрприз. Длятся
подобные сцены недолго, но даже их хватает, чтобы свалится на пол от отчаяния и
стыда.&lt;/p&gt;
</description>
        <pubDate>Sat, 01 Aug 2026 00:00:00 +0000</pubDate>
        <link>https://grishaev.me/rus-translit/</link>
        <guid isPermaLink="true">https://grishaev.me/rus-translit/</guid>
        
        <category>movies</category>
        
        
      </item>
    
  </channel>
</rss>
