<?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>Sat, 11 Jul 2026 12:03:05 +0000</pubDate>
    <lastBuildDate>Sat, 11 Jul 2026 12:03:05 +0000</lastBuildDate>
    <generator>Jekyll v4.2.0</generator>
    
      <item>
        <title>Is AI Profitable Yet?</title>
        <description>
&lt;p&gt;Еще один сайт-страничка называется &lt;a href=&quot;https://isaiprofitable.com/&quot;&gt;Is AI Profitable Yet?&lt;/a&gt;. За указанным
заголовком следует лаконичное No, а ниже – фирмы вроде Гугла, Амазона и так
далее. Напротив каждой указано, сколько денег они потратили на эй-ай и сколько
заработали.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/static/aws/is-ai-profitable/1.jpeg&quot; /&gt;&lt;/p&gt;

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

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

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

&lt;p&gt;Поживем – увидим.&lt;/p&gt;
</description>
        <pubDate>Sun, 05 Jul 2026 00:00:00 +0000</pubDate>
        <link>https://grishaev.me/is-ai-profitable/</link>
        <guid isPermaLink="true">https://grishaev.me/is-ai-profitable/</guid>
        
        <category>life</category>
        
        <category>ai</category>
        
        
      </item>
    
      <item>
        <title>Как работать?</title>
        <description>&lt;p&gt;У меня простой вопрос: как вообще работать? Я о том, что если следить за всеми
потоками информации, то весь день я просижу в корпоративных мессаджере и
почте. Редактор и терминал я ни разу не открою.&lt;/p&gt;

&lt;p&gt;Смотрите, есть почта. Туда валится Джира, уведомления из Teams, правки к статьям
в Вики, внутренние новости, тренинги, вебинары. В Teams – дюжина чатов и
каналов, не считая личных сообщений.&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>Sun, 05 Jul 2026 00:00:00 +0000</pubDate>
        <link>https://grishaev.me/how-to-work/</link>
        <guid isPermaLink="true">https://grishaev.me/how-to-work/</guid>
        
        <category>life</category>
        
        <category>work</category>
        
        
      </item>
    
      <item>
        <title>Don't paste the AI, please</title>
        <description>
&lt;p&gt;Нашел забавный сайт-страничку: &lt;a href=&quot;https://dontquotetheai.com/&quot;&gt;Don’t paste the AI, please&lt;/a&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;UPD аналоги:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://stopcitingai.com&quot;&gt;Stop Citing AI&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
        <pubDate>Sun, 05 Jul 2026 00:00:00 +0000</pubDate>
        <link>https://grishaev.me/dont-paste-ai/</link>
        <guid isPermaLink="true">https://grishaev.me/dont-paste-ai/</guid>
        
        <category>life</category>
        
        <category>ai</category>
        
        
      </item>
    
      <item>
        <title>Глава 6. Язык JSON Path</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;/li&gt;
  &lt;li&gt;Функции на языке Python&lt;/li&gt;
  &lt;li&gt;Версионирование и архивация документов&lt;/li&gt;
  &lt;li&gt;Релевантный поиск&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;

    Содержание

&lt;/h2&gt;

&lt;ul id=&quot;toc-item-pg-book-json-ch05&quot;&gt;
  &lt;li&gt;&lt;a href=&quot;#главы&quot; id=&quot;toc-item-pg-book-json-ch05-главы&quot;&gt;Главы&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#знакомство-с-json-path&quot; id=&quot;toc-item-pg-book-json-ch05-знакомство-с-json-path&quot;&gt;Знакомство с JSON Path&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#виды-путей&quot; id=&quot;toc-item-pg-book-json-ch05-виды-путей&quot;&gt;Виды путей&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#раскрытие-массивов&quot; id=&quot;toc-item-pg-book-json-ch05-раскрытие-массивов&quot;&gt;Раскрытие массивов&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#индекс-с-поддержкой-путей&quot; id=&quot;toc-item-pg-book-json-ch05-индекс-с-поддержкой-путей&quot;&gt;Индекс с поддержкой путей&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#методы-и-операторы-json-path&quot; id=&quot;toc-item-pg-book-json-ch05-методы-и-операторы-json-path&quot;&gt;Методы и операторы JSON Path&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#пути-со-звездочками&quot; id=&quot;toc-item-pg-book-json-ch05-пути-со-звездочками&quot;&gt;Пути со звездочками&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#составление-путей-в-коде&quot; id=&quot;toc-item-pg-book-json-ch05-составление-путей-в-коде&quot;&gt;Составление путей в коде&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#подзапросы&quot; id=&quot;toc-item-pg-book-json-ch05-подзапросы&quot;&gt;Подзапросы&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

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

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

&lt;p&gt;Последние два десятилетия формат JSON популярен во всех сферах айти. Это привело
к тому, что многие базы данных поддерживают его в той или иной мере. Слово
“поддерживает” означает, что база не только хранит JSON, но и предлагает
средства, чтобы извлечь его подмножество или обновить без передачи всего
документа. В Postgres поддержка JSON особенно сильна; в последние годы такие же
возможности появились в SQLite, MySQL и SQL Server.&lt;/p&gt;

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

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

&lt;p&gt;Описанное выше напоминает &lt;a href=&quot;https://en.wikipedia.org/wiki/XPath&quot;&gt;XPath&lt;/a&gt; – язык путей XML. Формат XML, хоть и уступил в
популярности JSON, все еще встречается на многих платформах. Причина, по которой
появился язык XPath, была та же: привести частные решения к общему
знаменателю. Удобно, когда одно и то же выражение XPath работает и в Python, и в
Javascript, и в базе данных.&lt;/p&gt;

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

&lt;p&gt;Формат JSON, хоть и отличается от XML, имеет с ним нечто общее – путь. Приведем
один и тот же документ в двух форматах:&lt;/p&gt;

&lt;div class=&quot;language-xml 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;cp&quot;&gt;&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;users&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;user&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;id=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;101&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;age=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;33&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;Ivan Petrov&lt;span class=&quot;nt&quot;&gt;&amp;lt;/user&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;user&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;id=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;135&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;age=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;52&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;Andrey Ivanov&lt;span class=&quot;nt&quot;&gt;&amp;lt;/user&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;user&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;id=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;399&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;age=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;41&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;Anna Smirnova&lt;span class=&quot;nt&quot;&gt;&amp;lt;/user&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;/users&amp;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;users&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;id&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;101&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;age&quot;&lt;/span&gt;&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;33&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;name&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;Ivan Petrov&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;id&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;135&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;age&quot;&lt;/span&gt;&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;52&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;name&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;Andrey Ivanov&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;id&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;399&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;age&quot;&lt;/span&gt;&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;41&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;name&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;Anna Smirnova&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Чтобы добраться до значения “Ivan Petrov” в XML, укажем следующий путь XPath:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;/users/user[1]/text()
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;На языке JSON Path выглядит иначе:&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;$.users[0].name
&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;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;users.0.id   101
users.0.age  33
users.0.name Ivan Petrov

users.1.id   135
users.1.age  41
users.1.name Andrey Ivanov

users.2.id   399
users.2.age  41
users.2.name Anna Smirnova
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

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

&lt;p&gt;Идея JSON Path в том, чтобы на любой платформе путь к элементу выглядел
одинаково. В настоящий момент пути JSON работают в Postgres, Microsoft SQL
Server, Oracle, а также документных базах вроде OpenSearch. У каждой реализации
свои особенности, однако в целом они похожи.&lt;/p&gt;

&lt;p&gt;В случае с Postgres у языка JSON Path есть прародитель под названием
&lt;a href=&quot;https://postgrespro.ru/docs/enterprise/18/jsquery&quot;&gt;Jsquery&lt;/a&gt;. Просим читателя не путать его с библиотекой jQuery для
JavaScript: разница во второй букве s. Jsquery решает ту же задачу: упрощенно
описывает путь к элементам, которые нужно извлечь. Изначально Jsquery
спонсировался фирмой Wargaming в период активной работы над &lt;a href=&quot;https://worldoftanks.eu/ru/clanwars/globalmap/&quot;&gt;Глобальной
Картой&lt;/a&gt;. Это пошаговая игра с обширным состоянием, которое включает
сведения о кланах, игроках, технике, положении фишек на карте и многое
другое. Состояние игры хранилось в виде JSON в базе данных. Приходилось часто
запрашивать его подмножество: какие игроки остались в игре, какие локации
захвачены, какие свободны и так далее. Чтобы не выгружать документ целиком, в
Wargaming пользовались расширением Jsquery, разработанным в PostgresPro.&lt;/p&gt;

&lt;p&gt;Сегодня это расширение встроено в поставку PostgresPro; в обычном Postgres его
устанавливают отдельно. Далее по тексту мы обсуждаем только классический JSON
Path, потому что он доступен в Postgres по умолчанию. Разобравшись с JSON Path,
читатель без труда освоит Jsquery.&lt;/p&gt;

&lt;h2 id=&quot;знакомство-с-json-path&quot;&gt;Знакомство с JSON Path&lt;/h2&gt;

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

&lt;div class=&quot;language-sql highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;create&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;table&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;demo&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;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;demo&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;err&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;nv&quot;&gt;&quot;users&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;err&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;nv&quot;&gt;&quot;id&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;101&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;nv&quot;&gt;&quot;age&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;33&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;nv&quot;&gt;&quot;name&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;&quot;Ivan Petrov&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;id&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;135&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;nv&quot;&gt;&quot;age&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;52&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;nv&quot;&gt;&quot;name&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;&quot;Andrey Ivanov&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;id&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;399&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;nv&quot;&gt;&quot;age&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;41&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;nv&quot;&gt;&quot;name&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;&quot;Anna Smirnova&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;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_path_query_first&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;s1&quot;&gt;'$.users.name'&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;node&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;demo&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;┌───────────────┐
│     node      │
├───────────────┤
│ &quot;Ivan Petrov&quot; │
└───────────────┘
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Разберемся, что здесь происходит. Знак доллара в пути означает ссылку на вершину
документа. Каждый следующий уровень отделяется точкой. Лексемы между точками
совпадают с именами ключей. Если ключ содержит только символы a-z, A-Z, цифры и
подчеркивание, их указывают как есть. В противному случае лексемы экранируют –
обрамляют двойными кавычками. Например, в языках семейства Lisp приняты ключи с
дефисами: вместо &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;user_id&lt;/code&gt; пишут &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;user-id&lt;/code&gt;. Подобные ключи записываются так:
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;$.user.&quot;user-id&quot;&lt;/code&gt;. Ключ &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;user&lt;/code&gt; не нуждается в кавычках, однако их наличие не
будет ошибкой.&lt;/p&gt;

&lt;p&gt;Оговоримся, что путь &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;$.users.name&lt;/code&gt; охватывает имена всех пользователей, однако
функция &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;jsonb_path_query_first&lt;/code&gt; выбирает только первый из них (что отражает
суффикс &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;_first&lt;/code&gt;). Возможны другие подходы, когда элементы возвращаются в виде
массива jsonb. Такой результат производит функция &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;jsonb_path_query_array&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;jsonb_path_query_array&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;s1&quot;&gt;'$.users.name'&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;node&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;demo&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;┌───────────────────────────────────────────────────┐
│                       node                        │
├───────────────────────────────────────────────────┤
│ [&quot;Ivan Petrov&quot;, &quot;Andrey Ivanov&quot;, &quot;Anna Smirnova&quot;] │
└───────────────────────────────────────────────────┘
&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;jsonb_path_query&lt;/code&gt; возвращает таблицу с одним столбцом jsonb; ее можно
соединить с другой таблицей, выбрать из нее данные как из подзапроса и так
далее. С другими функциями семейства &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;jsonb_path_&lt;/code&gt; читатель ознакомится в
&lt;a href=&quot;https://postgrespro.ru/docs/postgrespro/current/functions-json&quot;&gt;документации&lt;/a&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_path_query&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;s1&quot;&gt;'$.users.name'&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;node&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;demo&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;┌─────────────────┐
│      node       │
├─────────────────┤
│ &quot;Ivan Petrov&quot;   │
│ &quot;Andrey Ivanov&quot; │
│ &quot;Anna Smirnova&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;Одна из особенностей JSON Path в следующем: путь к элементу может быть строгим
или нет. В английском языке для этого используют термины strict и lax. Последнее
переводится как “расслабленный” или “вялый”. Будем называть такой путь
нестрогим. Тип пути указывают в самом начале. Это строгий путь:&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;strict $.path.to.node
&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;lax $.path.to.node
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;По умолчанию путь считается нестрогим (lax).&lt;/p&gt;

&lt;p&gt;Разница между ними в том, что происходит, когда путь выходит за границы
документа. Предположим, по ошибке мы составили путь, которого нет в
документе. Пусть это будет &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;$.hello.test&lt;/code&gt;. Если задать ему тип strict, при
попытке извлечь элемент получим ошибку:&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_path_query&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;s1&quot;&gt;'strict $.hello.test'&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;node&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;demo&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;-- ERROR:  JSON object does not contain key &quot;hello&quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Если же пусть нестрогий (lax), поиск элемента окончится, как только мы вышли за
границы документа. При этом ошибки не будет, и функция вернет null. Уточним –
именно стандартный NULL, а не null-значение JSON:&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_path_query&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;s1&quot;&gt;'lax $.hello.test'&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;node&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;demo&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;-- null&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;field[index]&lt;/code&gt; указывает на элемент массива, при этом index должен
быть неотрицательным числом. Отрицательные индексы вроде -1 для доступа с конца
массива не работают. Звездочка на месте индекса означает все элементы массива;
подобный путь описывает уже не один, а множество элементов.&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;jsonb_path_query&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;s1&quot;&gt;'strict $.users[*].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;node&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;demo&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;┌──────┐
│ node │
├──────┤
│ 101  │
│ 135  │
│ 399  │
└──────┘
&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;lax $.users.id&lt;/code&gt; или просто
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;$.users.id&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;users[*]&lt;/code&gt; мы указали users; Postgres определил, что это массив
и применил следующее поле id ко всем элементам. Запись удобна краткостью: не
нужно явно указывать, где объекты, а где массивы. Объекты и массивы могут
чередоваться произвольно с одним исключением: нелья, чтобы два массива следовали
один за другим. Другими словами, нестрогий путь не работает с массивом
массивов. Приведем слегка надуманный пример, где строгий путь сработает:&lt;/p&gt;

&lt;div class=&quot;language-sql highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;select&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;jsonb_path_query&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;matrix&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;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;&quot;id&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;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;id&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;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;id&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;3&lt;/span&gt;&lt;span class=&quot;err&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;err&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;&quot;id&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;4&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;id&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;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;id&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;err&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;err&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;&quot;id&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;7&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;id&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;8&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;id&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;9&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;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;s1&quot;&gt;'strict $.matrix[*][*].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;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;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;┌───┐
│ x │
├───┤
│ 1 │
│ 2 │
│ 3 │
│ 4 │
│ 5 │
│ 6 │
│ 7 │
│ 8 │
│ 9 │
└───┘
&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_path_query&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;matrix&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;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;&quot;id&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;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;id&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;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;id&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;3&lt;/span&gt;&lt;span class=&quot;err&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;err&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;&quot;id&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;4&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;id&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;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;id&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;err&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;err&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;&quot;id&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;7&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;id&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;8&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;id&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;9&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;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;s1&quot;&gt;'lax $.matrix.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;x&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;/p&gt;

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

&lt;h2 id=&quot;индекс-с-поддержкой-путей&quot;&gt;Индекс с поддержкой путей&lt;/h2&gt;

&lt;p&gt;В главе 4 мы говорили об индексации всего json-документа. Напомним, мы
использовали индекс GIN с опцией jsonb_ops (если ее опустить, она применится по
умолчанию):&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_doc_gin_jsonb_ops&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;doc&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;jsonb_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;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@&amp;gt;&lt;/code&gt; подхватывал индекс, что мы и видели в плане
запроса.&lt;/p&gt;

&lt;p&gt;Если указать в свойствах индекса &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;jsonb_path_ops&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;. Эти операторы принимают пути JSON, чтобы проверить их наличие или
сравнить значение документа с переданным. Удалим прошлый индекс и построим
новый:&lt;/p&gt;

&lt;div class=&quot;language-sql highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;drop&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;index&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;exists&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;idx_applications_doc_gin_jsonb_ops&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;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_doc_gin_jsonb_path_ops&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;doc&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;jsonb_path_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;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;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;'foo'&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;s1&quot;&gt;'baz'&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;123&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-000000000070'&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;doc&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;@?&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'lax $.foo.bar.baz'&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;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 │ 00000000-0000-0000-0000-000000000070 │
└────┴──────────────────────────────────────┘
&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;/p&gt;

&lt;div class=&quot;language-sql highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;explain&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;analyze&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;doc&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;@@&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'lax $.departments.users.email == &quot;user_65@test.com&quot; '&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;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;┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│                                                                         QUERY PLAN                                                                          │
├─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ Limit  (cost=90.20..126.64 rows=10 width=16) (actual time=1.703..1.747 rows=10 loops=1)                                                                     │
│   -&amp;gt;  Bitmap Heap Scan on applications  (cost=90.20..36325.58 rows=9943 width=16) (actual time=1.702..1.744 rows=10 loops=1)                                │
│         Recheck Cond: (doc @@ '($.&quot;departments&quot;.&quot;users&quot;.&quot;email&quot; == &quot;user_65@test.com&quot;)'::jsonpath)                                                          │
│         Heap Blocks: exact=10                                                                                                                               │
│         -&amp;gt;  Bitmap Index Scan on idx_applications_doc_gin_jsonb_path_ops  (cost=0.00..87.71 rows=9943 width=0) (actual time=0.901..0.902 rows=3000 loops=1) │
│               Index Cond: (doc @@ '($.&quot;departments&quot;.&quot;users&quot;.&quot;email&quot; == &quot;user_65@test.com&quot;)'::jsonpath)                                                      │
│ Planning Time: 0.113 ms                                                                                                                                     │
│ Execution Time: 1.770 ms                                                                                                                                    │
└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
&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;doc&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;@@&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'lax $.created_by.name == &quot;User 0999&quot;'&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;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;doc&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;@@&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'lax $.application_id == 1513 '&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;p&quot;&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Значение справа от предиката должно быть корректным JSON-документом. Если это
строка, она заключается в двойные кавычки согласно синтаксису JSON.&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;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;doc&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;@@&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'$.created_by.id == $.departments.users.id'&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;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;@?&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;h2 id=&quot;методы-и-операторы-json-path&quot;&gt;Методы и операторы JSON Path&lt;/h2&gt;

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

&lt;p&gt;Один из возможных сценариев &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.size()&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;doc&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;'departments'&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;'users'&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;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;$$&lt;/span&gt;
&lt;span class=&quot;err&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;nv&quot;&gt;&quot;id&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;&quot;00000000-0000-0000-0000-000000009999&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;nv&quot;&gt;&quot;name&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;&quot;User 9999&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;nv&quot;&gt;&quot;role&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;&quot;manager&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;nv&quot;&gt;&quot;email&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;&quot;user_9999@test.com&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;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-000000000065'&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;...users.size() &amp;gt; 2&lt;/code&gt; вернет заявку 65:&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;doc&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;'departments'&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;'$.users.size() &amp;gt; 2 '&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;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                  │
├──────────────────────────────────────┤
│ 00000000-0000-0000-0000-000000000065 │
└──────────────────────────────────────┘
&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;.size()&lt;/code&gt; работает с массивом, но не объектом. Чтобы
узнать размер объекта (число ключей в нем), его приводят к массиву методом
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.keyvalue()&lt;/code&gt;. Каждый его элемент – объект с ключами &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;key&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;id&lt;/code&gt;
(порядковым номером):&lt;/p&gt;

&lt;div class=&quot;language-sql highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;select&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;jsonb_path_query&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;users&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nv&quot;&gt;&quot;101&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;&quot;name&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;&quot;Ivan&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;nv&quot;&gt;&quot;123&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;&quot;name&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;&quot;John&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;nv&quot;&gt;&quot;523&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;&quot;name&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;&quot;Johann&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;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;s1&quot;&gt;'$.users.keyvalue()'&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;node&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;┌───────────────────────────────────────────────────────┐
│                         node                          │
├───────────────────────────────────────────────────────┤
│ {&quot;id&quot;: 20, &quot;key&quot;: &quot;101&quot;, &quot;value&quot;: {&quot;name&quot;: &quot;Ivan&quot;}}   │
│ {&quot;id&quot;: 20, &quot;key&quot;: &quot;123&quot;, &quot;value&quot;: {&quot;name&quot;: &quot;John&quot;}}   │
│ {&quot;id&quot;: 20, &quot;key&quot;: &quot;523&quot;, &quot;value&quot;: {&quot;name&quot;: &quot;Johann&quot;}} │
└───────────────────────────────────────────────────────┘
&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;$.users.keyvalue().size()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Среди прочих операторов интересен &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;like_regex&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;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;doc&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;@@&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'$.departments.users.name like_regex &quot;^user 12[0-9]{1}$&quot; flag &quot;i&quot;'&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;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                  │
├──────────────────────────────────────┤
│ 00000000-0000-0000-0000-000000008129 │
│ 00000000-0000-0000-0000-000000009100 │
│ 00000000-0000-0000-0000-000000009101 │
│ 00000000-0000-0000-0000-000000009102 │
│ 00000000-0000-0000-0000-000000009103 │
│ 00000000-0000-0000-0000-000000009104 │
│ 00000000-0000-0000-0000-000000009105 │
│ 00000000-0000-0000-0000-000000009106 │
│ 00000000-0000-0000-0000-000000009107 │
│ 00000000-0000-0000-0000-000000009108 │
└──────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Флаги регулярного выражения – это подсказки о том, как его
интерпретировать. Наиболее полезны из них “i” и “q”. Первый (insensitive)
означает, что выражение игнорирует регистр символов. Как правило, регистр
действительно не важен: нам интересны все варианты записи (строчными и
прописными буквами), поэтому флаг добавляют к выражению. Второй флаг q (quoted)
означает, что служебные символы читаются как есть – у них нет особой
семантики. Так, с флагом &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;q&lt;/code&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;, а не любую цифру. Флаг quoted необходим, когда выражение
приходит со стороны и ему нельзя доверять.&lt;/p&gt;

&lt;p&gt;Огорчим читателя тем, что оператор &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;like_regex&lt;/code&gt; не использует индекс. В общих
случаях индекс работает с операторами равенства и сравнения, а именно &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;=&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;=&amp;gt;&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;gt;&lt;/code&gt;. Более сложные операторы вроде &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;like_regex&lt;/code&gt;, увы, провоцируют
линейный обход таблицы. В случае с заявками запрос будет особо долгим, потому
что их миллион. Таким образом &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;like_regex&lt;/code&gt; не снимает проблему поиска по
вхождению строки, которую мы решали при помощи триграммного индекса. Используйте
этот оператор только для служебных нужд.&lt;/p&gt;

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

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

&lt;p&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;jsonb_path_query&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;users&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nv&quot;&gt;&quot;101&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;&quot;name&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;&quot;Ivan&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;nv&quot;&gt;&quot;123&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;&quot;name&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;&quot;John&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;nv&quot;&gt;&quot;523&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;&quot;name&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;&quot;Johann&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;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;s1&quot;&gt;'$.users.*.name'&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;name&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;┌──────────┐
│   name   │
├──────────┤
│ &quot;Ivan&quot;   │
│ &quot;John&quot;   │
│ &quot;Johann&quot; │
└──────────┘
&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;friends&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;jsonb_path_query&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;users&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nv&quot;&gt;&quot;101&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;&quot;name&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;&quot;Ivan&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;&quot;friends&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;&quot;991&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;&quot;name&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;&quot;Anna&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;nv&quot;&gt;&quot;523&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;&quot;name&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;&quot;Kirill&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;nv&quot;&gt;&quot;123&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;&quot;name&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;&quot;John&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;&quot;friends&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;&quot;353&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;&quot;name&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;&quot;Gleb&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;nv&quot;&gt;&quot;566&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;&quot;name&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;&quot;Petr&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;nv&quot;&gt;&quot;523&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;&quot;name&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;&quot;Johann&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;&quot;friends&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;&quot;834&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;&quot;name&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;&quot;Marina&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;nv&quot;&gt;&quot;235&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;&quot;name&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;&quot;Galina&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;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;s1&quot;&gt;'$.users.**.name'&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;name&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;┌──────────┐
│   name   │
├──────────┤
│ &quot;Ivan&quot;   │
│ &quot;Kirill&quot; │
│ &quot;Anna&quot;   │
│ &quot;John&quot;   │
│ &quot;Gleb&quot;   │
│ &quot;Petr&quot;   │
│ &quot;Johann&quot; │
│ &quot;Galina&quot; │
│ &quot;Marina&quot; │
└──────────┘
&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;01-02-2025&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;$.**.created_at == &quot;01-02-2025&quot;
&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 == &quot;6c92590b-2daa-487f-a50d-434d34185015&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;До сих пор мы обходили вопрос: как составить выражение JSON Path в приложении?
Выше мы писали запросы так, словно путь и значение известны заранее. На практике
это не так: то и другое приходят от сторонних сервисов или от пользователей.&lt;/p&gt;

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

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;json&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;re&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;dеf&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;needs_quoting&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;lexem&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;re&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;search&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;[^a-zA-Z0-9_]+&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;lexem&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;dеf&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;maybe_quote&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;lexem&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;needs_quoting&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;lexem&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;s&quot;&gt;'&quot;%s&quot;'&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;lexem&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;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;n&quot;&gt;lexem&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;dеf&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;json_path&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;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;op&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;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;$.%s %s %s&quot;&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;.&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;join&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;([&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;maybe_quote&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;for&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;part&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;path&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;p&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;dumps&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;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;needs_quoting&lt;/code&gt; проверяет, нуждается ли элемент пути в экранировании –
обрамлении кавычками. Если в элемент содержит символы, отличные от латинских
букв, цифр и подчеркивания, то результат положительный. Функция &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;maybe_quote&lt;/code&gt;
экранирует элемент, если это необходимо (отсюда префикс &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;maybe&lt;/code&gt;). Финальная
функция &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;json_path&lt;/code&gt; принимает список элементов пути, оператор и значение,
которое приводится к JSON. Примеры вызова:&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;print&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;json_path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;([&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;departments&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;users&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;email&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;],&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;==&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;test@test.com&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;# $.departments.users.email == &quot;test@test.com&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;json_path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;([&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;test&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;full-name&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;is-active?&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;],&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;==&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;c1&quot;&gt;# $.test.&quot;full-name&quot;.&quot;is-active?&quot; == true
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Мы построили выражение JSON Path, и нужно передать его в запрос. Здесь возможны
два варианта. Первый – передать путь параметром как обычную строку:&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;query&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;&quot;&quot;
  select id, doc from applications
  where doc @@ ?
  limit 100
&quot;&quot;&quot;&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;s&quot;&gt;'$.departments.users.email == &quot;user_65@test.com&quot;'&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;conn&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;execute&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;query&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;params&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;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;Этот способ защищает вас от инъекций и манипуляций с кавычками. Недостаток в
том, что некоторые системы пишут в логи запросы без параметров. В результате вы
увидите только условие &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;where doc @@ ?&lt;/code&gt;, а что было на месте вопроса –
неизвестно.&lt;/p&gt;

&lt;p&gt;Второй способ в том, чтобы внедрить путь прямо в тело SQL. Чтобы обезопасить
себя от инъекций, окружите путь тегами вида &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;$xxx$&lt;/code&gt;, где &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;xxx&lt;/code&gt; – случайное
выражение. Ниже для ясности мы выбрали тег &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;$jsonpath$&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;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;n&quot;&gt;jsonpath&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;departments&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;users&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;email&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;&quot;user_65@test.com&quot;&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;jsonpath&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;$&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;p&quot;&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Можно расширить вашу ORM, чтобы код ниже&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;Applications&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;nb&quot;&gt;filter&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;doc__atat&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;attrs&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;application_id&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;],&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;==&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;12345&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;where&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;s1&quot;&gt;'$.attrs.application_id == 12345'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;В случае с 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&lt;/span&gt; &lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Lookup&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;django.db.models&lt;/span&gt; &lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Field&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;JsonbAtAt&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Lookup&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;lookup_name&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;atat&quot;&lt;/span&gt;

    &lt;span class=&quot;n&quot;&gt;dеf&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;as_sql&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;compiler&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;connection&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;return&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;%s @@ %s&quot;&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;column&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;p&quot;&gt;),&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;params&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;Field&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;register_lookup&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;JsonbAtAt&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;json_path&lt;/code&gt; и другие, приведенные выше.&lt;/p&gt;

&lt;h2 id=&quot;подзапросы&quot;&gt;Подзапросы&lt;/h2&gt;

&lt;p&gt;Рассмотрим еще одну возможность JSON Path, которую до сих пор избегали –
подзапросы. Чтобы материал был понятней, подберем такой документ и требование,
которое нельзя выполнить без подзапросов.&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;nl&quot;&gt;&quot;departments&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;id&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;00000000-0000-0000-0000-000000000005&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;code&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;dep_5&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;name&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;Department 5&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;users&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;id&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;00000000-0000-0000-0000-000000000055&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;name&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;User 55&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;role&quot;&lt;/span&gt;&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;support&quot;&lt;/span&gt;&lt;span class=&quot;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;user_55@test.com&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;id&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;00000000-0000-0000-0000-000000000065&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;name&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;User 65&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;role&quot;&lt;/span&gt;&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;decision-maker&quot;&lt;/span&gt;&lt;span class=&quot;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;user_65@test.com&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;id&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;00000000-0000-0000-0000-000000000015&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;code&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;dep_15&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;name&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;Department 15&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;users&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;id&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;00000000-0000-0000-0000-000000000075&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;name&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;User 75&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;role&quot;&lt;/span&gt;&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;support&quot;&lt;/span&gt;&lt;span class=&quot;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;user_75@test.com&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;id&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;00000000-0000-0000-0000-000000000035&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;name&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;User 35&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;role&quot;&lt;/span&gt;&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;decision-maker&quot;&lt;/span&gt;&lt;span class=&quot;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;user_35@test.com&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Требование в том, чтобы найти документы, над которыми работал пользователь с
кодом X в роли Y, например 65 и support.&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;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;doc&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;@@&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'$.departments.users.id == &quot;00000000-0000-0000-0000-000000000065&quot;'&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;and&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;s1&quot;&gt;'$.departments.users.role == &quot;support&quot;'&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;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                  │
├──────────────────────────────────────┤
│ 00000000-0000-0000-0000-000000001045 │
│ 00000000-0000-0000-0000-000000002045 │
│ 00000000-0000-0000-0000-000000003045 │
│ 00000000-0000-0000-0000-000000003055 │
│ 00000000-0000-0000-0000-000000006045 │
│ 00000000-0000-0000-0000-000000007055 │
│ 00000000-0000-0000-0000-000000011065 │
│ 00000000-0000-0000-0000-000000013045 │
│ 00000000-0000-0000-0000-000000016045 │
│ 00000000-0000-0000-0000-000000021055 │
└──────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Какие-то данные мы получили, однако верны ли они? Изучим первую заявку с номером
1045, точнее ее поле departments. Вот что в ней содержится:&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;nl&quot;&gt;&quot;departments&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;id&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;00000000-0000-0000-0000-000000000005&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;code&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;dep_5&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;name&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;Department 5&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;users&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;id&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;00000000-0000-0000-0000-000000000055&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;name&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;User 55&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;role&quot;&lt;/span&gt;&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;support&quot;&lt;/span&gt;&lt;span class=&quot;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;user_55@test.com&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;id&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;00000000-0000-0000-0000-000000000065&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;name&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;User 65&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;role&quot;&lt;/span&gt;&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;decision-maker&quot;&lt;/span&gt;&lt;span class=&quot;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;user_65@test.com&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Обратите внимание: у пользователя 65 роль &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;decision-maker&lt;/code&gt;, а не &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;support&lt;/code&gt;. Роль
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;support&lt;/code&gt;, напротив, принадлежит пользователю 55. Почему же заявка попала в
результат?&lt;/p&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-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;n&quot;&gt;doc&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;@@&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'$.departments.users.id == &quot;00000000-0000-0000-0000-000000000065&quot;'&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;departments[0].users[1].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;n&quot;&gt;doc&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;@@&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'$.departments.users.role == &quot;support&quot;'&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;departments[0].users[0].role&lt;/code&gt;. Оба условия были истинны, а остальное очевидно:
объединение AND было тоже истинным, поэтому мы получили документ. С точки зрения
Postgres ошибок не было; проблема в том, что мы неверно запросили данные.&lt;/p&gt;

&lt;p&gt;Сделать это нужно по-другому, в два этапа. На первом шаге составляют минимальный
общий путь. Для обоих путей &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;$.departments.users.id&lt;/code&gt; и
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;$.departments.users.role&lt;/code&gt; он равен &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;$.departments.users&lt;/code&gt;.&lt;/p&gt;

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

&lt;p&gt;Алгоритм, что мы описали, называется подзапросом JSON Path – не путайте его с
подзапросами 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;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;doc&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;@?&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'$.departments.users ? (@.id == &quot;00000000-0000-0000-0000-000000000065&quot; &amp;amp;&amp;amp; @.role == &quot;support&quot;)'&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;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;@&lt;/code&gt; – он означает текущую позицию в
документе. В подзапросе можно по-прежнему сослаться на корень документа при
помощи доллара, но как правило этого не требуется.&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;@@&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;ol&gt;
  &lt;li&gt;отфильтровать департаменты по коду;&lt;/li&gt;
  &lt;li&gt;выбрать их пользователей;&lt;/li&gt;
  &lt;li&gt;отфильтровать пользователей по номеру и роли.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Путь на языке JSONPath:&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;doc&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;@?&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'$.departments[*] ? (@.code == &quot;dep_5&quot;) .users ? (@.id == &quot;00000000-0000-0000-0000-000000000065&quot; &amp;amp;&amp;amp; @.role == &quot;support&quot;)'&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;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;┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│                                                                                 QUERY PLAN                                                                                  │
├─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ Limit  (cost=137.69..174.12 rows=10 width=16) (actual time=5.307..5.412 rows=10 loops=1)                                                                                    │
│   -&amp;gt;  Bitmap Heap Scan on applications  (cost=137.69..36533.88 rows=9990 width=16) (actual time=5.303..5.407 rows=10 loops=1)                                               │
│         Recheck Cond: (doc @? '$.&quot;departments&quot;[*]?(@.&quot;code&quot; == &quot;dep_5&quot;).&quot;users&quot;?(@.&quot;id&quot; == &quot;00000000-0000-0000-0000-000000000065&quot; &amp;amp;&amp;amp; @.&quot;role&quot; == &quot;support&quot;)'::jsonpath)     │
│         Rows Removed by Index Recheck: 21                                                                                                                                   │
│         Heap Blocks: exact=31                                                                                                                                               │
│         -&amp;gt;  Bitmap Index Scan on idx_applications_doc_gin_jsonb_path_ops  (cost=0.00..135.19 rows=9990 width=0) (actual time=5.199..5.199 rows=470 loops=1)                 │
│               Index Cond: (doc @? '$.&quot;departments&quot;[*]?(@.&quot;code&quot; == &quot;dep_5&quot;).&quot;users&quot;?(@.&quot;id&quot; == &quot;00000000-0000-0000-0000-000000000065&quot; &amp;amp;&amp;amp; @.&quot;role&quot; == &quot;support&quot;)'::jsonpath) │
│ Planning Time: 0.170 ms                                                                                                                                                     │
│ Execution Time: 5.830 ms                                                                                                                                                    │
└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

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

&lt;p&gt;Итак, мы рассмотрели язык JSON Path и его основные возможности, предикаты,
методы, подзапросы. Мы коснулись лишь малой части того, что описано в
документации; предлагаем читателю ознакомиться с ней на официальном сайте. С
другой стороны, мы заострили внимание на тех неочевидных моментах, которые даже
в документации описаны скупо. Автор надеется, что JSON Path поможет вам в работе
со сложными документами. В следующей главе мы рассмотрим еще одну технику,
основанную на этом языке.&lt;/p&gt;
</description>
        <pubDate>Fri, 03 Jul 2026 00:00:00 +0000</pubDate>
        <link>https://grishaev.me/pg-book-json-ch06/</link>
        <guid isPermaLink="true">https://grishaev.me/pg-book-json-ch06/</guid>
        
        <category>postgres</category>
        
        <category>json</category>
        
        <category>book</category>
        
        <category>sql</category>
        
        
      </item>
    
      <item>
        <title>Глава 5. Ограничения в документах</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;/li&gt;
  &lt;li&gt;Функции на языке Python&lt;/li&gt;
  &lt;li&gt;Версионирование и архивация документов&lt;/li&gt;
  &lt;li&gt;Релевантный поиск&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;

    Содержание

&lt;/h2&gt;

&lt;ul id=&quot;toc-item-pg-book-json-ch05&quot;&gt;
  &lt;li&gt;&lt;a href=&quot;#главы&quot; id=&quot;toc-item-pg-book-json-ch05-главы&quot;&gt;Главы&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#ограничение-check&quot; id=&quot;toc-item-pg-book-json-ch05-ограничение-check&quot;&gt;Ограничение CHECK&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#ограничение-unique-и-уникальный-индекс&quot; id=&quot;toc-item-pg-book-json-ch05-ограничение-unique-и-уникальный-индекс&quot;&gt;Ограничение UNIQUE и уникальный индекс&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#ограничение-foreign-key-ссылки&quot; id=&quot;toc-item-pg-book-json-ch05-ограничение-foreign-key-ссылки&quot;&gt;Ограничение FOREIGN KEY (ссылки)&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#прочие-сведения&quot; id=&quot;toc-item-pg-book-json-ch05-прочие-сведения&quot;&gt;Прочие сведения&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

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

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

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

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

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

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

&lt;h2 id=&quot;ограничение-check&quot;&gt;Ограничение CHECK&lt;/h2&gt;

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

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

&lt;div class=&quot;language-sql highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;drop&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;exists&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;idx_applications_application_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;Код ниже создаст ограничение с типом CHECK:&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;constraint&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ctr_doc_application_id_nn&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;check&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;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;'application_id'&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;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;...add constraint&lt;/code&gt; следует машинное имя ограничения. Убедитесь,
что имя хотя бы примерно сообщает о том, что и как оно проверяет. В нашем случае
doc и application_id указывают на колонку и поле документа, а часть nn означает
not null. За именем следует тип ограничения (CHECK), а дальше – код, специфичный
для конкретного типа. По ходу главы мы рассмотрим и другие виды ограничений.&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;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;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;s1&quot;&gt;'{&quot;test&quot;: &quot;abc&quot;}'&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;-- ERROR:  new row for relation &quot;applications&quot; violates check constraint &quot;ctr_doc_application_id_nn&quot;&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;-- DETAIL:  Failing row contains (b6edce9d-285f-4df0-9bba-5f65d390608a, {&quot;test&quot;: &quot;abc&quot;}, 2026-06-05 17:49:33.757553+03, null).&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;application_id&lt;/code&gt;
записано именно число. Однако в прошлых экспериментах мы добавили документы, где
поле &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;application_id&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;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;doc&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;@@&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'$.application_id.type() != &quot;number&quot; '&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Удалите прежнее ограничение:&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;drop&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;constraint&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ctr_doc_application_id_nn&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;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;constraint&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ctr_doc_application_id_int&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;check&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;nb&quot;&gt;int&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-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;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;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;s1&quot;&gt;'{&quot;application_id&quot;: &quot;abc&quot;}'&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;-- ERROR: cannot cast jsonb string to type integer&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

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

&lt;p&gt;Более удачное решение – проверять документ в приложении до записи в базу. Обычно
это делают при помощи JSON-схемы. Библиотеки для работы со схемами есть для всех
популярных языков. Храните схемы в отдельном репозитории, чтобы ими могли
пользоваться клиенты на Python, Java и других языках. Приведем фрагмент схемы
нашей заявки:&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;type&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;object&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;properties&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;id&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;type&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;string&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;format&quot;&lt;/span&gt;&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;uuid&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&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;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;enum&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;active&quot;&lt;/span&gt;&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;deleted&quot;&lt;/span&gt;&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;nl&quot;&gt;&quot;created_at&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;type&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;string&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;format&quot;&lt;/span&gt;&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;date-time&quot;&lt;/span&gt;&lt;span class=&quot;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;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;type&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;integer&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;organization&quot;&lt;/span&gt;&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;err&quot;&gt;...&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;err&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;required&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;id&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;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;s2&quot;&gt;&quot;created_at&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;...&quot;&lt;/span&gt;&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;Резюмируюя, можно сказать, что ограничения CHECK малополезны для
документов. Если можно вынести проверку в схему, так и следует поступить. В
редких случаях CHECK может опираться на глобальные настройки или параметры
сеанса. Однако такие примеры не будем рассматривать ввиду их надуманности.&lt;/p&gt;

&lt;h2 id=&quot;ограничение-unique-и-уникальный-индекс&quot;&gt;Ограничение UNIQUE и уникальный индекс&lt;/h2&gt;

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

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

&lt;div class=&quot;language-sql highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;create&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;table&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;users_demo&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;id&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;integer&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;primary&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;full_name&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;email&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;unique&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;email&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;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;post_id&lt;/code&gt; и &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;tag_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;create&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;table&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;post_tags&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;post_id&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;integer&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;tag_id&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;integer&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;k&quot;&gt;unique&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;post_id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;tag_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;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Если вы забыли часть &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;alter&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;table&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;post_tags&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;add&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;constraint&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ctx_post_id_tag_id_u&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;unique&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;post_id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;tag_id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Если мы выполним следующий запрос дважды:&lt;/p&gt;

&lt;div class=&quot;language-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;post_tags&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;post_id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;tag_id&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;100&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;10&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;/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;ERROR:  duplicate key value violates unique constraint &quot;ctx_post_id_tag_id_u&quot;
DETAIL:  Key (post_id, tag_id)=(100, 10) already exists.
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Особенность ограничения UNIQUE в том, что оно принимает столбцы, а не
произвольные выражения. Не получится создать уникальное ограничение на поле
документа. С точки зрения 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;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;constraint&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ctx_doc_application_id_u&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;unique&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;nb&quot;&gt;int&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;));&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;-- ERROR:  syntax error at or near &quot;(&quot;&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;-- LINE 3: unique (((doc-&amp;gt;&amp;gt;'application_id')::int));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

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

&lt;p&gt;Удалите из таблицы applications ограничения, что мы создали ранее. Посмотреть их
имена можно командой &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;\d+ applications&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;Indexes:
    &quot;applications_pkey&quot; PRIMARY KEY, btree (id)
    ...
Check constraints:
    &quot;ctr_doc_application_id_int&quot; CHECK (((doc -&amp;gt;&amp;gt; 'application_id'::text)::integer) IS NOT NULL)
&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_doc_application_id_u&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;nb&quot;&gt;int&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;unique&lt;/code&gt; поддерживает только индекс btree. Применить его к индексам GIN
или GIST не получится.&lt;/p&gt;

&lt;p&gt;В прошлой главе мы уже создали индекс btree для поля &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;application_id&lt;/code&gt; под
названием &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;idx_applications_application_id&lt;/code&gt;. С новым уникальным индексом в нем
нет нужды, поэтому удалите его командой &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;drop index&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;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;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;s1&quot;&gt;'{&quot;application_id&quot;: &quot;12345&quot;}'&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;-- ERROR:  duplicate key value violates unique constraint &quot;idx_doc_application_id_u&quot;&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;-- DETAIL:  Key (((doc -&amp;gt;&amp;gt; 'application_id'::text)::integer))=(12345) already exists.&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;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;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;s1&quot;&gt;'{&quot;test&quot;: &quot;abc&quot;}'&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;Почему Postgres пропускает документы без номера? Дело в том, что когда поля
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;application_id&lt;/code&gt; нет, выражение &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;doc -&amp;gt;&amp;gt; 'application_id'&lt;/code&gt; возвращает
NULL. Уникальный индекс устроен так, что каждый NULL не равен другому NULL. Это
общая идиома Postgres: NULL считается неизвестностью, а неизвестности нельзя
сравнивать.&lt;/p&gt;

&lt;p&gt;На поведение NULL можно повлиять, указав опцию индекса &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;nulls not
distinct&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;drop&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_u&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;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_u&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;nb&quot;&gt;int&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;Теперь индекс трактует значения NULL как равные. Получится добавить только один
документ без номера, но не больше:&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;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;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;s1&quot;&gt;'{&quot;test&quot;: &quot;abc&quot;}'&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;-- OK&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;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;s1&quot;&gt;'{&quot;test&quot;: &quot;abc&quot;}'&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;-- ERROR:  duplicate key value violates unique constraint &quot;idx_doc_application_id_u&quot;&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;-- DETAIL:  Key (((doc-&amp;gt;&amp;gt;'application_id'::text)::integer))=(null) already exists.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

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

&lt;p&gt;Выше мы сделали номер уникальным глобально, то есть в рамках всей
таблицы. Иногда уникальность требуется в рамках периода. Скажем, бухгалтерия
обнуляет счетчик заявок каждый год. Номер 12345 может встречаться в заявках за
2025 и 2024 годы, но не одновременно в том или другом.&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;drop&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_u&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;created_at_year&lt;/code&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;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;created_at_year&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;timestamptz&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;date_trunc&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;'year'&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_at'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)::&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;timestamp&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;unique&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;index&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;idx_doc_app_id_date_u&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;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;nb&quot;&gt;int&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;created_at_year&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;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;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;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;s1&quot;&gt;'{&quot;application_id&quot;: &quot;10001111&quot;, &quot;created_at&quot;: &quot;2026-06-05T15:33:55Z&quot;}'&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;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;s1&quot;&gt;'{&quot;application_id&quot;: &quot;10001111&quot;, &quot;created_at&quot;: &quot;2027-06-05T15:33:55Z&quot;}'&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Обе вставки сработали без ошибок. Но если повторить один из запросов, операция
не пройдет:&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;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;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;s1&quot;&gt;'{&quot;application_id&quot;: &quot;10001111&quot;, &quot;created_at&quot;: &quot;2026-12-05T15:33:55Z&quot;}'&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;-- ERROR:  duplicate key value violates unique constraint &quot;idx_doc_app_id_date_u&quot;&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;-- DETAIL:  Key (((doc -&amp;gt;&amp;gt; 'application_id'::text)::integer), get_year(doc))=(10001111, 2026-01-01 00:00:00+03) already exists.&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;((doc-&amp;gt;&amp;gt;'application_id')::int)&lt;/code&gt;. Это
значит, что быстрый поиск по номеру по-прежнему в нашем распоряжении. Возможно,
вам понадобится другой доступ: сначала за указанный год, а затем по номеру. В
этом случае переставьте компоненты индекса местами.&lt;/p&gt;

&lt;p&gt;Уникальный индекс дает еще одну возможность: можно указать его в выражении &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ON
CONFLICT&lt;/code&gt; при вставке документа. Напишем запрос UPSERT, который реагирует именно
на конфликт номера и года, а не другие ограничения:&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;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;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;s1&quot;&gt;'{&quot;application_id&quot;: &quot;10001111&quot;, &quot;created_at&quot;: &quot;2026-12-05T15:33:55Z&quot;}'&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;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;nb&quot;&gt;int&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;created_at_year&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;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;do&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;nothing&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;Что именно поместить после DO – зависит от конкретной задачи. Выше мы
проигнорировали операцию, но возможно, вам потребуется обновить какие-то поля
документа.&lt;/p&gt;

&lt;p&gt;Обратите внимание, что мы указали выражение индекса, а не его имя. Форма ON
CONFLICT ожидает от нас либо имя уникального столбца, либо выражение, по
которому построен уникальный индекс. В результате запрос выглядит
громоздко. Если вы строете запросы при помощи ORM, поместите выражение индекса в
константу.&lt;/p&gt;

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

&lt;h2 id=&quot;ограничение-foreign-key-ссылки&quot;&gt;Ограничение FOREIGN KEY (ссылки)&lt;/h2&gt;

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

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

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

&lt;p&gt;Поскольку мы храним документы в Postgres, нам доступны ссылочные
ограничения. Ниже мы покажем техническую возможность, а пользоваться ей или нет
– остается на ваше усмотрение.&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;organizations&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;id&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;uuid&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;primary&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;key&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;default&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;uuid_generate_v4&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(),&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;doc&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;jsonb&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;compression&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;lz4&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;updated_at&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;timestamptz&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Второй шаг – добавим в таблицу организации. Очевидно, их первичные ключи должны
быть взяты из заявок, а не сгенерированы случайно. Прежде всего удалим мусорные
заявки, которые мы создали во время прошлых экспериментов. Для этого выполним
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DELETE&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;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;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;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,id}'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&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;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;distinct&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,id}'&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;org_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;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;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;┌──────────────────────────────────────┐
│                org_id                │
├──────────────────────────────────────┤
│ 00000000-0000-0000-0000-000000000000 │
│ 00000000-0000-0000-0000-000000000001 │
│ 00000000-0000-0000-0000-000000000002 │
│ 00000000-0000-0000-0000-000000000003 │
│ 00000000-0000-0000-0000-000000000004 │
│ 00000000-0000-0000-0000-000000000005 │
│ 00000000-0000-0000-0000-000000000006 │
│ 00000000-0000-0000-0000-000000000007 │
│ 00000000-0000-0000-0000-000000000008 │
│ 00000000-0000-0000-0000-000000000009 │
│ 00000000-0000-0000-0000-000000000010 │
│ 00000000-0000-0000-0000-000000000011 │
│ 00000000-0000-0000-0000-000000000012 │
&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;distinct&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;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,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,id}'&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;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;jsonb_build_object&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
        &lt;span class=&quot;s1&quot;&gt;'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,id}'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
        &lt;span class=&quot;s1&quot;&gt;'status'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;     &lt;span class=&quot;s1&quot;&gt;'active'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;s1&quot;&gt;'code'&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,code}'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
        &lt;span class=&quot;s1&quot;&gt;'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;'{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;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;applications&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;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                  │                                                        doc                                                        │
├──────────────────────────────────────┼───────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ 00000000-0000-0000-0000-000000000000 │ {&quot;id&quot;: &quot;00000000-0000-0000-0000-000000000000&quot;, &quot;code&quot;: &quot;0&quot;, &quot;status&quot;: &quot;active&quot;, &quot;short_name&quot;: &quot;Organization 0&quot;}   │
│ 00000000-0000-0000-0000-000000000001 │ {&quot;id&quot;: &quot;00000000-0000-0000-0000-000000000001&quot;, &quot;code&quot;: &quot;1&quot;, &quot;status&quot;: &quot;active&quot;, &quot;short_name&quot;: &quot;Organization 1&quot;}   │
│ 00000000-0000-0000-0000-000000000002 │ {&quot;id&quot;: &quot;00000000-0000-0000-0000-000000000002&quot;, &quot;code&quot;: &quot;2&quot;, &quot;status&quot;: &quot;active&quot;, &quot;short_name&quot;: &quot;Organization 2&quot;}   │
│ 00000000-0000-0000-0000-000000000003 │ {&quot;id&quot;: &quot;00000000-0000-0000-0000-000000000003&quot;, &quot;code&quot;: &quot;3&quot;, &quot;status&quot;: &quot;active&quot;, &quot;short_name&quot;: &quot;Organization 3&quot;}   │
│ 00000000-0000-0000-0000-000000000004 │ {&quot;id&quot;: &quot;00000000-0000-0000-0000-000000000004&quot;, &quot;code&quot;: &quot;4&quot;, &quot;status&quot;: &quot;active&quot;, &quot;short_name&quot;: &quot;Organization 4&quot;}   │
│ 00000000-0000-0000-0000-000000000005 │ {&quot;id&quot;: &quot;00000000-0000-0000-0000-000000000005&quot;, &quot;code&quot;: &quot;5&quot;, &quot;status&quot;: &quot;active&quot;, &quot;short_name&quot;: &quot;Organization 5&quot;}   │
│ 00000000-0000-0000-0000-000000000006 │ {&quot;id&quot;: &quot;00000000-0000-0000-0000-000000000006&quot;, &quot;code&quot;: &quot;6&quot;, &quot;status&quot;: &quot;active&quot;, &quot;short_name&quot;: &quot;Organization 6&quot;}   │
│ 00000000-0000-0000-0000-000000000007 │ {&quot;id&quot;: &quot;00000000-0000-0000-0000-000000000007&quot;, &quot;code&quot;: &quot;7&quot;, &quot;status&quot;: &quot;active&quot;, &quot;short_name&quot;: &quot;Organization 7&quot;}   │
│ 00000000-0000-0000-0000-000000000008 │ {&quot;id&quot;: &quot;00000000-0000-0000-0000-000000000008&quot;, &quot;code&quot;: &quot;8&quot;, &quot;status&quot;: &quot;active&quot;, &quot;short_name&quot;: &quot;Organization 8&quot;}   │
│ 00000000-0000-0000-0000-000000000009 │ {&quot;id&quot;: &quot;00000000-0000-0000-0000-000000000009&quot;, &quot;code&quot;: &quot;9&quot;, &quot;status&quot;: &quot;active&quot;, &quot;short_name&quot;: &quot;Organization 9&quot;}   │
│ 00000000-0000-0000-0000-000000000010 │ {&quot;id&quot;: &quot;00000000-0000-0000-0000-000000000010&quot;, &quot;code&quot;: &quot;10&quot;, &quot;status&quot;: &quot;active&quot;, &quot;short_name&quot;: &quot;Organization 10&quot;} │
│ 00000000-0000-0000-0000-000000000011 │ {&quot;id&quot;: &quot;00000000-0000-0000-0000-000000000011&quot;, &quot;code&quot;: &quot;11&quot;, &quot;status&quot;: &quot;active&quot;, &quot;short_name&quot;: &quot;Organization 11&quot;} │
│ 00000000-0000-0000-0000-000000000012 │ {&quot;id&quot;: &quot;00000000-0000-0000-0000-000000000012&quot;, &quot;code&quot;: &quot;12&quot;, &quot;status&quot;: &quot;active&quot;, &quot;short_name&quot;: &quot;Organization 12&quot;} │
&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 into organizations&lt;/code&gt;, удалите &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;limit&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;insert&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;into&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;organizations&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;select&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;distinct&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;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,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,id}'&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;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;jsonb_build_object&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
        &lt;span class=&quot;s1&quot;&gt;'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,id}'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
        &lt;span class=&quot;s1&quot;&gt;'status'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;     &lt;span class=&quot;s1&quot;&gt;'active'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;s1&quot;&gt;'code'&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,code}'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
        &lt;span class=&quot;s1&quot;&gt;'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;'{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;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;applications&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;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;organizations&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 │
├───────┤
│  1000 │
└───────┘
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

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

&lt;div class=&quot;language-sql highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;select&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;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;app_id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;org&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;'code'&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;org_code&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;org&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;'short_name'&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;org_name&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;join&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;organizations&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;org&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;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;o&quot;&gt;#&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'{organization,id}'&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;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;org&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;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;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_id                │ org_code │     org_name     │
├──────────────────────────────────────┼──────────┼──────────────────┤
│ 00000000-0000-0000-0000-000000635585 │ 585      │ Organization 585 │
│ 00000000-0000-0000-0000-000000635586 │ 586      │ Organization 586 │
│ 00000000-0000-0000-0000-000000635587 │ 587      │ Organization 587 │
│ 00000000-0000-0000-0000-000000635588 │ 588      │ Organization 588 │
│ 00000000-0000-0000-0000-000000635589 │ 589      │ Organization 589 │
│ 00000000-0000-0000-0000-000000635590 │ 590      │ Organization 590 │
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

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

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

&lt;p&gt;Postgres предлагает ограничение FOREIGN KEY – внешний ключ. Оно проверяет, что
строка, на которую ссылается поле таблицы, действительно существует. Особенность
внешних ключей в том, что они работают только с колонками, а не
выражениями. Команда ниже вызовет ошибку синтаксиса, потому что после слов
foreign key ожидается колонка или их комбинация, а не выражение:&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;constraint&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;fk_org_id&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;foreign&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;key&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;((&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;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,id}'&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;references&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;organizations&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;c1&quot;&gt;-- ERROR:  syntax error at or near &quot;(&quot;&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;-- LINE 2: foreign key ((doc #&amp;gt;&amp;gt; '{organization,id}')::uuid) references...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

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

&lt;p&gt;Приведем пример: в таблице demo две обычные колонки &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;x&lt;/code&gt; и &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;y&lt;/code&gt;, а третья &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sum&lt;/code&gt; –
вычисляемая, которая складывает &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;x&lt;/code&gt; и &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;y&lt;/code&gt;. При вставке не нужно беспокоиться о
поведении колонки &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sum&lt;/code&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;create&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;table&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;demo&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;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;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;y&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;integer&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;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;sum&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;integer&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;x&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;y&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;insert&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;into&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;demo&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;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;mi&quot;&gt;3&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;4&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;returning&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;n&quot;&gt;y&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;sum&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;┌───┬───┬─────┐
│ x │ y │ sum │
├───┼───┼─────┤
│ 1 │ 2 │   3 │
│ 3 │ 4 │   7 │
└───┴───┴─────┘
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;До версии 18 Postgres поддерживал только один вид вычисляемой колонки –
хранимый. Выше это свойство задано словом stored. Хранимая колонка вычисляет
значение и записывает на диск. При чтении поля sum не происходит обращения к x
или y.&lt;/p&gt;

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

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

&lt;p&gt;Независимо от типа – хранимая или виртуальная – вычисляемая колонка доступна
только для чтения. Записать в нее данные невозможно.&lt;/p&gt;

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

&lt;p&gt;Вычисляемые колонки доступны в индексах и ограничениях. Вынесем поле заявки
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;((doc #&amp;gt;&amp;gt; '{organization,id}')::uuid)&lt;/code&gt; в вычисляемую колонку и наложим на нее
ограничение FOREIGN KEY. Первый шаг – подготовка поля:&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;_org_id&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;uuid&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;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,id}'&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;stored&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;Обратите внимание на два факта. Во-первых, поле начинается с подчеркивания, что
говорит о его приватном характере. Во-вторых, выражение приводится к uuid, чтобы
его тип совпадал с типом первичного ключа организаций. Приведение типа служит
дополнительной проверкой: если добавить заявку с неправильной ссылкой, получим
ошибку.&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;n&quot;&gt;_org_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;limit&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;10&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;/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                  │               _org_id                │
├──────────────────────────────────────┼──────────────────────────────────────┤
│ 00000000-0000-0000-0000-000000635649 │ 00000000-0000-0000-0000-000000000649 │
│ 00000000-0000-0000-0000-000000635650 │ 00000000-0000-0000-0000-000000000650 │
│ 00000000-0000-0000-0000-000000635651 │ 00000000-0000-0000-0000-000000000651 │
│ 00000000-0000-0000-0000-000000635652 │ 00000000-0000-0000-0000-000000000652 │
│ 00000000-0000-0000-0000-000000635653 │ 00000000-0000-0000-0000-000000000653 │
&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;constraint&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;fk_org_id&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;foreign&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;key&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;_org_id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;references&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;organizations&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;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;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;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;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;1999111&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;nv&quot;&gt;&quot;organization&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;nv&quot;&gt;&quot;id&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;&quot;967849e5-9c59-4fa2-bc57-0a4a84bbd41c&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;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;-- ERROR:  insert or update on table &quot;applications&quot; violates foreign key constraint &quot;fk_org_id&quot;&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;-- DETAIL:  Key (_org_id)=(967849e5-9c59-4fa2-bc57-0a4a84bbd41c) is not present in table &quot;organizations&quot;.&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;delete&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;organizations&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-000000000649'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;-- ERROR:  update or delete on table &quot;organizations&quot; violates foreign key constraint &quot;fk_org_id&quot; on table &quot;applications&quot;&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;-- DETAIL:  Key (id)=(00000000-0000-0000-0000-000000000649) is still referenced from table &quot;applications&quot;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;В обоих случаях база не выполнила то, что нарушает ограничение.&lt;/p&gt;

&lt;p&gt;В теле FOREIGN KEY можно указать реакцию на удаление. Выражение &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;on delete
cascade&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;constraint&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;fk_org_id&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;foreign&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;key&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;_org_id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;references&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;organizations&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;on&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;delete&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;cascade&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;delete&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;organizations&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-000000000649'&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;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,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;'00000000-0000-0000-0000-000000000649'&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;/p&gt;

&lt;p&gt;FOREIGN KEY предлагает другой вариант удаления: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;on delete set null&lt;/code&gt;. С ним
удаляется только та запись, что мы указали, а ссылкам в других таблицах
присваивается NULL. Вспомним, однако, что поле &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;_org_id&lt;/code&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;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;constraint&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;fk_org_id&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;foreign&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;key&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;_org_id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;references&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;organizations&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;on&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;delete&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;set&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;c1&quot;&gt;-- ERROR:  invalid ON DELETE action for foreign key constraint containing generated column&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Можно решить проблему триггером. Перед удалением организации вызывается функция,
которая находит заявки и удаляет в них поле organization. Таким образом DELETE
сводится к UPDATE; колонка &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;_org_id&lt;/code&gt; будет вычислена повторно и получит значение
NULL. Подготовим функцию триггера:&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_org_ref_null&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;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;'organization'&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;null&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,id}'&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;o&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;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;/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_organizations_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;organizations&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_org_ref_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-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;organizations&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-000000000651'&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;_org_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;o&quot;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'organization'&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;org&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-000000635651'&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                  │ _org_id │ org  │
├──────────────────────────────────────┼─────────┼──────┤
│ 00000000-0000-0000-0000-000000635651 │ &amp;lt;null&amp;gt;  │ null │
└──────────────────────────────────────┴─────────┴──────┘
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;прочие-сведения&quot;&gt;Прочие сведения&lt;/h2&gt;

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

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

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

&lt;p&gt;У вычисляемых колонок есть еще одно свойство: индекс по ним работает немного
быстрее, чем по выражениям. Как вы помните, индекс btree сопоставляет значение с
позицией строки, где оно находится. В редких случаях Postgres проверяет условие
повторно, даже если строка найдена. Скажем, мы создали индекс на выражение
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;(((doc #&amp;gt;&amp;gt; '{organization,id}')::uuid))&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;p&quot;&gt;...&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,id}'&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;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;UUID&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;Чтобы выполнить условие повторно (это случается при нехватке памяти), Postgres
читает колонку doc и берет и из нее поле &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;organization&lt;/code&gt;→&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;id&lt;/code&gt;. Напомним, что
при определенном пороге значения jsonb сбрасываются в TOAST-таблицу, где они
хранятся в виде фрагментов. Чтобы получить документ, требуется прочитать и
соединить все фрагменты. Для больших документов это влечет дисковые операции и
время процессора.&lt;/p&gt;

&lt;p&gt;Если же индекс создан по полю &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;_org_id&lt;/code&gt;, обращение к документу требуется. Тип
uuid использует хранение PLAIN (без тостов и сжатия, см. главу 4), поэтому
значение находится в основной таблице, а не TOAST. Проверка &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;_org_id = &amp;lt;UUID&amp;gt;&lt;/code&gt;
выполняется быстрее и потому предпочтительней.&lt;/p&gt;

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

&lt;p&gt;В следующей главе мы вернемся к теме, с которой временно переключились: как
извлекать данные из JSON-документов, но на этот раз – более мощными средствами.&lt;/p&gt;
</description>
        <pubDate>Fri, 12 Jun 2026 00:00:00 +0000</pubDate>
        <link>https://grishaev.me/pg-book-json-ch05/</link>
        <guid isPermaLink="true">https://grishaev.me/pg-book-json-ch05/</guid>
        
        <category>postgres</category>
        
        <category>json</category>
        
        <category>book</category>
        
        <category>sql</category>
        
        
      </item>
    
      <item>
        <title>Глава 4. Индексирование JSON</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;/li&gt;
  &lt;li&gt;Функции на языке Python&lt;/li&gt;
  &lt;li&gt;Версионирование и архивация документов&lt;/li&gt;
  &lt;li&gt;Релевантный поиск&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;

    Содержание

&lt;/h2&gt;

&lt;ul id=&quot;toc-item-pg-book-json-ch04&quot;&gt;
  &lt;li&gt;&lt;a href=&quot;#главы&quot; id=&quot;toc-item-pg-book-json-ch04-главы&quot;&gt;Главы&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#знакомство-с-explain&quot; id=&quot;toc-item-pg-book-json-ch04-знакомство-с-explain&quot;&gt;Знакомство с EXPLAIN&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#простые-btree-индексы&quot; id=&quot;toc-item-pg-book-json-ch04-простые-btree-индексы&quot;&gt;Простые Btree-индексы&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#еще-раз-о-uuid-v7&quot; id=&quot;toc-item-pg-book-json-ch04-еще-раз-о-uuid-v7&quot;&gt;Еще раз о UUID v7&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#индексирование-полей-json&quot; id=&quot;toc-item-pg-book-json-ch04-индексирование-полей-json&quot;&gt;Индексирование полей JSON&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#индексирование-дат&quot; id=&quot;toc-item-pg-book-json-ch04-индексирование-дат&quot;&gt;Индексирование дат&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#коротко-о-hash-индексе&quot; id=&quot;toc-item-pg-book-json-ch04-коротко-о-hash-индексе&quot;&gt;Коротко о hash-индексе&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#статистика-индексов&quot; id=&quot;toc-item-pg-book-json-ch04-статистика-индексов&quot;&gt;Статистика индексов&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#составные-индексы&quot; id=&quot;toc-item-pg-book-json-ch04-составные-индексы&quot;&gt;Составные индексы&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#поиск-по-шаблону&quot; id=&quot;toc-item-pg-book-json-ch04-поиск-по-шаблону&quot;&gt;Поиск по шаблону&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#триграммный-поиск-по-многим-полям&quot; id=&quot;toc-item-pg-book-json-ch04-триграммный-поиск-по-многим-полям&quot;&gt;Триграммный поиск по многим полям&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#коротко-о-ранжировании&quot; id=&quot;toc-item-pg-book-json-ch04-коротко-о-ранжировании&quot;&gt;Коротко о ранжировании&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#регулярные-выражения&quot; id=&quot;toc-item-pg-book-json-ch04-регулярные-выражения&quot;&gt;Регулярные выражения&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#индексирование-документа-целиком&quot; id=&quot;toc-item-pg-book-json-ch04-индексирование-документа-целиком&quot;&gt;Индексирование документа целиком&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;В прошлой главе мы опробовали поиск документов по разным полям. На тот момент
нас интересовало, можно ли это делать в принципе. Мы упомянули, что отложим
вопрос быстродействия на потом, когда будем разбираться с индексами. Этим мы и
займемся в этой главе: узнаем, как индексировать документы по отдельным полям и
целиком, какие бывают индексы и многое другое.&lt;/em&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;Индексы встречаются в коде на языках вроде Java или Python. Предположим, есть
список пользователей, и нужно искать их по номеру. Чтобы не сканировать список
каждый раз, строится словарь вида &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;id-&amp;gt;user&lt;/code&gt;, и тогда пользователя можно извлечь
за константное время. Чтобы построить такой словарь, потребуется один
обход. Приведем код на Python:&lt;/p&gt;

&lt;!-- more --&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;users&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;id&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;101&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;name&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;Ivan&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;age&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;14&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;id&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;202&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;name&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;John&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;age&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;34&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;id&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;303&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;name&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;Juan&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;age&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;51&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;id_to_user&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;user&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;id&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;user&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;user&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;users&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;user&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;id_to_user&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;202&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;user&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;# {'id': 202, 'name': 'John', 'age': 34}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Подобные словари называются индексными или просто индексами. Выше индекс
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;id_to_user&lt;/code&gt; сопоставляет номер пользователя и объект. Его можно упростить:
вместо самого пользователя хранить его позицию в списке. По id мы получим
позицию, а по ней – словарь пользователя:&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;id_to_index&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;user&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;id&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;i&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;i&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;user&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;enumerate&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;users&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;index&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;id_to_index&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;202&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# 1
&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;user&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;users&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;index&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;user&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;# {'id': 202, 'name': 'John', 'age': 34}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Аналогично строятся индексы по другим полям, например имени или электронной
почте. В отличие от ID, эти поля не уникальны, поэтому в значениях словаря будет
не один элемент, а их список. Ниже индекс &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;name_to_ids&lt;/code&gt; показывает, что в
системе три пользователя по имени John и кодами 202, 345 и 582.&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;name_to_ids&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;'Ivan'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;101&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;],&lt;/span&gt;
    &lt;span class=&quot;s&quot;&gt;'John'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;202&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;345&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;582&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;p&gt;По умолчанию выборка строк устроена линейно. Алгоритм перебирает записи по одной
и проверяет, подходит ли она условию WHERE. Если да, запись попадает в итоговую
выборку. Чем больше таблица, тем дольше выполняется обход. Ситуация, когда когда
запрос проверяет все записи, называется full sequence scan или, на жаргоне
специалистов по базам данных, “фул-скан”.&lt;/p&gt;

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

&lt;p&gt;Чтобы ускорить поиск в таблице, используют индексы. Реляционные базы чаще всего
полагаются на индексы &lt;a href=&quot;https://ru.wikipedia.org/wiki/B-%D0%B4%D0%B5%D1%80%D0%B5%D0%B2%D0%BE&quot;&gt;B-tree&lt;/a&gt;. Это структура данных, которая связывает
значение с номером страницы, где находится строка. Чаще всего значение –
колонка, однако может быть и сложным выражением: конкатенацией полей, вызовом
функции и так далее.&lt;/p&gt;

&lt;p&gt;Индекс B-tree устроен как дерево узлов; в свою очередь узлы делятся на
уровни. Если это не самый нижний уровень, узлы хранят значения и ссылки на
нижележащие узлы. Нижний уровень, который еще называют листовым, содержит
координаты строки – TID, Tuple ID. Эта пара чисел (P, T), где P (page) – номер
страницы, а T (tuple) – номер кортежа внутри нее. На схеме ниже показан типичный
индекс B-tree:&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;┌──────────────────────────────────────────────────────────┐
│                           ┌───────────┐                  │
│                           │ Metadata  │                  │
│                           └─────┬─────┘                  │
│                                 │                        │
│ values                    ┌─────▼─────┐                  │
│ lvl 1          ┌──────────│  4 32 64  ├──────────┐       │
│                │          └─────┬─────┘          │       │
│                │                │                │       │
│ values   ┌─────▼─────┐    ┌─────▼─────┐    ┌─────▼─────┐ │
│ lvl 2    │  4 16 25  ◀────▶ 32 43 49  ◀────▶ 64 79 89  │ │
│          └──┬──┬──┬──┘    └──┬──┬──┬──┘    └──┬──┬──┬──┘ │
│             │  │  │          │  │  │          │  │  │    │
│          ┌──▼──▼──▼──┐    ┌──▼──▼──▼──┐    ┌──▼──▼──▼──┐ │
│ page     │104 523 662│    │223 303 315│    │662 824 908│ │
│ tuple    │  5  25  61│    │ 40  23   1│    │ 13  21   7│ │
│          └───────────┘    └───────────┘    └───────────┘ │
└──────────────────────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Если требуется найти в таблице строку со значением 25, происходит
следующее. Сперва мы оказываемся на первом уровне индекса. Для простоты он
содержит всего три значения: 4, 32 и 64, но на практике их гораздо
больше. Промышленный индекс содержит два в степени N элементов в одном узле,
например 256 или 512, что делает невозможным его реалистичное изображение.&lt;/p&gt;

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

&lt;p&gt;Числу 25 сопоставлена пара: номер страницы (662), где находится искомая строка,
и номер строки в этой странице (61). Положение страницы в файле вычислим по
номеру: для этого умножим его на 8192 (размер страницы в байтах по
умолчанию). Получим смещение в 5.423.104 байта от начала файла. Postgres читает
из файла 8 килобайт с найденным смещением – это и есть страница 662.&lt;/p&gt;

&lt;p&gt;Страница помещается в буферный кэш; в среднем она содержит 10-50 строк. Чтобы
найти среди них нужную, Postgres умножает длину строки (L) на ее номер
(61). Длина строки постоянна в рамках таблицы и известна заранее. По полученному
смещению Postgres читает L байт, что дает искомую строку.&lt;/p&gt;

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

&lt;p&gt;В интернете доступны приложения на Javascript, которые визуализируют устройство
Btree. Можно указать размер узла, порог разбиения и другие начальные факторы,
после чего добавлять и удалять элементы. При этом на холсте показано дерево
узлов с плавными анимациями. Одно из таких приложений доступно по адресу
&lt;a href=&quot;https://btree.app/&quot;&gt;btree.app&lt;/a&gt;; предлагаем читателю поработать с ним, чтобы лучше понять
устройство Btree.&lt;/p&gt;

&lt;p&gt;Индексы бывают разных структур и топологий: кроме btree, Postgres поддерживает
hash-индексы, семейство GIN и GiST для типов-коллекций, BRIN для чрезвычайно
больших таблиц. Расширение PostGis предлагает пространственные индексы для
поиска на плоскости и в пространстве, индексы заполняющей кривой и так
далее. Некоторые из них мы рассмотрим в этой и следующих
главах. Заинтересованным читателям еще раз порекомендуем &lt;a href=&quot;https://postgrespro.ru/docs/enterprise/16/internals&quot;&gt;книги&lt;/a&gt; и
&lt;a href=&quot;https://postgrespro.ru/docs/postgrespro/current/btree&quot;&gt;документацию&lt;/a&gt; компании Postgres Pro, где индексы описаны во всех
подробностях.&lt;/p&gt;

&lt;h2 id=&quot;знакомство-с-explain&quot;&gt;Знакомство с EXPLAIN&lt;/h2&gt;

&lt;p&gt;Вспомнив теорию, вернемся к нашему миллиону заявок. Когда мы объявили таблицу
applications, для первичного ключа автоматически создался индекс. Каждый раз,
когда мы ищем документ по полю id, база обращается к нему. Это легко проверить,
предварив запрос командой &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;EXPLAIN&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;explain&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;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-000000411999'&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;┌─────────────────────────────────────────────────────────────────────────────────────────┐
│                                       QUERY PLAN                                        │
├─────────────────────────────────────────────────────────────────────────────────────────┤
│ Index Scan using applications_pkey on applications  (cost=0.42..8.44 rows=1 width=1729) │
│   Index Cond: (id = '00000000-0000-0000-0000-000000411999'::uuid)                       │
└─────────────────────────────────────────────────────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

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

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

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

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

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

&lt;p&gt;На тему того, как правильно читать план, написаны многие статьи. Поскольку наша
книга посвящена работе с JSON, мы не будем рассматривать EXPLAIN во всех
деталях. Перечислим только те моменты, на которых будем заострять
внимание. Заинтересованным читателям рекомендуем книги
&lt;a href=&quot;https://postgrespro.ru/education/books/qptbook&quot;&gt;“PostgreSQL 16. Оптимизация запросов”&lt;/a&gt; и
&lt;a href=&quot;https://postgrespro.ru/education/books/advancedsql&quot;&gt;“PostgreSQL. Профессиональный SQL”&lt;/a&gt;, где команда EXPLAIN описана
подробно.&lt;/p&gt;

&lt;p&gt;Итак, в выводе EXPLAIN ANALYZE нас интересует следующее. Прежде всего это
показатель cost, который выражен двумя числами через две точки. В примере выше
это cost=0.42..8.44. Cost, что по-английски “стоимость”, описывает затраты на
выполнение запроса в абстрактных единицах. Это не секунды и не мегабайты памяти,
а условная валюта, в которой сервер оценивает усилия.&lt;/p&gt;

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

&lt;div class=&quot;language-sql highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;explain&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;select&lt;/span&gt; &lt;span class=&quot;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;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;┌─────────────────────────────────────────────────────────────────────────┐
│                               QUERY PLAN                                │
├─────────────────────────────────────────────────────────────────────────┤
│ Seq Scan on applications  (cost=0.00..359524.33 rows=965633 width=1729) │
└─────────────────────────────────────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

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

&lt;div class=&quot;language-sql highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;explain&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;select&lt;/span&gt; &lt;span class=&quot;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;order&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;by&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;nb&quot;&gt;int&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;┌──────────────────────────────────────────────────────────────────────────────────────────────┐
│                                          QUERY PLAN                                          │
├──────────────────────────────────────────────────────────────────────────────────────────────┤
│ Gather Merge  (cost=1000457.74..1094345.26 rows=804694 width=1733)                           │
│   Workers Planned: 2                                                                         │
│   -&amp;gt;  Sort  (cost=999457.72..1000463.59 rows=402347 width=1733)                              │
│         Sort Key: (((doc -&amp;gt;&amp;gt; 'application_id'::text))::integer)                              │
│         -&amp;gt;  Parallel Seq Scan on applications  (cost=0.00..356909.07 rows=402347 width=1733) │
└──────────────────────────────────────────────────────────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

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

&lt;p&gt;Параметр ANALYZE добавляет в план новый критерий: фактическое время запроса. Это
строка Execution Time; в примере ниже она показывает 3.9 миллисекунды:&lt;/p&gt;

&lt;div class=&quot;language-sql highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;explain&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;analyze&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;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-000000411999'&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;┌───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│                                                            QUERY PLAN                                                             │
├───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ Index Scan using applications_pkey on applications  (cost=0.42..8.44 rows=1 width=1830) (actual time=3.786..3.789 rows=1 loops=1) │
│   Index Cond: (id = '00000000-0000-0000-0000-000000411999'::uuid)                                                                 │
│ Planning Time: 0.179 ms                                                                                                           │
│ Execution Time: 3.906 ms                                                                                                          │
└───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Чем меньше этот показатель, тем лучше. Трудно обозначить порог, выше которого
запрос считается медленным – это зависит от его природы и требований. В идеале
запросы вида get-by-id (штучная запись по ключу) укладываются в несколько
десятков миллисекунд (10-20), а выборка разумного числа записей по индексу – в
несколько сотен (100-300). Однако эти числа условны и могут быть пересмотрены.&lt;/p&gt;

&lt;p&gt;У сервера Postgresql есть особенность, которую, возможно, вы замечали. Если
выполнить запрос дважды, во второй раз он сработает быстрее. Разница
значительна: иной раз она составляет порядок, например: сначала 3 секунды, а
затем 300 миллисекунд.&lt;/p&gt;

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

&lt;p&gt;Выражение &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;EXPLAIN BUFFERS&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;explain&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;analyze&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;buffers&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;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-000000411999'&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;┌───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│                                                            QUERY PLAN                                                             │
├───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ Index Scan using applications_pkey on applications  (cost=0.42..8.44 rows=1 width=1830) (actual time=2.820..2.824 rows=1 loops=1) │
│   Index Cond: (id = '00000000-0000-0000-0000-000000411999'::uuid)                                                                 │
│   Buffers: shared read=4                                                                                                          │
│ Planning Time: 0.750 ms                                                                                                           │
│ Execution Time: 2.957 ms                                                                                                          │
└───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
&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;┌───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│                                                            QUERY PLAN                                                             │
├───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ Index Scan using applications_pkey on applications  (cost=0.42..8.44 rows=1 width=1830) (actual time=0.054..0.055 rows=1 loops=1) │
│   Index Cond: (id = '00000000-0000-0000-0000-000000411999'::uuid)                                                                 │
│   Buffers: shared hit=4                                                                                                           │
│ Planning Time: 0.105 ms                                                                                                           │
│ Execution Time: 0.070 ms                                                                                                          │
└───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Отличие в строках shared read=4 и shared hit=4. Слово “read” означает
“прочитано” (с диска), а “hit” (“попадание”) – удачный поиск в буферном
кэше. Обратите внимание, что второй запрос отработал в 40 раз быстрее – вот
какое ускорение дает буферный кэш.&lt;/p&gt;

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

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

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

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

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

&lt;p&gt;Сканирование индекса тоже бывает разным: кроме обычного “Index Scan” встречается
“Index Only Scan”. Этот метод означает, что данные взяты из индекса без
обращения к диску. Метод Bitmap Index Scan подразумевает два действия: в первый
раз строится битовая карта страниц, которые подходят условию. Битовая карта –
это массив, где бит с номером N означает какой-то признак, например подходит
запись условию или нет. Карт может быть несколько: для условия &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;where field1 =
'abc' and field2 = 'xyz'&lt;/code&gt; могут быть построены две отдельных карты. Далее они
объединяются битовым AND, и по итоговой карте обходится основная таблица.&lt;/p&gt;

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

&lt;p&gt;Мы поверхностно рассмотрели вывод EXPLAIN и что нас в нем интересует. Вернемся к
главной теме главы – работе с индексами.&lt;/p&gt;

&lt;h2 id=&quot;простые-btree-индексы&quot;&gt;Простые Btree-индексы&lt;/h2&gt;

&lt;p&gt;Итак, у нас есть индекс для первичного ключа, чтобы искать документы по полю
id. Другой частый случай – вывести документы дате создания или за период:
прошлую неделю или квартал. Для этого нужен индекс btree по полю
&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;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_created_at&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;btree&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;Сразу после создания выполните команду ANALYZE с именем таблицы:&lt;/p&gt;

&lt;div class=&quot;language-sql highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;analyze&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;applications&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;Эта команда обновляет статистику: строит гистограмму (распределение) значений и
выделяет те, что встречаются часто. Статистика крайне важна для построения
плана. Иногда запрос не использует индекс, потому что данные изменились (был
крупный UPDATE), а статистика все еще содержит старые показатели. Postgres
обновляет статистику по расписанию, но мы вызвали команду явно. Предполагается,
что после каждого действия над индексом вы тоже вызываете ANALYZE.&lt;/p&gt;

&lt;p&gt;Убедимся, что индекс работает. Для этого выберем 100 последних документов и
проверим план. Выражение Index Scan показывает, что мы попали в индекс:&lt;/p&gt;

&lt;div class=&quot;language-sql highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;explain&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;select&lt;/span&gt; &lt;span class=&quot;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;order&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;by&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;created_at&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;desc&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;limit&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;100&lt;/span&gt;&lt;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;┌────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│                                                     QUERY PLAN                                                     │
├────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ Limit  (cost=0.42..37.19 rows=100 width=1736)                                                                      │
│   -&amp;gt;  Index Scan using idx_applications_created_at on applications  (cost=0.42..368293.21 rows=1001719 width=1736) │
└────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Вариант с диапазоном: ограничим время создания документа каким-то днем. Для
этого служит оператор between, где левое и правое значения – границы:&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;explain
select id, doc from applications
where created_at between '2025-07-30 00:00:00Z' and '2025-07-30 23:59:59Z'
order by created_at desc;
&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;┌───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│                                                                        QUERY PLAN                                                                         │
├───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ Index Scan using idx_applications_created_at on applications  (cost=0.42..10918.55 rows=2711 width=1738)                                                  │
│   Index Cond: ((created_at &amp;gt;= '2025-07-30 03:00:00+03'::timestamp with time zone) AND (created_at &amp;lt;= '2025-07-31 02:59:59+03'::timestamp with time zone)) │
└───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
&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;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;'2025-07-30'&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;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;'2025-07-30'&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;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 day'&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 second'&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;timestamptz&lt;/code&gt;, во втором – прибавляем
день и вычитаем секунду.&lt;/p&gt;

&lt;p&gt;Обратите внимание, что мы создали индекс в режиме по убыванию (слово desc после
created_at). Мы выбрали этот способ, потому что чаще всего в работе нужны
последние документы. Это упрощает работу сервера: если направления индекса и
сортировки запроса совпадают (оба asc или оба desc), индекс обходится в прямом
порядке. Если нет, то с конца.&lt;/p&gt;

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

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

&lt;p&gt;Если для поля создан индекс btree, вот какие возможности он дает в запросах:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;поиск по точному значению (оператор =);&lt;/li&gt;
  &lt;li&gt;сортировка по возрастанию и убыванию (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;order by ... asc/desc&lt;/code&gt;);&lt;/li&gt;
  &lt;li&gt;поиск по условию “меньше” и “больше” (&amp;lt;, &amp;lt;=, &amp;gt;, =&amp;gt;);&lt;/li&gt;
  &lt;li&gt;отбор по диапазону (оператор between).&lt;/li&gt;
&lt;/ul&gt;

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

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

&lt;h2 id=&quot;еще-раз-о-uuid-v7&quot;&gt;Еще раз о UUID v7&lt;/h2&gt;

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

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

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

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;00000000-0000-0000-0000-000000411997
00000000-0000-0000-0000-000000411998
00000000-0000-0000-0000-000000411999
&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;f1f0a631-a393-47d6-be8d-e082222a9b9a
d108a560-98a4-4c6a-b63e-d4d4f90cc3f7
b5efc9a5-8d27-44e6-a5d8-d81c519820fa
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

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

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

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

&lt;p&gt;Схематично UUID v7 представлен ниже. Его лидирующая часть – число микросекунд
эпохи Unix. Автор произвел три идентификатора, и временная метка каждый раз
увеличивалась.&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;┌─────────────────────┬───────┬───────────────┐
│Unix timestamp, ms   │Seq    │Random part    │
├─────────────────────┼───────┼───────────────┤
│1778344072736        │1      │6AC83E922      │
├─────────────────────┼───────┼───────────────┤
│1778344106125        │3      │7C4C36B36      │
├─────────────────────┼───────┼───────────────┤
│1778344112067        │2      │7A42AC302      │
└─────────────────────┴───────┴───────────────┘
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;После времени Unix несколько бит выделено под счетчик в рамках одной
миллисекунды. Современные компьютеры столь быстры, что производят сотни
идентификаторов за одну миллисекунду. Если заполнить оставшиеся биты случайно,
теряется упорядоченность – ключевое свойство алгоритма. Чтобы упорядочить
идентификаторы, в рамках каждой миллисекунды запускается счетчик. Скажем, если в
момент времени 1778344106125 приложение произвело четыре идентификатора, то у
последнего в поле seq будет записано число 3 (если считать от нуля). Оставшиеся
после счетчика биты заполняются случайно (поле Random part).&lt;/p&gt;

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

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://theproductguy.in/blogs/uuid-v7-time-ordered/&quot;&gt;UUID v7: Time-Ordered UUIDs for Better Database Performance&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://www.thenile.dev/blog/uuidv7&quot;&gt;UUIDv7 Comes to PostgreSQL 18&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://www.rfc-editor.org/rfc/rfc9562&quot;&gt;RFC 9562 Universally Unique IDentifiers (UUIDs)&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://postgrespro.ru/docs/postgresql/current/functions-uuid&quot;&gt;Postgres Pro: функции генерирования UUID&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;Алгоритм UUID v7 появился в Postgres с версии 18, которая вышла осенью 2025
года. Если вы еще не обновились, возможны следующие варианты:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;установите стороннее расширение &lt;a href=&quot;https://pgxn.org/dist/pg_uuidv7/&quot;&gt;pg_uuidv7&lt;/a&gt;;&lt;/li&gt;
  &lt;li&gt;создавайте идентификаторы в приложении силами Python, Java и других языков;&lt;/li&gt;
  &lt;li&gt;напишите функцию на встроенном языке Postgres. Примеры и обсуждение доступны в
фрагменте gist &lt;a href=&quot;https://gist.github.com/kjmph/5bd772b2c2df145aa645b837da7eca74&quot;&gt;по ссылке&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Покажем теперь, как искать записи по датам, используя идентификатор. Заявки, что
мы произвели в прошлой главе, не подходят для этой цели – их ключи не являются
UUID v7. Нам понадобится таблица с двумя полями: ключом и временем.&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;uuid_v7_demo&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;primary&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;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;p&quot;&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

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

&lt;div class=&quot;language-sql highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;insert&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;into&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;uuid_v7_demo&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;select&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;uuidv7&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;interval&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'-1 day'&lt;/span&gt; &lt;span class=&quot;o&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;current_timestamp&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 day'&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;x&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;9999&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;Без параметров функция uuidv7 производит идентификатор с текущим временем. Можно
передать интервал (в том числе отрицательный), который играет роль
смещения. Запрос производит десять тысяч записей, при этом из created_at
вычитается день, два и так далее. То же самое касается ключа: при каждой
генерации от текущей метки Unix timestamp отнимается нарастающий интервал. Если
выбрать строки по возрастанию ключа, порядок будет согласован с датой создания:&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;uuid_v7_demo&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;order&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;by&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;id&lt;/span&gt; &lt;span class=&quot;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;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           │
├──────────────────────────────────────┼───────────────────────────────┤
│ 00d4e887-b56f-7584-9190-d522f266d4bf │ 1998-12-23 20:31:14.630735+03 │
│ 00d4edae-116f-757d-a295-7f89e12c8bda │ 1998-12-24 20:31:14.630735+03 │
│ 00d4f2d4-6d6f-7575-b5d0-621727a083ed │ 1998-12-25 20:31:14.630735+03 │
│ 00d4f7fa-c96f-756c-999d-2c22d558596e │ 1998-12-26 20:31:14.630735+03 │
│ 00d4fd21-256f-7567-8158-5acbfc067301 │ 1998-12-27 20:31:14.630735+03 │
│ 00d50247-816f-755d-8377-c12809f0a1c3 │ 1998-12-28 20:31:14.630735+03 │
│ 00d5076d-dd6f-7550-b939-7c10f9f9ce5f │ 1998-12-29 20:31:14.630735+03 │
│ 00d50c94-396f-754d-bb77-9d663b4f31ea │ 1998-12-30 20:31:14.630735+03 │
│ 00d511ba-956f-7544-a16d-e9c7f8613f04 │ 1998-12-31 20:31:14.630735+03 │
│ 00d516e0-f16f-753e-8a0d-1854d65ae69e │ 1999-01-01 20:31:14.630735+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;uuid_extract_timestamp&lt;/code&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;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;uuid_extract_timestamp&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;ts&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;uuid_v7_demo&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;order&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;by&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;id&lt;/span&gt; &lt;span class=&quot;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;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           │             ts             │
├──────────────────────────────────────┼───────────────────────────────┼────────────────────────────┤
│ 00d4e887-b56f-7584-9190-d522f266d4bf │ 1998-12-23 20:31:14.630735+03 │ 1998-12-23 20:31:14.671+03 │
│ 00d4edae-116f-757d-a295-7f89e12c8bda │ 1998-12-24 20:31:14.630735+03 │ 1998-12-24 20:31:14.671+03 │
│ 00d4f2d4-6d6f-7575-b5d0-621727a083ed │ 1998-12-25 20:31:14.630735+03 │ 1998-12-25 20:31:14.671+03 │
│ 00d4f7fa-c96f-756c-999d-2c22d558596e │ 1998-12-26 20:31:14.630735+03 │ 1998-12-26 20:31:14.671+03 │
│ 00d4fd21-256f-7567-8158-5acbfc067301 │ 1998-12-27 20:31:14.630735+03 │ 1998-12-27 20:31:14.671+03 │
│ 00d50247-816f-755d-8377-c12809f0a1c3 │ 1998-12-28 20:31:14.630735+03 │ 1998-12-28 20:31:14.671+03 │
│ 00d5076d-dd6f-7550-b939-7c10f9f9ce5f │ 1998-12-29 20:31:14.630735+03 │ 1998-12-29 20:31:14.671+03 │
│ 00d50c94-396f-754d-bb77-9d663b4f31ea │ 1998-12-30 20:31:14.630735+03 │ 1998-12-30 20:31:14.671+03 │
│ 00d511ba-956f-7544-a16d-e9c7f8613f04 │ 1998-12-31 20:31:14.630735+03 │ 1998-12-31 20:31:14.671+03 │
│ 00d516e0-f16f-753e-8a0d-1854d65ae69e │ 1999-01-01 20:31:14.630735+03 │ 1999-01-01 20:31:14.671+03 │
│ 00d51c07-4d6f-7536-af14-ebc968cfa7b4 │ 1999-01-02 20:31:14.630735+03 │ 1999-01-02 20:31:14.671+03 │
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

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

&lt;div class=&quot;language-sql highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;select&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;uuid_v7_demo&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;where&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;id&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;between&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;uuidv7&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;interval&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'-3 months'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;and&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;uuidv7&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;interval&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'-2 months'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;order&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;by&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;id&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;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           │
├──────────────────────────────────────┼───────────────────────────────┤
│ 019c489b-7949-7d87-aa8f-de9eeb8726e4 │ 2026-02-10 20:31:14.630735+03 │
│ 019c4dc1-d549-7d7c-85f6-ba7201c61a63 │ 2026-02-11 20:31:14.630735+03 │
│ 019c52e8-3149-7d76-b205-897059e6d8ff │ 2026-02-12 20:31:14.630735+03 │
│ 019c580e-8d49-7d68-88d7-e846afc2c5b5 │ 2026-02-13 20:31:14.630735+03 │
│ 019c5d34-e949-7d61-8199-7ce0fed26860 │ 2026-02-14 20:31:14.630735+03 │
│ 019c625b-4549-7d5a-971e-fa0b9a827175 │ 2026-02-15 20:31:14.630735+03 │
│ 019c6781-a149-7d4f-a4f3-3f3ea25bcb2e │ 2026-02-16 20:31:14.630735+03 │
│ 019c6ca7-fd49-7d45-9449-ae8b3c2f8a9f │ 2026-02-17 20:31:14.630735+03 │
│ 019c71ce-5949-7d3f-84a5-f11c99d9e3cf │ 2026-02-18 20:31:14.630735+03 │
│ 019c76f4-b549-7d35-821c-f64da5398f11 │ 2026-02-19 20:31:14.630735+03 │
│ 019c7c1b-1149-7d2c-af1e-2b27042b726f │ 2026-02-20 20:31:14.630735+03 │
│ 019c8141-6d49-7d26-8e6e-a9cab44b4b64 │ 2026-02-21 20:31:14.630735+03 │
│ 019c8667-c949-7d1f-b9f5-7cf09ff6c908 │ 2026-02-22 20:31:14.630735+03 │
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Если предварить запрос выражением EXPLAIN ANALYZE, увидим попадание в индекс:&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;┌──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│                                                                    QUERY PLAN                                                                    │
├──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ Limit  (cost=0.29..54.38 rows=100 width=24) (actual time=16.618..16.744 rows=28.00 loops=1)                                                      │
│   Buffers: shared hit=134                                                                                                                        │
│   -&amp;gt;  Index Scan using uuid_v7_demo_pkey on uuid_v7_demo  (cost=0.29..601.26 rows=1111 width=24) (actual time=16.615..16.737 rows=28.00 loops=1) │
│         Filter: ((id &amp;gt;= uuidv7('-3 mons'::interval)) AND (id &amp;lt;= uuidv7('-2 mons'::interval)))                                                    │
│         Rows Removed by Filter: 9971                                                                                                             │
│         Index Searches: 1                                                                                                                        │
│         Buffers: shared hit=134                                                                                                                  │
│ Planning Time: 0.139 ms                                                                                                                          │
│ Execution Time: 16.767 ms                                                                                                                        │
└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

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

&lt;p&gt;Автор надеется, что читатель осведомлен о преимуществах UUID v7 и с радостью
применит их. Далее по тексту мы будем пользоваться UUID v4 – случайным и
неупорядоченным. Цель этого решения в том, чтобы примеры из книги работали со
многими версиями Postgres, а не только 18.&lt;/p&gt;

&lt;h2 id=&quot;индексирование-полей-json&quot;&gt;Индексирование полей JSON&lt;/h2&gt;

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

&lt;div class=&quot;language-sql highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;select&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;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;'application_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;app_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;limit&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;10&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;/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                  │ app_id │
├──────────────────────────────────────┼────────┤
│ 00000000-0000-0000-0000-000000009985 │ 9985   │
│ 00000000-0000-0000-0000-000000009986 │ 9986   │
│ 00000000-0000-0000-0000-000000009987 │ 9987   │
│ 00000000-0000-0000-0000-000000009988 │ 9988   │
│ 00000000-0000-0000-0000-000000009989 │ 9989   │
│ 00000000-0000-0000-0000-000000009990 │ 9990   │
│ 00000000-0000-0000-0000-000000009991 │ 9991   │
│ 00000000-0000-0000-0000-000000009992 │ 9992   │
│ 00000000-0000-0000-0000-000000009993 │ 9993   │
│ 00000000-0000-0000-0000-000000009994 │ 9994   │
└──────────────────────────────────────┴────────┘
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Требуется искать и сортировать документы по номеру. Для этого объявим индекс
btree, где поле приводится к числу:&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_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;k&quot;&gt;using&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;btree&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;nb&quot;&gt;int&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. Прежде всего это поиск по точному
значению:&lt;/p&gt;

&lt;div class=&quot;language-sql highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;explain&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;select&lt;/span&gt; &lt;span class=&quot;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;nb&quot;&gt;int&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;550433&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;┌───────────────────────────────────────────────────────────────────────────────────────────────────────┐
│                                              QUERY PLAN                                               │
├───────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ Index Scan using idx_applications_application_id on applications  (cost=0.42..8.44 rows=1 width=1730) │
│   Index Cond: (((doc -&amp;gt;&amp;gt; 'application_id'::text))::integer = 550433)                                  │
└───────────────────────────────────────────────────────────────────────────────────────────────────────┘
&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;explain&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;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;nb&quot;&gt;int&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;between&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;550000&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;and&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;550099&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;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;nb&quot;&gt;int&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;asc&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;┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│                                                             QUERY PLAN                                                              │
├─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ Index Scan using idx_applications_application_id on applications  (cost=0.42..286.56 rows=94 width=1734)                            │
│   Index Cond: ((((doc -&amp;gt;&amp;gt; 'application_id'::text))::integer &amp;gt;= 550000) AND (((doc -&amp;gt;&amp;gt; 'application_id'::text))::integer &amp;lt;= 550099)) │
└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

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

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

&lt;p&gt;Вот как это продемонстрировать: изменим WHERE так, чтобы результат был тем же
самым, но синтаксис отличался.  Для этого вместо двойной стрелки поставим
двойной уголок с массивом:&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;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;/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;gt;&amp;gt;&lt;/code&gt; и &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;#&amp;gt;&amp;gt;&lt;/code&gt; возвращают элемент JSON в виде
строки. Отличаются они методом доступа к элементу: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;-&amp;gt;&amp;gt;&lt;/code&gt; принимает имя поля, а
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;#&amp;gt;&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;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;nb&quot;&gt;int&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;nb&quot;&gt;int&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;explain&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;analyze&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;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;nb&quot;&gt;int&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;550099&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;┌───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│                                                            QUERY PLAN                                                             │
├───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ Gather  (cost=1000.00..259833.33 rows=5000 width=1830) (actual time=268.194..453.280 rows=1 loops=1)                              │
│   Workers Planned: 2                                                                                                              │
│   Workers Launched: 2                                                                                                             │
│   -&amp;gt;  Parallel Seq Scan on applications  (cost=0.00..258333.33 rows=2083 width=1830) (actual time=88.504..146.584 rows=0 loops=3) │
│         Filter: (((doc #&amp;gt;&amp;gt; '{application_id}'::text[]))::integer = 550099)                                                        │
│         Rows Removed by Filter: 333333                                                                                            │
│ Planning Time: 2.099 ms                                                                                                           │
│ Execution Time: 453.380 ms                                                                                                        │
└───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Мы получили тот же документ 550099, но это заняло почти полсекунды и обход всей
таблицы.&lt;/p&gt;

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

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;n&quot;&gt;FIELD_APP_ID&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;SQL&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;raw&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;((doc #&amp;gt;&amp;gt; '{application_id}')::int)&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&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;nb&quot;&gt;all&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;().&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;filter&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;FIELD_APP_ID&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;eq&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1199&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)).&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;query&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;::int&lt;/code&gt;). Сравнить
такой индекс можно только с числом. Попытка передать строку окончится ошибкой
еще до выполнения запроса.&lt;/p&gt;

&lt;p&gt;Что случится, если убрать из индекса часть &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;::int&lt;/code&gt;? В этом случае индекс хранит
строки, а номер документа тоже передают строкой. Покажем это на примере:&lt;/p&gt;

&lt;div class=&quot;language-sql highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;create&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;index&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;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_id_text&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;btree&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;explain&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;analyze&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;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;'550099'&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;┌──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│                                                                      QUERY PLAN                                                                      │
├──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ Index Scan using idx_applications_application_id_text on applications  (cost=0.42..8.44 rows=1 width=1830) (actual time=0.060..0.062 rows=1 loops=1) │
│   Index Cond: ((doc -&amp;gt;&amp;gt; 'application_id'::text) = '550099'::text)                                                                                    │
│ Planning Time: 0.196 ms                                                                                                                              │
│ Execution Time: 0.085 ms                                                                                                                             │
└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
&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;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;as&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;doc_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;order&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;by&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;limit&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;100&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;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_id  │
├─────────┤
│ 1       │
│ 10      │
│ 100     │
│ 1000    │
│ 10000   │
│ 100000  │
│ 1000000 │
│ 100001  │
│ 100002  │
│ 100003  │
│ 100004  │
│ 100005  │
│ 100006  │
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

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

&lt;p&gt;Вот зачем мы проводим поле к числу: чтобы сортировка работала по принципу чисел,
а не строк. Кроме того, приведение типа гарантирует, что в базе не окажется
документов, где &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;application_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;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;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;nv&quot;&gt;&quot;some_field&quot;&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;nv&quot;&gt;&quot;application_id&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;&quot;123a456&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;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-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;ERROR: invalid input syntax for type integer: &quot;123a456&quot;
&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;application_id&lt;/code&gt; или оно содержит null, выражение
индекса тоже вернет null, и нарушений не будет. Позже мы рассмотрим, как сделать
некоторые поля документа обязательными (не NULL) на уровне базы.&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;created_at&lt;/code&gt; и
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;updated_at&lt;/code&gt;. Чтобы искать по ним документы, добавьте обычный btree-индекс. Если
в качестве ключа используется UUID версии 7, он уже хранит временную метку, так
что индекс по created_at может и не потребоваться.&lt;/p&gt;

&lt;p&gt;Документ содержит даты, отражающие бизнес-процессы. Это могут быть даты
одобрения заявки, ее отклонения, сдачи в архив, сроки рассмотрения. Довольно
часто документы ищут по этим критериям. Представим, что заявка содержит поле
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;approved_at&lt;/code&gt; – дату одобрения в формате ISO. Вот как назначить случайную дату в
рамках прошлого года всем заявкам:&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;'approved_at'&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_json&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;o&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;p&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;p&quot;&gt;)&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;/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;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;'approved_at'&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;applications&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;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                  │                doc                 │
├──────────────────────────────────────┼────────────────────────────────────┤
│ 00000000-0000-0000-0000-000000009985 │ &quot;2025-10-23T15:59:46.943586+03:00&quot; │
│ 00000000-0000-0000-0000-000000009986 │ &quot;2025-12-17T17:25:57.205986+03:00&quot; │
│ 00000000-0000-0000-0000-000000009987 │ &quot;2025-11-18T21:23:10.137186+03:00&quot; │
│ 00000000-0000-0000-0000-000000009988 │ &quot;2026-01-22T15:32:48.498786+03:00&quot; │
│ 00000000-0000-0000-0000-000000009989 │ &quot;2025-07-20T23:52:08.908386+03:00&quot; │
│ 00000000-0000-0000-0000-000000009990 │ &quot;2025-08-07T10:45:12.690786+03:00&quot; │
│ 00000000-0000-0000-0000-000000009991 │ &quot;2025-12-06T19:23:07.660386+03:00&quot; │
│ 00000000-0000-0000-0000-000000009992 │ &quot;2026-05-04T18:27:36.681186+03:00&quot; │
│ 00000000-0000-0000-0000-000000009993 │ &quot;2025-11-16T02:07:33.791586+03:00&quot; │
│ 00000000-0000-0000-0000-000000009994 │ &quot;2025-11-03T05:24:20.783586+03:00&quot; │
└──────────────────────────────────────┴────────────────────────────────────┘
&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;application_id&lt;/code&gt;. Разница в том, что поле приводится к
типу &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;timestamp&lt;/code&gt;, а не &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;int&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;language-sql highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;create&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;index&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;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_approved_at&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;btree&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;'approved_at'&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;at&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;time&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;zone&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'utc'&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;/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;functions in index expression must be marked IMMUTABLE
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

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

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

&lt;p&gt;{
  “type”: “object”,
  “properties”: {
    “approved_at”: {
      “type”: “string”,
      “format”: “date-time”
    }
  }
}&lt;/p&gt;

&lt;p&gt;Спецификация JSON Schema предлагает формат &lt;a href=&quot;https://json-schema.org/understanding-json-schema/reference/type#dates-and-times&quot;&gt;date-time&lt;/a&gt; для
строки, который гарантирует, что внутри – дата и время в формате ISO.&lt;/p&gt;

&lt;p&gt;Поэтому, чтобы заставить Postgres построить индекс, обернем приведение типа в
функцию с тегом IMMUTABLE:&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;to_tsz_immutable&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;k&quot;&gt;returns&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;timestamptz&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;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;timestamptz&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Уточним: технически возможно написать функцию, которая объявлена неизменяемой,
но не является таковой. Postgres никак это не проверяет; ответственность за тег
immutable лежит на вас. Если построить индекс с функцией, которая зависит от
внешних факторов, вас ждет странное поведение.&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;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_approved_at&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;btree&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;to_tsz_immutable&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;'approved_at'&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;explain&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;analyze&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;to_tsz_immutable&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;'approved_at'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;between&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;current_timestamp&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;'3 months'&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;and&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;current_timestamp&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 months'&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;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;┌──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│                                                                                                      QUERY PLAN                                                                                                      │
├──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ Limit  (cost=0.43..401.63 rows=100 width=16) (actual time=0.080..0.247 rows=100 loops=1)                                                                                                                             │
│   -&amp;gt;  Index Scan using idx_applications_approved_at on applications  (cost=0.43..20060.44 rows=5000 width=16) (actual time=0.078..0.233 rows=100 loops=1)                                                            │
│         Index Cond: ((to_tsz_immutable((doc -&amp;gt;&amp;gt; 'approved_at'::text)) &amp;gt;= (CURRENT_TIMESTAMP - '3 mons'::interval)) AND (to_tsz_immutable((doc -&amp;gt;&amp;gt; 'approved_at'::text)) &amp;lt;= (CURRENT_TIMESTAMP - '1 mon'::interval))) │
│ Planning Time: 0.257 ms                                                                                                                                                                                              │
│ Execution Time: 0.277 ms                                                                                                                                                                                             │
└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;При работе с ORM поместите выражение &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;to_tsz_immutable(doc-&amp;gt;&amp;gt;'approved_at')&lt;/code&gt; в
константу, чтобы гарантировать попадание в индекс.&lt;/p&gt;

&lt;p&gt;По аналогии строятся индексы для других типов. Если Postgres не принимает
выражение &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;field::type&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;REINDEX&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;INDEX&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;my_index_name&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;коротко-о-hash-индексе&quot;&gt;Коротко о hash-индексе&lt;/h2&gt;

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

&lt;p&gt;Построим hash-индекс для поля &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;organization.short_name&lt;/code&gt; с кодом клиента. Нам не
нужна сортировка этому полю, потому что коды клиентов не образуют
последовательности. В выражении индекса изменится только тип:&lt;/p&gt;

&lt;div class=&quot;language-sql highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;create&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;index&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;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_org_short_name_hash&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;hash&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;/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;explain&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;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;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'Organization 543'&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;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;┌───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│                                                      QUERY PLAN                                                       │
├───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ Limit  (cost=0.00..400.15 rows=100 width=16)                                                                          │
│   -&amp;gt;  Index Scan using idx_applications_org_short_name_hash on applications  (cost=0.00..20007.50 rows=5000 width=16) │
│         Index Cond: ((doc #&amp;gt;&amp;gt; '{organization,short_name}'::text[]) = 'Organization 543'::text)                        │
└───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Практика показывает, что для чисел и небольших строк индекс hash оказывается
примерно на 10% быстрее. Однако на больших строках индекс замедляется:
хэш-функция перебирает символы один за другим, и чем длиннее строка, тем дольше
вычисляется хэш. Исследование и замеры вы найдете в блоге компании Cybertec в
статье &lt;a href=&quot;https://www.cybertec-postgresql.com/en/postgresql-hash-index-performance/&quot;&gt;“PostgreSQL hash index performance”&lt;/a&gt;.&lt;/p&gt;

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

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

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

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

&lt;div class=&quot;language-sql highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;select&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;pg_stat_user_indexes&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;where&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;relname&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'applications'&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;-- order by last_idx_scan desc nulls last&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;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;┌──────────────────────────────────────────────────┬──────────┬──────────────┬───────────────┐
│                   indexrelname                   │ idx_scan │ idx_tup_read │ idx_tup_fetch │
├──────────────────────────────────────────────────┼──────────┼──────────────┼───────────────┤
│ applications_pkey                                │        4 │            4 │             4 │
│ idx_applications_created_at                      │        5 │       258830 │             1 │
│ idx_applications_application_id                  │        1 │          100 │           100 │
│ idx_applications_org_short_name_hash             │        6 │         4790 │           790 │
│ idx_applications_status                          │        6 │      1495416 │             0 │
│ idx_applications_status_org_short_name           │        1 │          263 │           263 │
│ idx_applications_application_id_trgm             │        1 │           20 │             0 │
│ idx_applications_application_org_short_code_trgm │        2 │         2000 │             0 │
│ idx_applications_application_trgm_pattern        │        4 │         4138 │             0 │
│ idx_applications_application_id_text             │        2 │            2 │             2 │
└──────────────────────────────────────────────────┴──────────┴──────────────┴───────────────┘
&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;idx_scan&lt;/code&gt; – число обращений к
индексу. Очевидно, если индекс не используется, показатель будет низким;
сортировка по idx_scan выявит кандидатов на удаление.&lt;/p&gt;

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

&lt;p&gt;Полное описание &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pg_stat_user_indexes&lt;/code&gt; вы найдете в &lt;a href=&quot;https://postgrespro.ru/docs/postgrespro/current/monitoring-stats&quot;&gt;документации
Postgres&lt;/a&gt;.&lt;/p&gt;

&lt;h2 id=&quot;составные-индексы&quot;&gt;Составные индексы&lt;/h2&gt;

&lt;p&gt;Мы выяснили, как создать индекс для отдельного поля. Также мы выяснили, что
индексов не должно быть чрезмерно много. Принцип понятен, но следовать ему на
практике нелегко. Часто нас просят добавить индекс на еще одно поле, второе,
третье… их число растет, а производительность запросов – не особо. Все потому,
что массовое добавление индексов не решает проблему их качества. Ниже мы
рассмотрим случай, когда один составной индекс полезнее двух простых.&lt;/p&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;B&lt;/code&gt; и &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;C&lt;/code&gt;. Все они покрыты индексом
btree. Каждый запрос попадает в свой индекс без нареканий:&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;from&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;entity&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;where&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;a&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&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;from&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;entity&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;where&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;b&lt;/span&gt; &lt;span class=&quot;o&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;k&quot;&gt;select&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;entity&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;where&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;c&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;/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;from&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;entity&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;where&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;a&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;and&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;b&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'test'&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;and&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;c&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

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

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

&lt;ul&gt;
  &lt;li&gt;поле &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;assigned_to&lt;/code&gt; равно текущему пользователю;&lt;/li&gt;
  &lt;li&gt;статус заявки равен &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;active&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&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;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;'assigned_to'&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'ivanov@acme.com'&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;and&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;'status'&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'active'&lt;/span&gt;&lt;span class=&quot;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;assigned_to&lt;/code&gt; (назначенный
пользователь) будет почтой вида &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;user_xxx@test.com&lt;/code&gt;, где &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;xxx&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;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;'active'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'pending'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'approved'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'deleted'&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;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Это не совсем верно: в реальности большинство заявок имеют финальный статус
(одобрено или отклонено) и лишь малая часть находится на рассмотрении.&lt;/p&gt;

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

&lt;ul&gt;
  &lt;li&gt;65% – заявка сдана в архив;&lt;/li&gt;
  &lt;li&gt;20% – одобрена;&lt;/li&gt;
  &lt;li&gt;10% – отклонена;&lt;/li&gt;
  &lt;li&gt;оставшиеся 5% – в работе.&lt;/li&gt;
&lt;/ul&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;generate_status&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;plpgsql&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;n&quot;&gt;func&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;$&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;declare&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;r&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;float4&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;begin&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;r&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;random&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;case&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;when&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;r&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;65&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;then&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'archived'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;  &lt;span class=&quot;c1&quot;&gt;-- 65%&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;when&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;r&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;85&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;then&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'approved'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;  &lt;span class=&quot;c1&quot;&gt;-- 20%&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;when&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;r&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;95&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;then&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'rejected'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;  &lt;span class=&quot;c1&quot;&gt;-- 10%&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;else&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'active'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;                  &lt;span class=&quot;c1&quot;&gt;--  5%&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;case&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;n&quot;&gt;func&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;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;'assigned_to'&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;n&quot;&gt;format&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;'user_%s@test.com'&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;nb&quot;&gt;int&lt;/span&gt; &lt;span class=&quot;o&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;doc&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;'status'&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;n&quot;&gt;generate_status&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;to_char&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&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;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;mi&quot;&gt;1000000&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'0.9999'&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;ratio&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;'status'&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;status&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;group&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;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;1&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;┌─────────┬──────────┐
│  ratio  │  status  │
├─────────┼──────────┤
│  0.6498 │ archived │
│  0.2001 │ approved │
│  0.1002 │ rejected │
│  0.0499 │ active   │
└─────────┴──────────┘
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Теперь когда оба поля готовы, объявим для каждого btree-индекс:&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_status&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;btree&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;'status'&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;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_assigned_to&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;btree&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;'assigned_to'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;));&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;analyze&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;/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;explain&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;analyze&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;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;'status'&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;'active'&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;'assigned_to'&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;'user_999@test.com'&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;┌────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│                                                                   QUERY PLAN                                                                   │
├────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ Bitmap Heap Scan on applications  (cost=407.13..526.75 rows=30 width=1915) (actual time=8.974..9.047 rows=45 loops=1)                          │
│   Recheck Cond: (((doc -&amp;gt;&amp;gt; 'assigned_to'::text) = 'user_999@test.com'::text) AND ((doc -&amp;gt;&amp;gt; 'status'::text) = 'active'::text))                  │
│   Heap Blocks: exact=45                                                                                                                        │
│   -&amp;gt;  BitmapAnd  (cost=407.13..407.13 rows=30 width=0) (actual time=8.938..8.940 rows=0 loops=1)                                               │
│         -&amp;gt;  Bitmap Index Scan on idx_applications_assigned_to  (cost=0.00..8.99 rows=609 width=0) (actual time=0.361..0.361 rows=1000 loops=1) │
│               Index Cond: ((doc -&amp;gt;&amp;gt; 'assigned_to'::text) = 'user_999@test.com'::text)                                                          │
│         -&amp;gt;  Bitmap Index Scan on idx_applications_status  (cost=0.00..397.88 rows=30060 width=0) (actual time=7.956..7.956 rows=49918 loops=1) │
│               Index Cond: ((doc -&amp;gt;&amp;gt; 'status'::text) = 'active'::text)                                                                          │
│ Planning Time: 0.316 ms                                                                                                                        │
│ Execution Time: 9.338 ms                                                                                                                       │
└────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
&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;idx_applications_assigned_to&lt;/code&gt; и пометил в битовой карте кандидатов по полю
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;assigned_to&lt;/code&gt;. Далее составил такую же карту для статуса (индекс
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;idx_applications_status&lt;/code&gt;). Обе карты объединяются битовым AND; в ней остались
только те единичные биты, что были в двух исходных одновременно. По итоговой
карте Postgres выбрал строки и еще раз проверил условия. В выборке оказалось 45
документов. На исполнение ушло 9 миллисекунд – хороший показатель, который,
однако, можно улучшить.&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;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_status_assigned_to&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;btree&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;'status'&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;'assigned_to'&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;analyze&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;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;и повторим запрос &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;explain analyze&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;┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│                                                                       QUERY PLAN                                                                        │
├─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ Index Scan using idx_applications_status_assigned_to on applications  (cost=0.42..185.65 rows=51 width=1915) (actual time=0.139..0.223 rows=45 loops=1) │
│   Index Cond: (((doc -&amp;gt;&amp;gt; 'status'::text) = 'active'::text) AND ((doc -&amp;gt;&amp;gt; 'assigned_to'::text) = 'user_999@test.com'::text))                             │
│ Planning Time: 1.733 ms                                                                                                                                 │
│ Execution Time: 0.270 ms                                                                                                                                │
└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Запрос занял 0.27 миллисекунды – в сорок раз ниже! Разумеется, от запуска к
запуску эта цифра плавает: выдает значения от 0.1 до 0.4. Разница на порядок,
однако, сохраняется. Это подтверждает тезис о том, что поиск по составному
индексу быстрее, чем два промежуточных поиска с объединением результатов.&lt;/p&gt;

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

&lt;p&gt;Если взглянуть на индекс по паре [статус, пользователь], заметно
следующее. Чтобы показать пользователю, над какими заявками ему работать, мы
указываем статус active. Другие статусы нас не интересуют. Раз так, можно
создать индекс по полю assigned_to, но так, чтобы он охватывал только активные
заявки. Вот как это сделать:&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_assigned_to_active&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;btree&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;'assigned_to'&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;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;'status'&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'active'&lt;/span&gt;&lt;span class=&quot;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;where&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;┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│                                                                       QUERY PLAN                                                                        │
├─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ Index Scan using idx_applications_status_assigned_to on applications  (cost=0.42..185.65 rows=51 width=1915) (actual time=0.139..0.223 rows=45 loops=1) │
│   Index Cond: (((doc -&amp;gt;&amp;gt; 'status'::text) = 'active'::text) AND ((doc -&amp;gt;&amp;gt; 'assigned_to'::text) = 'user_999@test.com'::text))                             │
│ Planning Time: 1.733 ms                                                                                                                                 │
│ Execution Time: 0.270 ms                                                                                                                                │
└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

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

&lt;p&gt;Предположим, заявка на кредит может быть разных типов: обычной, когда заемщиком
выступает организация, и государственной, когда деньги занимает целая
страна. Пусть это будет поле &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;credit_type&lt;/code&gt; со значениями &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;org&lt;/code&gt; и
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;country&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;generate_credit_type&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;plpgsql&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;n&quot;&gt;func&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;$&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;declare&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;r&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;float4&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;begin&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;r&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;random&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;case&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;when&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;r&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;85&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;then&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'org'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;  &lt;span class=&quot;c1&quot;&gt;-- 85%&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;else&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'country'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;            &lt;span class=&quot;c1&quot;&gt;-- 15%&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;case&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;n&quot;&gt;func&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;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;'credit_type'&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;n&quot;&gt;generate_credit_type&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;Требования таковы, что рядовые сотрудники не должны видеть заявки от государств,
потому что их рассматривает особое звено. Поэтому в запрос, который выводит
активные заявки сотрудника, придется добавить условие, что тип не равен
“country”:&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;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;'credit_type'&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;'country'&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;assigned_to&lt;/code&gt;, покрывающий только
активные заявки с типом &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;'org'&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;language-sql highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;create&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;index&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;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_active_org_assigned_to&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;btree&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;'assigned_to'&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;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;'status'&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'active'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&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;'credit_type'&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;'org'&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;explain&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;analyze&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;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;'status'&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;'active'&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;'credit_type'&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;'org'&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;'assigned_to'&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;'user_999@test.com'&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;(doc-&amp;gt;&amp;gt;'status') = 'active'&lt;/code&gt; и &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;(doc-&amp;gt;&amp;gt;'status') = 'active'&lt;/code&gt;
база определит, что конкретно этому запросу подходит индекс
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;idx_applications_active_org_assigned_to&lt;/code&gt;. Он хранит не всю тройку полей, а
только пользователей. Такой индекс компактен, и поиск по нему быстрее. При этом
никакой фильтрации по статусу и типу заявки не было – мы просто взяли нужный
индекс.&lt;/p&gt;

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

&lt;p&gt;Проектируя составной индекс, располагайте компоненты так, чтобы покрыть как
можно больше сценариев. Если вы фильтруете записи по полям A и B и создали
индекс (A, B), это позволит фильтровать в том числе по A. Если же индекс
объявлен как (B, A), то условию по A понадобится отдельный индекс. В этом случае
проще удалить (B, A) и создать (A, B).&lt;/p&gt;

&lt;h2 id=&quot;поиск-по-шаблону&quot;&gt;Поиск по шаблону&lt;/h2&gt;

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

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

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

&lt;ul&gt;
  &lt;li&gt;подчеркивание (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;_&lt;/code&gt;) означает строго один любой символ;&lt;/li&gt;
  &lt;li&gt;процент (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;%&lt;/code&gt;) означает ноль и более любых символов.&lt;/li&gt;
&lt;/ul&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;s1&quot;&gt;'123456789'&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;/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;s1&quot;&gt;'123456789'&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;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Вернет истину, потому что процент поглощает символы 4, 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;select&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'1a3'&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;like&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'1_3'&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;Вернет истину, потому что символ “a” удовлетворяет подчеркиванию (любому
символу).&lt;/p&gt;

&lt;p&gt;Оператор ilike (insensitive like) действует аналогично, но без учета регистра. С
цифрами этого не заметно, поэтому приведем пример с электронной почтой. Ниже мы
проверяем, какие адреса на домене &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;gmail.com&lt;/code&gt; содержат имя john:&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;email&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;email&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;ilike&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'%john%@gmail.com'&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;matches&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;s1&quot;&gt;'JOHN_SMITH@Gmail.com'&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;s1&quot;&gt;'Adam.Smith@GMAIL.com'&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;s1&quot;&gt;'John+Anderson@gmail.com'&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;s1&quot;&gt;'johnson@gmail.com'&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;email&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;┌─────────────────────────┬─────────┐
│          email          │ matches │
├─────────────────────────┼─────────┤
│ JOHN_SMITH@Gmail.com    │ t       │
│ Adam.Smith@GMAIL.com    │ f       │
│ John+Anderson@gmail.com │ t       │
│ johnson@gmail.com       │ t       │
└─────────────────────────┴─────────┘
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Возвращаемся к нашим заявкам. Предположим, о номере заявки мы помним только то,
что в нем была комбинация 12345. Чтобы найти все варианты, выполним запрос:&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;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;like&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;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                  │
├──────────────────────────────────────┤
│ 00000000-0000-0000-0000-000000012345 │
│ 00000000-0000-0000-0000-000000112345 │
│ 00000000-0000-0000-0000-000000123450 │
│ 00000000-0000-0000-0000-000000123451 │
│ 00000000-0000-0000-0000-000000123452 │
│ 00000000-0000-0000-0000-000000123453 │
│ 00000000-0000-0000-0000-000000123454 │
│ 00000000-0000-0000-0000-000000123455 │
&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;pg_trgm&lt;/code&gt;; оно встроено в Postgres и считается
доверенным. Последнее означает, что его может включить обычный пользователь, а
не только администратор. Сочетание trgm означает trigram – триграмма, часть
слова из трех букв. Расширение разбивает текст на триграммы и использует их в
различных операциях. Так, слово hello становится массивом триграмм как в примере
ниже:&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;err&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;&quot;  h&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;&quot; he&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ell&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;hel&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;llo&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;&quot;lo &quot;&lt;/span&gt;&lt;span class=&quot;err&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;create extension&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;n&quot;&gt;extension&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;pg_trgm&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_id_trgm&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;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;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;Обратите внимание, что тип индекса – не btree, а GIN. Аббревиатура означает
Generalized Inverted Index – обобщенный инвертированный индекс. Этот тип
применяют в случаях, когда индексируемое значение состоит из элементов. Частный
случай GIN – поиск в массивах и триграммах. Переменная &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;gin_trgm_ops&lt;/code&gt; задает
параметры, специфичные для триграмм.&lt;/p&gt;

&lt;p&gt;Кроме GIN, индекс может быть объявлен с типом GiST, что означает Generalized
Search Tree (обобщенное дерево поиска). У каждого из этих типов преимущества и
недостатки; обобщая, можно сказать, что GIN производительней в чтении, но
занимает больше места. GiST, напротив, компактней, но медленней в плане
доступа. Также GiST опирается на хэширование элементов, поэтому для решения
коллизий выражение индекса может вычисляться дважды.&lt;/p&gt;

&lt;p&gt;В данной книге мы не будем сравнивать производительность GIN и GiST. Для
триграмм документация Postgres &lt;a href=&quot;https://postgrespro.ru/docs/postgrespro/18/pgtrgm?lang=ru&quot;&gt;рекомендует&lt;/a&gt; использовать тип GIN, что
мы и сделаем. Предлагаем читателю создать индексы обоих типов и замерить
скорость самостоятельно.&lt;/p&gt;

&lt;p&gt;Убедимся, что поиск по частичному номеру попадает в индекс. Повторим недавний
запрос:&lt;/p&gt;

&lt;div class=&quot;language-sql highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;explain&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;analyze&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;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;like&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;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;┌───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│                                                                      QUERY PLAN                                                                       │
├───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ Limit  (cost=32.77..428.27 rows=100 width=16) (actual time=0.629..0.732 rows=20 loops=1)                                                              │
│   -&amp;gt;  Bitmap Heap Scan on applications  (cost=32.77..428.27 rows=100 width=16) (actual time=0.628..0.729 rows=20 loops=1)                             │
│         Recheck Cond: ((doc -&amp;gt;&amp;gt; 'application_id'::text) ~~ '%12345%'::text)                                                                           │
│         Heap Blocks: exact=13                                                                                                                         │
│         -&amp;gt;  Bitmap Index Scan on idx_applications_application_id_trgm  (cost=0.00..32.75 rows=100 width=0) (actual time=0.601..0.601 rows=20 loops=1) │
│               Index Cond: ((doc -&amp;gt;&amp;gt; 'application_id'::text) ~~ '%12345%'::text)                                                                       │
│ Planning Time: 0.646 ms                                                                                                                               │
│ Execution Time: 0.765 ms                                                                                                                              │
└───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Выражение Bitmap Index Scan присутствует, время выполнения – 0.7
миллисекунд. Индекс работает.&lt;/p&gt;

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

&lt;div class=&quot;language-sql highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;explain&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;analyze&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;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;like&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'%99%'&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;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;┌────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│                                                       QUERY PLAN                                                       │
├────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ Limit  (cost=0.00..874.50 rows=100 width=16) (actual time=0.096..5.397 rows=100 loops=1)                               │
│   -&amp;gt;  Seq Scan on applications  (cost=0.00..265000.00 rows=30303 width=16) (actual time=0.095..5.387 rows=100 loops=1) │
│         Filter: ((doc -&amp;gt;&amp;gt; 'application_id'::text) ~~ '%99%'::text)                                                     │
│         Rows Removed by Filter: 4378                                                                                   │
│ Planning Time: 0.189 ms                                                                                                │
│ Execution Time: 5.424 ms                                                                                               │
└────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
&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;show_trgm&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;show_trgm&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;'#alpha!, ?beta=, ;gamma^'&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;┌─────────────────────────────────────────────────────────────────────────────────────────┐
│                                        show_trgm                                        │
├─────────────────────────────────────────────────────────────────────────────────────────┤
│ {&quot;  a&quot;,&quot;  b&quot;,&quot;  g&quot;,&quot; al&quot;,&quot; be&quot;,&quot; ga&quot;,alp,amm,bet,eta,gam,&quot;ha &quot;,lph,&quot;ma &quot;,mma,pha,&quot;ta &quot;} │
└─────────────────────────────────────────────────────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

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

&lt;p&gt;Если шаблон передал пользователь, проверяйте символы процента и
подчеркивания. Операторы (i)like трактуют их особым образом, что может привести
к тому, что запрос выдал лишние записи. Если пользователь имел в виду именно
подчеркивание или процент, экранируйте их – предварите обратной косой
чертой. Например, чтобы проверить текст на вхождение строки “50%”, передайте
следующий шаблон:&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;s1&quot;&gt;'more than 50% in total'&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;ilike&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'%50&lt;/span&gt;&lt;span class=&quot;se&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;c1&quot;&gt;-- t&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;Замечания по договору 12352/AB/04
&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;*по договору 12352/??/??
&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;organization.short_name&lt;/code&gt;,
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;assigned_to&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;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;Каждое поле формы связано с атрибутом документа. Если клиент заполнил форму как
в примере выше и нажал ввод, на сервер уйдет следующий JSON:&lt;/p&gt;

&lt;div class=&quot;language-json highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;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;s2&quot;&gt;&quot;12345&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;org_name&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;Acme&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;created_by&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;Maria&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;comment&quot;&lt;/span&gt;&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;reconciliation&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;Предполагается, что поля объединены логикой AND, и каждое из них проверяется на
вхождение. Сервер выполнит следующий запрос:&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;'%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;Мы не знаем заранее, сколько получим критериев поиска. Понадобится библиотека,
чтобы построить SQL без риска инъекций со стороны клиента. Проверьте, что клиент
отправил хотя бы один критерий поиска. Иначе мы получим либо неверный синтаксис
(пустой WHERE), либо запрос без WHERE, который читает всю таблицу.&lt;/p&gt;

&lt;p&gt;Есть и другой подход к поиску: предоставить клиентам поле ввода “а-ля Гугл”. Его
строка проверяет сразу несколько атрибутов. Это современный подход, и сегодня
его воспринимают как должное. Например, если клиент ввел 12345, мы вернем
документы, где эта строка встречается в номере, названии клиента, комментарии и
других полях.&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;Рассмотрим, как сделать подобный поиск. Наивное решение в том, чтобы составить
набор условий вида:&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;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;k&quot;&gt;ilike&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'%12345%'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;и объединить их оператором OR: либо это поле, либо то, другое и так далее. Для
каждого поля создайте триграммный индекс по образцу:&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_trgm_path_to_field&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;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;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;Предположим теперь, клиент ввел в поле поиска 12345 и нажал ввод. Сервер
построит запрос:&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;'%12345%'&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;'%12345%'&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;'%12345%'&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;'%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;План этого запроса велик и не поместится на странице книги. Описать его можно
так: четыре узла Bitmap Index Scan, каждый из которых сканирует свой индекс и
строит битовую карту. Четыре карты объединяются методом BitmapOr. По ней база
обходит таблицу applications и выбирает 20 записей. На ноутбуке автора запрос
выполнялся 3 миллисекунды.&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;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;field1&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;ilike&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'pattern'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&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;field2&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;ilike&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'pattern'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&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;/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;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;field1&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;o&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;field1&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;ilike&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'pattern'&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;Postgres предлагает несколько способов это сделать, в том числе:&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;Подготовим выражение, которое объединяет атрибуты документа через
пробел. Наивное решение:&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;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;o&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;s1&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;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;o&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;s1&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;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;o&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;s1&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;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;Недостаток в том, что если в документе нет хотя бы одного поля, конструкция
“занулится” – вернет NULL. Для несуществующего поля оператор &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;#&amp;gt;&amp;gt;&lt;/code&gt; производит
NULL, его соединение со строкой тоже даст NULL и так далее. Это легко проверить:&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;s1&quot;&gt;'foo'&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;null&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'bar'&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;result&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;┌────────┐
│ result │
├────────┤
│ &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;#&amp;gt;&amp;gt;&lt;/code&gt; формой &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;coalesce&lt;/code&gt; с пустой строкой — умолчанием для
NULL:&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;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;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;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;s1&quot;&gt;' '&lt;/span&gt;
    &lt;span class=&quot;o&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;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;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;s1&quot;&gt;' '&lt;/span&gt;
    &lt;span class=&quot;o&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;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;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;s1&quot;&gt;' '&lt;/span&gt;
    &lt;span class=&quot;o&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;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;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;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Это выражение устойчиво к NULL, но выглядит слишком шумно. Более удачный вариант
– взять функцию concat_ws. Она принимает разделитель и произвольные аргументы,
которые приводятся к строке; значения NULL при этом игнорируются:&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;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;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;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;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-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;ERROR: functions in index expression must be marked IMMUTABLE
&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;immutable&lt;/code&gt;). Для concat_ws это требование не выполняется. Почему?
Разве соединение строк пробелом носит побочный эффект?&lt;/p&gt;

&lt;p&gt;Дело в том, что &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;concat_ws&lt;/code&gt; работает в два этапа. &lt;strong&gt;Второй&lt;/strong&gt; из них – соединение
текстовых фрагментов разделителем; это действие, конечно, иммутабельно. Первый
этап приводит все значения к тексту и передает второму. Типы вроде дат и времени
зависят от локали и глобальных настроек, из-за чего приведение их к тексту не
считается иммутабельным.&lt;/p&gt;

&lt;p&gt;Функция, выполняющая второй этап, называется &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;text_concat_ws&lt;/code&gt;. Она написана на
языке Си поэтому недоступна без ухищрений. Postgres позволяет создать
функцию-обертку, которая сводится к вызову внутренней (internal) функции. В
примере ниже функция &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;immutable_concat_ws&lt;/code&gt; не имеет тела, а только передает
управление функции &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;text_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;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;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Объявлять подобные обертки может только администратор, поскольку функции на Си,
как правило, небезопасны.&lt;/p&gt;

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

&lt;pre&gt;&lt;code class=&quot;language-sq&quot;&gt;create index if not exists
idx_applications_application_trgm_pattern
on applications using gin
((immutable_concat_ws(
    ' ',
    (doc #&amp;gt;&amp;gt; '{application_id}'),
    (doc #&amp;gt;&amp;gt; '{organization,short_name}'),
    (doc #&amp;gt;&amp;gt; '{created_by,name}'),
    (doc #&amp;gt;&amp;gt; '{comment}')
)) gin_trgm_ops);
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Проверим поиск документов с новым индексом. Выполним запрос с шаблоном
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;%12345%&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;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;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;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;В плане окажется один узел Bitmap Index Scan вместо четырех. Время выполнения –
0.6 миллисекунды. Эксперимент подтвердил тезис, что один сложный индекс,
составленный под конкретные требования, лучше нескольких.&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-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;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;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;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;from&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;applications&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;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                  │               application_search_pattern               │
├──────────────────────────────────────┼────────────────────────────────────────────────────────┤
│ 00000000-0000-0000-0000-000000010049 │ 10049 Organization 49 User 0049 Comment number #10049  │
│ 00000000-0000-0000-0000-000000010050 │ 10050 Organization 50 User 0050 Comment number #10050  │
│ 00000000-0000-0000-0000-000000010051 │ 10051 Organization 51 User 0051 Comment number #10051  │
│ 00000000-0000-0000-0000-000000010052 │ 10052 Organization 52 User 0052 Comment number #10052  │
&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;immutable_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;drop&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;exists&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;idx_applications_application_trgm_pattern&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;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;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;application_search_pattern(doc)&lt;/code&gt; легче
запомнить. Если мы случайно ошиблись в названии функции, то получим ошибку, а не
молчаливый full seq scan.&lt;/p&gt;

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

&lt;h2 id=&quot;коротко-о-ранжировании&quot;&gt;Коротко о ранжировании&lt;/h2&gt;

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

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

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

&lt;ul&gt;
  &lt;li&gt;документ с номером 123;&lt;/li&gt;
  &lt;li&gt;документ, код организации которой равен 123;&lt;/li&gt;
  &lt;li&gt;документы, номера которых содержат 123;&lt;/li&gt;
  &lt;li&gt;документы, коды организаций которых содержат 123.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;h2 id=&quot;регулярные-выражения&quot;&gt;Регулярные выражения&lt;/h2&gt;

&lt;p&gt;Прежде чем закончить с триграммами, расскажем о еще одной их возможности.&lt;/p&gt;

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

&lt;p&gt;Вот как выглядит новый запрос. Вместо (i)like мы использовали оператор &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-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;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;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;o&quot;&gt;~&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'Organization 313.+?#22313'&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;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                  │                        application_search_pattern                        │
├──────────────────────────────────────┼──────────────────────────────────────────────────────────────────────────┤
│ 00000000-0000-0000-0000-000000022313 │ 22313 Organization 313 User 0313 Comment number #22313 user_313@test.com │
└──────────────────────────────────────┴──────────────────────────────────────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;План подтверждает, что мы попали в индекс: в нем присутствует выражение Bitmap
Index Scan. Он не такой уж быстрый: время выполнения – почти 70 миллисекунд.&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;┌───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│                                                                          QUERY PLAN                                                                           │
├───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ Limit  (cost=444.78..890.03 rows=100 width=48) (actual time=69.670..69.904 rows=1 loops=1)                                                                    │
│   -&amp;gt;  Bitmap Heap Scan on applications  (cost=444.78..890.03 rows=100 width=48) (actual time=69.668..69.903 rows=1 loops=1)                                   │
│         Recheck Cond: (application_search_pattern(doc) ~ 'Organization 313.+?#22313'::text)                                                                   │
│         Rows Removed by Index Recheck: 19                                                                                                                     │
│         Heap Blocks: exact=13                                                                                                                                 │
│         -&amp;gt;  Bitmap Index Scan on idx_applications_application_trgm_pattern  (cost=0.00..444.75 rows=100 width=0) (actual time=69.503..69.503 rows=20 loops=1) │
│               Index Cond: (application_search_pattern(doc) ~ 'Organization 313.+?#22313'::text)                                                               │
│ Planning Time: 1.735 ms                                                                                                                                       │
│ Execution Time: 69.949 ms                                                                                                                                     │
└───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;О регулярных выражениях скажем следующее. Во-первых, это самобытный язык,
который выходит за рамки книги. Регулярные выражения описаны в различных статьях
и руководствах, поэтому излагать их здесь будет избыточным. Предлагаем читателю
ознакомиться с общими положениями &lt;a href=&quot;https://ru.wikipedia.org/wiki/%D0%A0%D0%B5%D0%B3%D1%83%D0%BB%D1%8F%D1%80%D0%BD%D1%8B%D0%B5_%D0%B2%D1%8B%D1%80%D0%B0%D0%B6%D0%B5%D0%BD%D0%B8%D1%8F&quot;&gt;в Википедии&lt;/a&gt;, а также &lt;a href=&quot;https://postgrespro.ru/docs/postgresql/current/functions-matching&quot;&gt;документацией
Postgres&lt;/a&gt;, где рассмотрены особенности их реализации.&lt;/p&gt;

&lt;p&gt;Во-вторых, несмотря на всю мощь, регулярные выражения не столь полезны в
поиске. Маловероятно, что клиенты настолько технически подкованы, что свободно
ими пользуются. Если сервер принимает регулярные выражения, ими можно
злоупотребить: составить такое выражение, которое выберет все записи из
таблицы. Другая крайность – построить очень сложную конструкцию, которая
нагрузит процессор. Примером служит регулярное выражение &lt;a href=&quot;https://pdw.ex-parrot.com/Mail-RFC822-Address.html&quot;&gt;электронной
почты&lt;/a&gt; из стандарта RFC822: в распечатанном виде оно занимает лист А4.&lt;/p&gt;

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

&lt;h2 id=&quot;индексирование-документа-целиком&quot;&gt;Индексирование документа целиком&lt;/h2&gt;

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

&lt;p&gt;Одно из полей заявки называется “departments”. В нем структура из двух уровней:
отделы (департаменты), связанные с заявкой, а ниже — сотрудники отделов, которые
работали над ней. Приведем фрагмент данных:&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;nl&quot;&gt;&quot;departments&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;id&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;224899ae-fac7-4555-8a94-f41f3c9ed634&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;code&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;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;name&quot;&lt;/span&gt;&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 Department&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;users&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
            &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;id&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;3f798e66-43a3-4d78-b5f9-3c3740b6676b&quot;&lt;/span&gt;&lt;span class=&quot;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;mironov.p@acme.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;name&quot;&lt;/span&gt;&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;Petr Mironov&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;role&quot;&lt;/span&gt;&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;manager&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
            &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
            &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;id&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;9ecef728-a3bb-4a1d-85d7-51dff2401c0b&quot;&lt;/span&gt;&lt;span class=&quot;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;smith.k@acme.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;name&quot;&lt;/span&gt;&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;Karol Smith&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;role&quot;&lt;/span&gt;&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;analyst&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
            &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;id&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;7513991b-9951-4bce-ad4a-470ff2d3febf&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;code&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;analytics&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;name&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;Analytics Department&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;users&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
            &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;id&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;361a08a5-3991-4b2a-9d49-8e4cf77b876b&quot;&lt;/span&gt;&lt;span class=&quot;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;smirnov.i@acme.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;name&quot;&lt;/span&gt;&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;Igor Smirnov&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;role&quot;&lt;/span&gt;&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;reader&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
            &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
            &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;id&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;11da81b9-b125-4a7e-9965-958cc5593a3e&quot;&lt;/span&gt;&lt;span class=&quot;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;jackson.j@acme.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;name&quot;&lt;/span&gt;&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 Jackson&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
                &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;role&quot;&lt;/span&gt;&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;support&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
            &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

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

&lt;p&gt;Предположим, нам поставили задание: найти заявки, над которыми работал
пользователь с определенной ролью. Скажем, в примере выше это Karol Smith в роли
аналитика. Нужно найти все заявки с подобным условием.&lt;/p&gt;

&lt;p&gt;Задание интересно тем, индекс btree нам не поможет. Путь к пользователю Karol
Smith следующий:&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;departments,0,users,1
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

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

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

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;departments,0,users,0
departments,0,users,1
departments,1,users,0
departments,1,users,1
...
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

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

&lt;p&gt;Чтобы решить проблему, построим индекс GIN для всего поля doc. Вот как он
выглядит:&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_doc_gin_jsonb_ops&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;doc&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;jsonb_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;Когда индекс GIN применяют к jsonb, можно указать опции. По умолчанию это
переменная &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;jsonb_ops&lt;/code&gt;, и выше мы задали ее явно. Postgres предлагает второй
набор названием &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;jsonb_path_ops&lt;/code&gt;; в чем его преимущество мы узнаем позже.&lt;/p&gt;

&lt;p&gt;Индекс GIN устроен сложнее btree. Он обходит документ и строит множество пар
путь -&amp;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;users&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[{&lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;name&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;Ivan&quot;&lt;/span&gt;&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;name&quot;&lt;/span&gt;&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;Huan&quot;&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-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;users.0.name = Ivan
users.1.name = Huan
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

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

&lt;p&gt;Новый индекс поддерживает несколько операторов, которых не было в
btree. Например, с помощью знака вопроса (&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;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;doc&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'application_id'&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;p&quot;&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

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

&lt;div class=&quot;language-sql highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;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;'__migrate__'&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_json&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;where&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;p&quot;&gt;...&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;where&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;s1&quot;&gt;'__migrate__'&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;?|&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;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;doc&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;?&amp;amp;&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'{application_id,status,amounts}'&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;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;@&amp;gt;&lt;/code&gt; (правое мороженое) – рекурсивное вхождение одного
документа в другой. Напомним, мы изучили его во второй главе, когда только
знакомились с JSON. Чтобы найти в недрах departments пользователя &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;user_123&lt;/code&gt; в
роли “lead”, составим следующий запрос:&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;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;s1&quot;&gt;'departments'&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;applications&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;where&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;doc&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;@&amp;gt;&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;$${&lt;/span&gt;
  &lt;span class=&quot;nv&quot;&gt;&quot;departments&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;err&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;&quot;users&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;err&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nv&quot;&gt;&quot;email&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;&quot;user_123@test.com&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;nv&quot;&gt;&quot;role&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;&quot;lead&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;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;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;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@&amp;gt;&lt;/code&gt; является минимальным подмножеством заявки. У обеих
сторон есть поле “departments”, и далее их значения проверяются
рекурсивно. Алгоритм переходит к массивам, элементам, объектам с полями “users”
и так далее.&lt;/p&gt;

&lt;p&gt;План запроса показывает, что мы попали в индекс; время выполнения составило 28
миллисекунд.&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;┌────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│                                                                         QUERY PLAN                                                                         │
├────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ Limit  (cost=189.50..524.92 rows=100 width=48) (actual time=26.982..28.416 rows=100 loops=1)                                                               │
│   -&amp;gt;  Bitmap Heap Scan on applications  (cost=189.50..33731.03 rows=10000 width=48) (actual time=26.980..28.406 rows=100 loops=1)                          │
│         Recheck Cond: (doc @&amp;gt; '{&quot;departments&quot;: [{&quot;users&quot;: [{&quot;email&quot;: &quot;user_123@test.com&quot;}]}]}'::jsonb)                                                     │
│         Heap Blocks: exact=100                                                                                                                             │
│         -&amp;gt;  Bitmap Index Scan on idx_applications_doc_gin_jsonb_ops  (cost=0.00..187.00 rows=10000 width=0) (actual time=26.623..26.623 rows=3000 loops=1) │
│               Index Cond: (doc @&amp;gt; '{&quot;departments&quot;: [{&quot;users&quot;: [{&quot;email&quot;: &quot;user_123@test.com&quot;}]}]}'::jsonb)                                                 │
│ Planning Time: 91.763 ms                                                                                                                                   │
│ Execution Time: 28.460 ms                                                                                                                                  │
└────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Индекс GIN и “мороженное” решают проблему массивов: можно найти элемент, зная
его значение, а не позицию. Это выгодно отличает GIN от btree, который требует
явного пути к значению и поэтому несовместим с массивами.&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;pg_size_pretty&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;pg_total_relation_size&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;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;app_table_size&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;┌────────────────┐
│ app_table_size │
├────────────────┤
│ 4877 MB        │
└────────────────┘
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&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;pg_size_pretty&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;pg_total_relation_size&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;'idx_applications_created_at'&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;created_at_index_size&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;┌───────────────────────┐
│ created_at_index_size │
├───────────────────────┤
│ 50 MB                 │
└───────────────────────┘
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Хотя таблица и занимает почти пять гигабайт, индекс bree весьма компактный:
всего 50 мегабайт, разница два порядка. Если же выполнить запрос для индекса
GIN, результат удивит: полтора гигабайта!&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;pg_size_pretty&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;pg_total_relation_size&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;'idx_applications_doc_gin_jsonb_ops'&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;gin_json_index_size&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;┌─────────────────────┐
│ gin_json_index_size │
├─────────────────────┤
│ 1492 MB             │
└─────────────────────┘
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

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

&lt;p&gt;Одно из решений в том, чтобы индексировать не весь документ, а его
подмножество. Поскольку пользователь находится в недрах поля “departments”,
проиндексируем только это поле:&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_doc_departments_gin_jsonb_ops&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;doc&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;'departments'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;])&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;jsonb_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;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@&amp;gt;&lt;/code&gt; находится не документ, а та часть, что мы
индексируем. В свою очередь документу справа не понадобится поле “departments”:
это будет массив.&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;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;s1&quot;&gt;'departments'&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;applications&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;where&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;'departments'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;@&amp;gt;&lt;/span&gt; &lt;span class=&quot;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;users&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;err&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;nv&quot;&gt;&quot;email&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;&quot;user_123@test.com&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;nv&quot;&gt;&quot;role&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;&quot;lead&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;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;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;Запрос выполнится быстрее: 22 миллисекунды против 28.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;SELECT&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;pg_size_pretty&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;pg_total_relation_size&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;'idx_applications_doc_departments_gin_jsonb_ops'&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;gin_json_deps_index_size&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;┌──────────────────────────┐
│ gin_json_deps_index_size │
├──────────────────────────┤
│ 237 MB                   │
└──────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

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

&lt;p&gt;Оператор &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@&amp;gt;&lt;/code&gt; возвращает истину или ложь, поэтому не может быть использован для
сортировки. Если выборка сортируется, Postgres оценивает, какой из двух
вариантов предпочесть. Первый — обойти записи по индексу сортировки и
отфильтровать их при помощи &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@&amp;gt;&lt;/code&gt;. Индекс GIN при этом использоваться не
будет. Второй вариант — обойти индекс GIN, получить строки и отсортировать их в
памяти. Выбор зависит от того, насколько точен индекс GIN. Если в запрос
одновременно содержит сортировку и оператор &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@&amp;gt;&lt;/code&gt;, обязательно проверьте план.&lt;/p&gt;

&lt;p&gt;Индекс GIN поддерживает еще два оператора: поиск по пути с предикатом &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;. Обе функции крайне мощны и представляют собой вещь в
себе. Чтобы познакомиться с ними, нам понадобится язык выражений JSON Path. Это
весьма обширная тема, и мы придержим ее для отдельной главы.&lt;/p&gt;

&lt;p&gt;В числе прочего мы упомянули технику под названием “вычисляемые столбцы”. Их
можно использовать как замену триггерам, а также в других нетривиальных
ситуациях. Как и в случае с JSON Path, тема требует вводных данных, поэтому мы
рассмотрим ее в следующей главе.&lt;/p&gt;
</description>
        <pubDate>Mon, 25 May 2026 00:00:00 +0000</pubDate>
        <link>https://grishaev.me/pg-book-json-ch04/</link>
        <guid isPermaLink="true">https://grishaev.me/pg-book-json-ch04/</guid>
        
        <category>postgres</category>
        
        <category>json</category>
        
        <category>book</category>
        
        <category>sql</category>
        
        
      </item>
    
      <item>
        <title>Вайбкодинг (2)</title>
        <description>&lt;p&gt;Говорит один разработчик: иногда меня просят сделать ревью, а я в нем ничего не
понимаю. Тогда я копирую ветку и прошу модель сделать ревью. Она хвалит хорошие
места и критикует плохие. Я копирую выхлоп и вставляю в оригинал.&lt;/p&gt;

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

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

&lt;p&gt;При таком раскладе зачем нам вообще ревьюер с моделью? Сделайте кнопку
“AI-review” или встройте модель в CI — по крайней мере будет ясно, откуда
дровишки.&lt;/p&gt;

&lt;p&gt;Помнится, в Южном Парке была забавная серия. Учитель дает детям сочинение; дети
никогда его не пишут и получают двойки. Зато теперь у всех — текст А4 от чата
джи-пи-ти. Учитель садиться проверять и не может: на 30 сочинений нет никаких
сил. Тогда он достает телефон и сканирует сочинения. ГПТ пишет рецензию и ставит
оценку. Круг замкнулся, все довольны.&lt;/p&gt;

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

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

&lt;p&gt;Пугая кого-то заменой или устранением, подумайте — не придется ли вам пройти
раньше своих жертв.&lt;/p&gt;
</description>
        <pubDate>Sat, 16 May 2026 00:00:00 +0000</pubDate>
        <link>https://grishaev.me/vibe-coding-2/</link>
        <guid isPermaLink="true">https://grishaev.me/vibe-coding-2/</guid>
        
        <category>programming</category>
        
        <category>vibecoding</category>
        
        
      </item>
    
      <item>
        <title>Вайбкодинг (1)</title>
        <description>&lt;p&gt;Вирус под названием “вайбкодинг” дошел и до нас. Начиналось все буднично: файлы
AGENTS.md в каждом репозитории; бесконечные обсуждения моделей в чате. У иных
разработчиков резко изменился стиль: годами они не писали ни одного комментария,
а сегодня у каждой функции докстринг; файлы разбиты на секции вида:&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;;;;;;;;;;;;;;;;;
;; public API
;;;;;;;;;;;;;;;;

;;;;;;;;;;;;;;;;
;; dev section
;;;;;;;;;;;;;;;;
&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;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;Смотрю код другого коллеги: это Питон-скрипт для перекладывания данных между
таблицами. Задаю простой вопрос – скрипт работает? Отвечает, что да. В ту же
минуту я вижу ошибку в SQL, из-за которой ничего работать не будет. Ради
интереса запускаю скрипт на тестовой базе. Происходит вот что:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;выскакивает неизвестная ошибка (до SQL даже не дошло)&lt;/li&gt;
  &lt;li&gt;из-за try/catch управление переходит в блок catch&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;При этом низкое качество кода коррелирует с дороговизной оборудования. Всем
известно, как выросла цена на память: в интернете все шутки только об
этом. Однако теперь коллеги обсуждают покупку Mac Studio со встроенной памятью
128 гигабайт. Стоит он от 5 до 7 тысяч долларов. Купить такую дорогую вещь,
чтобы генерить на ней неработающие скрипты?&lt;/p&gt;

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

&lt;p&gt;Пока что вайбкодинг напоминает вейпы. Не смотря на громкие обещания, все еще нет
данных за долгие периоды, и что особенно важно – последствий. А подозрения есть,
и очень нехорошие.&lt;/p&gt;
</description>
        <pubDate>Sat, 16 May 2026 00:00:00 +0000</pubDate>
        <link>https://grishaev.me/vibe-coding-1/</link>
        <guid isPermaLink="true">https://grishaev.me/vibe-coding-1/</guid>
        
        <category>programming</category>
        
        <category>vibecoding</category>
        
        
      </item>
    
      <item>
        <title>О домах</title>
        <description>&lt;p&gt;&lt;a href=&quot;/nyc-skyscraper-1/&quot;&gt;А какие могут быть выводы?&lt;/a&gt; И так все понятно.&lt;/p&gt;

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

&lt;p&gt;Дома не должны подпирать стратосферу. Лучшая высота здания – 5-10 этажей и не
больше. Такой дом не давит размером, не выделяется на фоне городского пейзажа,
не отбрасывает тень. Последнее, кстати, проблема городов вроде Чикаго: на улицах
между высоких зданий почти не бывает света. Это сырость, лужи и банальная
проступность.&lt;/p&gt;

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

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

&lt;p&gt;Мне как человеку искренне непонятно желание жить как можно выше. Зачем? Какая
цель? Чтобы ехать в лифте десять минут? Переживать за детей и домашних животных,
чтобы не выпали в окно? Если экстраполировать, то будет ли жилье на 450-м этаже
лучше, чем на 200-м? А на тысячном?&lt;/p&gt;

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

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

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

&lt;p&gt;Когда был в Москве, наблюдал жилые небоскребы по образу того, что в прошлой
заметке. Циклопической высоты, этажей этак 70. При этом сквозь открытые окна
видны черновые потолки. Квартиры выше 20 этажа не отделаны. Бетонные коробки
напоминают пчелиные соты. Вид у домов зловещий.&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;Вообще, важный критерий счастья – это когда в любое место ты попадаешь пешком. В
школу, в поликлинику, на работу, не важно… возможность добраться до цели
пешком за 15 минут – бесценна.&lt;/p&gt;

&lt;p&gt;Хорошо, когда эта возможность распространяется и на собственную квартиру. Не
всегда это возможно, но если к вам это относится, возможно, вы самый счастливый
человек! Хоть и не знаете этого.&lt;/p&gt;
</description>
        <pubDate>Sat, 16 May 2026 00:00:00 +0000</pubDate>
        <link>https://grishaev.me/nyc-skyscraper-2/</link>
        <guid isPermaLink="true">https://grishaev.me/nyc-skyscraper-2/</guid>
        
        <category>life</category>
        
        <category>buildings</category>
        
        <category>city</category>
        
        
      </item>
    
      <item>
        <title>Нью-Йоркский небоскреб</title>
        <description>&lt;p&gt;&lt;strong&gt;UPD: видео заблокировали, посмотреть уже нельзя.&lt;/strong&gt;&lt;/p&gt;

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

&lt;iframe width=&quot;100%&quot; height=&quot;500&quot; src=&quot;https://www.youtube.com/embed/-WEtpnKOBjs?start=&quot; frameborder=&quot;0&quot; allow=&quot;accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share&quot; referrerpolicy=&quot;strict-origin-when-cross-origin&quot; allowfullscreen=&quot;&quot;&gt;
&lt;/iframe&gt;

&lt;p&gt;В Нью-Йорке построили очень высокий и тонкий небоскреб. Выглядит как карандаш
или шприц: высота 435 метров (1428 футов), а ширина сечения сравнима с теннисным
кортом. Один этаж – одна квартира. Небоскреб предназначен для жилья, а не
офисов. Это апартаменты для богачей, и слова “лакшери”, “worldclass” так и
сыплются с экрана.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/static/aws/nyc-skyscraper-1/1.jpeg&quot; /&gt;&lt;/p&gt;

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

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

&lt;p&gt;&lt;img src=&quot;/assets/static/aws/nyc-skyscraper-1/2.jpeg&quot; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/static/aws/nyc-skyscraper-1/3.jpeg&quot; /&gt;&lt;/p&gt;

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

&lt;p&gt;&lt;img src=&quot;/assets/static/aws/nyc-skyscraper-1/5.jpeg&quot; /&gt;&lt;/p&gt;

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

&lt;p&gt;&lt;img src=&quot;/assets/static/aws/nyc-skyscraper-1/6.jpeg&quot; /&gt;&lt;/p&gt;

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

&lt;p&gt;&lt;img src=&quot;/assets/static/aws/nyc-skyscraper-1/7.jpeg&quot; /&gt;&lt;/p&gt;

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

&lt;p&gt;&lt;img src=&quot;/assets/static/aws/nyc-skyscraper-1/8.jpeg&quot; /&gt;&lt;/p&gt;

&lt;p&gt;И вот небоскреб достроили и стали заселять. Центр мира – Америка, Нью-Йорк,
Манхэттен. Вид – потрясающий, в ясную погоду простирается на километры. Ты
властелин, а мелкие твари копошатся у твоих подошв.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/static/aws/nyc-skyscraper-1/9.jpeg&quot; /&gt;&lt;/p&gt;

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

&lt;p&gt;Шума добавляет система насосов. Чтобы поднять воду на 400 метров, нужные мощные
насосные станции, которые шумят. Да, изоляция звука, все дела. Но полностью
скрыть звук от них невозможно.&lt;/p&gt;

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

&lt;p&gt;И вот здание стоит пустое. Кто-то, конечно, купил там квартиру, однако в целом
оно – призрак. Огромный гвоздь стоимостью словно из золота, который воткнули
неизвестно зачем. Очередной урок, что так делать не надо.&lt;/p&gt;

&lt;p&gt;Выводы я оставлю &lt;a href=&quot;/nyc-skyscraper-2/&quot;&gt;на следующую заметку&lt;/a&gt;.&lt;/p&gt;
</description>
        <pubDate>Sat, 16 May 2026 00:00:00 +0000</pubDate>
        <link>https://grishaev.me/nyc-skyscraper-1/</link>
        <guid isPermaLink="true">https://grishaev.me/nyc-skyscraper-1/</guid>
        
        <category>life</category>
        
        <category>nyc</category>
        
        <category>skyscraper</category>
        
        
      </item>
    
  </channel>
</rss>
