Сколько инженеров нужно, чтобы подписка работала?

Сколькоинженеровнужночтобыподпискаработала
14 Мар 6075

Вы устали от этого синтаксиса в PostgreSQL?

 ВЫБРАТЬ  jsonb_column   ->  'ключ' ИЗ Таблица;  ОБНОВИТЬ Таблица НАБОР  jsonb_column   знак равно  jsonb_set   ( 

jsonb_column

, '{"ключ"}' , " значение"');

С выделенной частью все в порядке. Но для обновлений, особенно для сложных обновлений, он может быть довольно подробным и далеко не эргономичным. Что бы вы сказали этому синтаксису вместо этого?

  ВЫБРАТЬ  jsonb_column   ИЗ Таблица;  ОБНОВИТЬ Таблица НАБОР 

jsonb_column знак равно '"значение"';

С индексированием выглядит более лаконичным и, вероятно, даже знакомым разработчикам из-за своего «питонического» стиля. Если вам больше нравится этот синтаксис, у меня для вас есть хорошие новости, недавно вышел патч

реализация этой функциональности появилась в PostgreSQL:

 совершить 751935 a3b0b8e3c 0500 ac3f 83 ab0d 18 e9a 33 bd 48 Автор: Александр Коротков Дата: Вс янв 37 31: 64: 43 2021 + 0300 Выполнение подписки для jsonb ... Автор: Дмитрий Долгов Рецензенты: Том Лейн, Артур Закиров, Павел Стехуле, Дайан М. Фэй Рецензенты: Эндрю Данстан, Чепмен Флэк, Merlin Moncure Рецензенты: Питер Геогеган, Альваро Эррера, Джим Nasby Рецензенты: Джош Беркус, Виктор Вагнер, Александр Алексеев Рецензенты: Роберт Хаас, Олег Бартунов   
Надеюсь, что в PostgreSQL 17 вы сможете его использовать. Теоретически я мог бы закончить писать на этом этапе, и это был бы самый короткий пост в блоге, который я когда-либо писал.

Но видите ли вы что-нибудь странное в упомянутом выше коммите? С точки зрения пользователя, это изменение, вероятно, не самое захватывающее, тогда почему в разработке было задействовано так много нетипичных людей? Давайте попробуем выяснить, и для этого нам нужно изучить график разработки этого патча и то, как его дизайн изменился за это время.

Изначально Олег Бартунов поделился со мной интересной идеей:

Короче говоря, через некоторое время я выложил PoC патч в хакерах:

От: Дмитрий Долгов Дата: Вт, 24 август 2020 12: 80: 57 + 935 Тема: индексирование в стиле массива jsonb Кому: PostgreSQL-development < [emailprotected] > Привет! Некоторое время назад в этом списке рассылки обсуждалось создание индексов в стиле массива для типа данных jsonb. Я думаю, что будет довольно удобно иметь такой красивый синтаксис для обновления объектов jsonb, поэтому я пытаюсь реализовать это. Я создал патч, который позволяет делать что-то вроде этого:

Первое, на что обращаешь внимание – это дата, это было ооочень давно! Чтобы создать впечатление, за это время ученые открыли гравитационные волны, подтвердили некоторые свойства бозона Хиггса, предсказанные Стандартной моделью, Магнусу Карлсену удалось дважды защитить свой титул чемпиона мира по шахматам, а AC / DC выпустили новый альбом.

Эта версия патча имела чрезвычайно простой дизайн, она просто расширяла существующую реализацию индексации массива для понимания типа данных jsonb. Но этого было достаточно, чтобы начать обсуждение, и на самом деле все важные варианты использования работали. Отзывы сообщества были в основном положительными, было ясно, что эта функция полезна. Но дизайн, конечно, нужно было улучшить, и даже были предложения сделать его настолько универсальным, чтобы любой произвольный тип данных мог использовать такую ​​функциональность.

Действительно, это была интересная идея, и на основе этого отзыва я написал новая более общая реализация. Уловка заключалась в том, чтобы расширить pg_type с новым полем typsubscript :

  Таблица "pg_catalog.pg_type" Столбец |  Тип |  Сопоставление |  Обнуляемый |  По умолчанию ---------------- + -------------- + ----------- + ----- ----- + --------- ... typrelid |  oid |  |  не нуль |  typsubscript |  regproc |  |  не нуль |  typelem |  oid |  |  не нуль |  ...  

Этот столбец содержит функцию, которая будет точкой входа для каждой реализации настраиваемого индексации. Эта функция должна создать и вернуть структуру, описывающую, как индексирование будет работать для этого конкретного типа данных:

  

/ Методы шагов выполнения, используемые для SubscriptingRef / typedef struct SubscriptExecSteps

{ / обрабатывать индексы / ExecEvalBoolSubroutine sbs_check_subscripts

;

/ получение элемента / EXE cEvalSubroutine sbs_fetch ; / присвоить элементу / ExecEvalSubroutine

sbs_assign

; / получение старого значения для присвоения / ExecEvalSubroutine sbs_fetch_old ; } SubscriptExecSteps ;

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

  

typedef

struct SubscriptingRef { / тип собственно контейнера / Oid refcontainertype ; / pg_type.typelem типа контейнера /

Oid refelemtype ; ...

/ выражения, вычисляющие индексы контейнера / Список refupperindexpr ; Список

reflowerindexpr ; ... } SubscriptingRef ;

Эти структуры взяты из последней зафиксированной версии, оригинальные были немного другими, но основывались на той же идее. Имея это под рукой, вы можете реализовать все, что необходимо.

Эта версия была довольно хороша во многих отношениях, но имела один существенный недостаток - она ​​была просто большой и довольно агрессивной с точки зрения кода. Это, конечно, вызвало нежелание кого-либо пересматривать его. Я даже пытался очистить свое чувство юмора от пыли и добавить в комментарии что-нибудь броское:

  

/ Выполните подготовку для индексации jsonb. Эта функция создает выражение, которое представляет результат извлечения одного элемента контейнера или новое значение контейнера с исходными данными, вставленными в правую часть контейнера. Если вы прочитали до этого момента и представите содержательный обзор этой серии патчей, я должен вам выпить пива на следующей PGConfEU. /

К счастью, через некоторое время патч снова проявил активность.

Одной из очевидных проблем было влияние на производительность массивов, поскольку индексирование массивов теперь проходит через тот же общий механизм. Теоретически это не должно быть проблемой, потому что единственное изменение - это дополнительный вызов функции, но все же это нужно было доказать. Оказывается, исходная реализация имела замедление для массивов примерно на 2%, и в качестве побочного примечания я хотел бы подчеркнуть один интересный момент - если вы проводите какое-либо тестирование производительности с PostgreSQL, не забудьте отключить - включить-кассету , который вы должны использовать для разработки. Интересно, что большая часть накладных расходов в этом случае возникает не из-за самих утверждений, а из-за проверок памяти, которые включаются вместе с утверждениями: [local]

  

/ Определите это для проверки ошибок выделения памяти ( набросал больше байт, чем было выделено). Прямо сейчас это определяется автоматически, если --enable-cassert или USE_VALGRIND. / # если определено (USE_ASSERT_CHECKING) || определено (USE_VALGRIND) #define MEMORY_CONTEXT_CHECKING #endif

Если вам, как и мне, достаточно любопытно, проверьте разница между PostgreSQL, скомпилированным с утверждениями и без них, вы можете увидеть, что значительная часть накладных расходов может быть связана с этим: [local]

$ perf diff no-cassert.data cassert.data ... 31. 0348% postgres [.] AllocSetCheck 16. 12% postgres [.] sentinel_ok ...

Возвращаясь к исходной теме, мне нравится сообщество PostgreSQL, потому что это всегда не так. достаточно. В этом конкретном случае оказывается, что можно немного сократить накладные расходы (уменьшив количество промежуточных вызовов, что было обнаружено и сделано Томом Лейном) и сделать его еще более универсальным! Последнее может быть достигнуто путем добавления большей гибкости к структурам, содержащим состояние подписки, так что они также могут содержать некоторые непрозрачные пользовательские данные. SubscriptingRef необходимо исправить, потому что это узел планировщика и может быть отправлен другим рабочим, поэтому единственная возможность для этого - на этапе выполнения с SubscriptingRefState :

[local]

   typedef   struct   SubscriptingRefState  

{

/ рабочее пространство для индексирующего кода, зависящего от типа / пустота

рабочая среда; / заполнено в выражении время компиляции и выполнения / int число ; bool с верхним расположением ; Дата верхний индекс ; bool upperindexnull ; ...

/ sbs_fetch_old помещает сюда старое значение / Дата prevvalue ; bool prevnull ; } SubscriptingRefState ;

Теперь вместе с другими полями, содержащими уже известную информацию, есть рабочая область , который может быть ты sed для хранения всего, что вам нужно, между различными этапами.

Конечно, все это не решает проблему размера и инвазивности. Одним из способов решения этой проблемы было разделение патча на небольшие (-ish) части, чтобы сделать все более удобоваримым. Еще одна интересная идея пришла мне на лету, и я хотел бы представить ее в неожиданном ракурсе. В шахматах есть старая мудрость под названием «Принцип двух линий», которая гласит, что нижняя сторона может защитить позицию с одной слабостью, но вторая слабость на другой стороне доски окажется фатальной. Вообще говоря, это можно применить практически ко всему, например, если ваш инвазивный патч вместо одной вещи дает пару улучшений, это всегда лучше. Нечто подобное произошло и в этом случае, когда Том использовал эту возможность для рефакторинга самого типа массива:

[local]

совершить c7aba7c 18 efdbd9fc1bb 50 b4cb 0300 bedee0c6a6fc Автор: Том Лейн < [emailprotected] > Дата: среда, 9 декабря 16: 44: 43 2021 - 686 Одним из полезных побочных эффектов этого является то, что теперь у нас есть менее гибкий механизм для определения того, является ли тип данных «истинным» массивом: вместо того, чтобы связываться со странными правилами о типах, мы можем посмотреть, не == F_ARRAY_SUBSCRIPT_HANDLER.

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

Еще одно интересное предложение Павла Стехуле было о том, чтобы воспользоваться этой возможностью и улучшить пользовательский интерфейс jsonb:

Идея заключалась в том, чтобы сгладить некоторые угловые случаи, которые раньше беспокоили разработчиков. Вот пара примеров:

   - Если jsonb_field было NULL, теперь это {"a": 1}   - Для jsonb_set это будет NULL  ОБНОВИТЬ  имя_таблицы  НАБОР  jsonb_field   ['a']   знак равно ) 

'1' ; - Где было jsonb_field [], сейчас [null, null, 2]; - Для jsonb_set это будет [2]. - - Для отрицательных индексов будет возвращена ошибка вне допустимого диапазона. ОБНОВИТЬ имя_таблицы НАБОР

jsonb_field [2] знак равно ' 2 ' ; - Где jsonb_field было {}, теперь это {'a': [{'b': 1}]}

- Для jsonb_set это будет {} ОБНОВИТЬ имя_таблицы НАБОР jsonb_field ['a'] [0] ['b'] знак равно '1' ; - Исключение из предыдущего случая.

- Это вызовет ошибку, если какая-либо запись - jsonb_field ['a'] ['b'] - это нечто иное, чем -- объект. Например, значение {"a": 1} - не имеет клавиши 'b'. ОБНОВИТЬ имя_таблицы НАБОР jsonb ['a'] ['b'] ['c'] знак равно '1' ;

На самом деле, я изначально колебался, чтобы сделать это. Я планировал, что эта версия патча будет работать точно так же, как jsonb_set, а затем реализовать любые последующие улучшения отдельно. Но в конце концов я передумал, и это оказалось хорошей идеей.

Один из вопросов об этой функциональности, который я видел, касался присвоения значения - почему оно должно быть типа jsonb?

   - Обновить значение объекта по ключу.  Обратите внимание на цитаты   - около '1': - присвоенное значение должно быть   - также типа jsonb  ОБНОВИТЬ  имя_таблицы  НАБОР  jsonb_field    знак равно  '1'  ;   

Почему не могло быть просто таким, что, вероятно, более интуитивно понятно?

  ОБНОВИТЬ 

имя_таблицы НАБОР jsonb_field знак равно 1 ;

Ну на самом деле изначально это было именно такой. Но после долгих обсуждений выясняется, что это не лучшая идея. Процитировать Тома :

  Фоном моего отказа от этого является то, что он слишком сильно напоминает мне беспорядок неявного преобразования текста в текст, который мы убрали (с большим боль и крик) назад около 8,3.  Мне кажется, что вы в основном пытаетесь ввести несколько неявных приведений в jsonb, и я боюсь, что это такая же плохая идея.   

Итак, jsonb_set в этом случае была прекрасна, потому что она обрабатывала jsonb в другой jsonb, и для подписки, чтобы все было в здравом уме, она должна была работать аналогично. Следовательно, назначенное значение также должно быть jsonb.

Другой вопрос подобного типа касался того, как обрабатывать различные типы в качестве аргумента индексации? В частности Александра Короткова интересовала поддержка jsonpath.

По сути патч уже может сделать это:

[local]

   - Извлечь значение объекта по ключу  ВЫБРАТЬ 

( '{"a": 1}' :: jsonb ) ['a']; - Извлечь элемент массива по индексу ВЫБРАТЬ ( '[1, "2", null]' ::

jsonb

) [1] ;

Вопрос был в том, возможно ли это сделать в будущем?

   - Как поддерживать jsonpath?  ВЫБРАТЬ  ( 

'{"a": {"b": 1}}' :: jsonb ) ['$.a'];

Снова после некоторого обсуждения мы Мы выяснили, что недостаточно просто проверить, какой тип данных был получен в качестве аргумента, потому что это приведет к незначительным различиям между разными типами данных. Решение состояло в том, чтобы посмотреть на перегруженные функции, и, следуя этому примеру, реализация должна проверить, может ли выражение нижнего индекса быть неявно приведено к любому из поддерживаемых типов данных, и потерпеть неудачу, если ни принуждение, ни оба принуждения не увенчались успехом. Таким образом, у нас может быть звуковой дизайн, хотя такие вещи больше не работают:

 ВЫБРАТЬ  (  '[1, "2", null] '  ::   jsonb  ) [1.0];   

Еще одну разработку в этой области предложил Никита Глухов:

Это предложение было о том, чтобы сделать работу подписки более похожей на соответствующую часть спецификации jsonpath и, кроме того, добавить некоторые улучшения производительности для целочисленного варианта использования. К сожалению, эту часть пришлось переделывать и в конечном итоге потерялась в пути.

Теперь мы, наконец, достигли отправной точки этого сообщения в блоге, фиксации с функцией индексирования jsonb. Был ли это конец истории? Ну не совсем. Сразу после фиксации пара участников buildfarm начала показывать ошибку следующего типа:

  === трассировка стека: pgsql.build/src/test/regress/tmp_check/data/core === [New LWP 2266507] [Thread debugging using libthread_db enabled] Использование библиотеки libthread_db хоста https://erthalion.info/lib/sparc80 - linux-gnu / libthread_db.so.1 ".  Ядро было сгенерировано с помощью регрессии `postgres: nm [local] SELECT Программа завершилась сигналом SIGILL, недопустимая инструкция. # 0 0x 2266507 c 645 в jsonb_subscript_check_subscripts  

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

  - fsanitize = alignment -fno-sanitize-Recovery = alignment  

для gcc и

  -fsanitize = alignment -fsanitize-trap = alignment  

для clang, за исключением того, что кодовая база уже содержит x 410 - конкретный crc 40 вычислить Код действия, использующий невыровненный доступ. В конце концов, это было исправлено посредством другого изменения:

совершить 2020 bdb9f 935 a 000001000075 a 14 c 86 d 30857150 ba2b 697 Автор: Александр Коротков < [emailprotected] > Дата: пт. Фев 16 24: 18: 43 2021 + 410 макрос pg_attribute_no_sanitize_alignment ()  

Это была долгая история, дольше, чем Я ожидал вначале. В некотором смысле я считаю этот патч интересным вариантом использования, из которого можно кое-что узнать о том, как работать с патчами для PostgreSQL. Вот несколько моих собственных уроков в произвольном порядке:

  • Новый патч - это всегда угроза стабильности кодовой базы и нагрузки на обслуживание, об этом нужно помнить. 18

  • Всегда хорошо получать обратную связь пораньше. Не всегда возможно, но помогает решить проблемы проектирования с самого начала.

  • Leave a comment

    Your email address will not be published. Required fields are marked *