Приветствую, current_user()!

В мире разработки баз данных хранение кодовой базы (далее - КБ) и её обновление на разных инстансах являются задачами, с которыми сталкиваются многие специалисты. Одним из общепринятых подходов к этой проблеме является использование файлов миграций, которые содержат изменения состояний базы данных и переносят её из одной версии в другую. Однако, помимо этого метода, существуют и другие инструменты и методы управления миграциями, которые могут быть более эффективными и удобными в различных сценариях.

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

Цель этой статьи - предоставить читателям новые идеи и инструменты для улучшения процесса управления изменениями в базе данных, что позволит им эффективнее и безопаснее развивать свои проекты.

Предисловие

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

В этой статье мы сосредоточимся исключительно на концепции подхода к хранению и разработке кодовой базы. Здесь мы не будем предлагать готовые инструменты (за исключением, возможно, псевдокода или bash-кода для пояснения происходящего).

Для удобства понимания, вся статья будет разделена на следующие части:

и так начнём.


Пример того, как в данный момент используют файлы миграций

Для различных инструментов и подходов выглядит всё по-разному, я же попытаюсь описать псевдовариант.

Смысл тут очень простой: у нас есть КБ версии прода, мы хотим хотим добавить новую фичу. Для этой фичи надо добавить какие-то колонки, поменять какие-то функции, добавить триггеры.

Все эти манипуляции заносятся в файл миграции с именем $number+1.sql, где number - это номер последней миграции в каталоге миграций.

После этого можно накатывать эту миграцию тем инструментом, которым пользуются в данном проекте.

Пример

Создадим файл миграции:

# путь к папке с миграциями
MIGRATION_PATH="~/git/project/sql/migrations"

# запишем сразу number+1 сюда
NUMBER="101" 

# в результате фичи добавляли таблицу users
echo "create table users(id int, name text);" >> $MIGRATION_PATH/$NUMBER.sql

# допустим, ещё был создан индекс по users.name
echo "create index on users(name);" >> $MIGRATION_PATH/$NUMBER.sql

Ну и всё, миграция готова, осталось только скормить его вашему ПО для заливки миграций

Инструментов для заливки миграций существует очень большое количество. Вот, например, некоторые из них: flyway, liquibase, pg_mig, db-migrate, pg_codekeeper.

В общем их алгоритм таких инструментов ( кроме pg_codekeeper ) выглядит следующим образом:

  1. Сбор всех файлов миграций на входе

  2. Проверка, залиты ли эти миграции в БД

  3. Те, которые не залиты - заливаются


Постановка задачи

Этот раздел назван не совсем корректно.

В нём будут описаны признаки подхода к разработке, к которым подойдёт описанная в этой статье концепция.

Задача ( критерии ):

  • в проекте бóльшая часть бизнес-логики реализована на уровне БД -> почти все изменения кода бэкенда затрагивают хранимые функции в БД

  • сделать удобное хранение кода в репозитории

    • чтобы можно было удобно смотреть историю изменений тех или иных

    • чтобы было удобно проводить ревью кода

  • бэкендеров/задач много - нужна хорошая устойчивость к конкурентной ( параллельной ) разработке

    • имеется в виду, что может быть открыто несколько МР на изменение одной и той-же функции в БД, нужно сделать так, чтобы изменения не затирали друг друга


Описание структуры концепции

Мне категорически не нравилось то, что мы должны создавать для каждой фичи свой файл миграции. При параллельной разработке часто бывало, что вносятся изменения в одну и ту же функцию, по итогу создавались 2 миграции, которые не видели изменения друг друга, из-за чего при заливке миграций в прод мы могли лишиться части функционала

Пример такой ситуации

Допустим, у нас была функция:

-- migration #100
create function myfunc(
  p_user_id bigint,
  p_amount numeric) 
returning bool 
as $$
begin
  return exists(
    select from users
    where 
      id = p_user_id
      and amount > p_amount);
end;
$$

Нам ставится задача, чтобы мы добавили сюда ещё проверку по status = 'active' , и в МР мы создадим файл миграции, в котором будет функция такого вида:

-- migration #101
create function myfunc(
  p_user_id bigint,
  p_amount numeric) 
returning bool 
as $$
begin
  return exists(
    select from users
    where 
      id = p_user_id
      and amount > p_amount
      and statuss = 'active');
end;
$$

Параллельно с этой задачей нам поставили новую задачу, в ней говорится, что проверка на amount должна быть с запасом в 100 у.е., т.е. amount > p_amount + 100 , получается, во втором МР мы создадим файл миграции, в котором будет функция:

-- migration #102
create function myfunc(
  p_user_id bigint,
  p_amount numeric) 
returning bool 
as $$
begin
  return exists(
    select from users
    where 
      id = p_user_id
      and amount > p_amount + 100);
end;
$$

И в чём вся проблема заключается:

когда инструмент для заливки миграций будет отрабатывать, он сначала зальёт первую миграцию, потом вторую. Соответственно, изменения по первой задаче зальются в БД, а потом сотрутся.

Чтобы этого избежать - приходится много раз всё проверять и писать кучу приблуд для проверок. Или-же не допускать параллельной разработки одного и того-же объекта в БД ( в данном случае это функция )

Чтобы избавиться от таких проблем можно попробовать хранить каждый объект БД в своей миграции ( как в традиционном программировании ). Например: создание таблицы `users` будет храниться в файле `public/table/users.sql`, а функция `myfunc` - в `public/function/myfunc.sql`

Также нам нужен инструмент, который сможет такие миграции заливать ( т.е. не только когда файл добавляется в в список, но и когда модифицируется ). Как раз liquibase умеет такое делать.

К чему мы приходим - можно взять код всех объектов БД, распарсить этот код в отдельные файлы-миграции. В будущем, когда эти файлы-миграции будут изменяться - не будет создаваться копия этих объектов ( как в примере выше ), а просто модифицироваться уже существующий файл-миграция.

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

По итогу, с учётом некоторых доработок наше хранилище миграций превращается в нечто подобное:

  • migrations/before_scripts/

    • Скрипты, которые выполняются перед заливкой основной части. Например, это скрипты, которые настраивают сессионные переменные

  • migrations/migrations_scripts/

    • Скрипты, которые выполняют миграцию структуры БД для некоторых объектов. Для удобства назовём их "добивочные" миграции

  • migrations/migrations/

    • Основные скрипты миграции данных

  • migrations/data_scripts/

    • Это скрипты, которые содержат в себе не схему БД, а именно данные таблиц ( например таблица переводов, таблица с конфигами и т.д. )

  • migrations/after_scripts/

    • Скрипты, которые выполнятся после заливки всех миграций. Например, обновление конфигов, пересбор каких-то вьюх, сброс статистики pg_state_statement и другое

Описание концепции разработки

В этом разделе я попробую описать всё, что должно происходить с МР и БД от начала разработки фичи и до её вливания в основную ветку ( или деплой в прод ).

И так, вся разработка у нас поделена на несколько частей ( стейджи ):

Теперь рассмотрим, конкретней каждый этап:

Примечание: ниже я буду употреблять некоторые возможно непонятные выражения, поэтому дам тут их пояснения:

  • master-ветка - ветка в репозитории, в которой хранится тот код, который в данный момент работает в проде

  • dev-ветка - ветка в репозитории, которая является "основной веткой разработки"

    • эта и только это ветка может сливаться в master-ветку

  • фич-ветка - ветка в репозитории, в которой содержатся изменения, касаемые разработки определённой фичи

    • эта ветка сливается в dev-ветку

  • прод-БД, dev-БД, фич-БД - базы данных, в которых содержится код из соответствующих им веток

Инициализация разработки фичи

Перед тем, как начать программисту работать с БД - её над создать. Для этого сделаем структурную копию БД из дев-ветки ( думаю, переменные, которые тут употребляются, не нуждаются в комментариях):

psql -d postgresql://$DB_USER:$DB_PASS@$DB_HOST:$DB_PORT/$sourceDB -c "create database $targetDB"
pg_dump --format=c --schema-only -d postgresql://$DB_USER:$DB_PASS@$DB_HOST:$DB_PORT/$sourceDB \
  | pg_restore --format=c -d postgresql://$DB_USER:$DB_PASS@$DB_HOST:$DB_PORT/$targetDB

Что выполняет этот код?

  1. Создаёт БД $targetDB

  2. Создаём дамп структуры БД $sourceDB ( это наша dev-БД )

  3. Восстанавливаем эту структуру в $targetDB

После этого, необходимо будет подключить к этой БД все внешние кроны, сервисы и т.д. Но это уже DevOps и тут мы не будем рассматривать всё это, нас интересует только БД и разработчик.

Очень удобно это всё делать на одном кластере с dev-веткой. Потому что у вас будут уже подключены все shared_library и созданы все роли ( также настроен общий hba ).

Разработка

Вот в этом разделе разработчик будет радоваться как никто другой.

Поскольку в таком подходе разработчику не надо нигде фиксировать свой код ( я имею в виду в файликах миграций, комментариях к МР, чатах и пр ). Всё что делает разработчик - делает свою работу непосредственно в фич-БД ( да, есть нюансы, но их очень мало ).

Когда разработчику необходимо слить результат своей работы в репозиторий и отправить на ревью - он выполняет следующие действия:

  1. Создаёт дамп структуры тестовой БД

  2. Парсит этот дамп и раскладывает по нужным папкам/файлам в репозитории.

  3. Коммитит и пушит

Да, некоторые программы умеют это делать за один раз, но всё же доверия к pg_dump больше :)

Небольшой пример

Попробую тут описать маленький пример того, как это выглядит.

# первым делом сделаем дамп схемы
pg_dump --file "dump_schema.sql" --format=p --schema-only -d postgresql://$1:$2@$3:$4/$5
Слияние фич-ветки в дев-ветку
# запустим скрипт ( в данном случае на python ), который распарсит все объекты и упакует всё в нужный файл
./pg_schema_split.py dump_schema.sql $project_path/sql/migrations

И так, дамп мы получили, теперь бы его распарсить.

Очень удобным будет вариант парсинга DDL/DCL через их идентификацию какой-нибудь библиотекой парсинга SQL-скриптов, постепенно складывая их по нужным папкам/файлам. Пример такой билиотеки: pg_query_go

В начале для тестов я писал вариант "нативного" парсера на python.
Всё дело в том, что pg_dump на выходе выдаёт скрипты в виде:

--
-- Name: users; Type: TABLE; Schema: public; Owner: postgres
--

CREATE TABLE public.users (
    id integer NOT NULL,
    name text
);


ALTER TABLE public.users OWNER TO postgres;

т.е. выводит шапку ( хедер ) блока кода, а потом в теле этого блока пишется сам скрипт. Если правильно всё распарсить - можно быстро понимать, где и к чему какой блок кода относится.

После распарсинга ещё надо переделать скрипты на множественный запуск ( например create table изменить на create table if not exists ).
Замечу, что не все объекты поддерживают if not exists / replace , для некоторых приходится писать подобную оболочку:

DO $$
BEGIN
  IF NOT EXISTS ( <проверка того или иного объекта> ) THEN
    <скрипт создания объекта>
  END IF;
END
$$;

Примеры того, что на выходе - будут ниже ( в примере ниже показаны старые скриншоты, их я делал ещё со скриптом на python).

Если в результате ревью/тестов обнаружились проблемы - можно спокойно править всё прямо в базе ( как при разработке ) и скриптом сливать в фич-ветку.

После того, как разработчик всё сделал, прошли тесты, ревьюер всё проверил - можно перейти к подготовке для вливания в дев-ветку.

Слияние фич-ветки в дев-ветку

Как раз на этом этапе создадутся те самые непонятные "добивочные" миграции.

Пример, зачем такие миграции нужны:

Была таблица:

create table if not exists users(
  id int,
  name text
);

В результате разработки фичи мы добавили в эту таблицу поле age int , наш скрипт миграции для этой таблицы стал следующим:

create table if not exists users(
  id int,
  name text,
  age int
);

Как все мы прекрасно понимаем, выполнение этого кода заново не создаст новой колонки в проде.

Нам нужен запрос, который создаст колонку отдельно:

alter table if exists users add column if not exists age int;

Такие скрипты легко генерировать через различные sql-diff-утилиты.

Последовательность будет следующая:

  1. Создадим новую БД от дев-ветки ( скрипт мы и так знаем, не буду дублировать )

  2. Накатим туда те основные миграции, которые у нас получились в результате работы прошлого этапа.

  3. Сравним новую БД и фич-БД какой-нибудь sql=diff-утилитой, а результата запишем в файл "добивочной" миграции

Вот пример использования pg_codekeeper-а:

$run_pgcodekeeper $newDB_url $targetDB_url >> $migrationsPath/migration_scripts/$continueMigrationName.sql

Когда создастся "добивочная" миграция, всё что надо будет сделать - ещё раз её проверить ( автоматика- это круто, но прод уронить тоже не хочется ). После чего - закрыть МР.

В итоге блок-схема работы будет выглядеть как-то так
В итоге блок-схема работы будет выглядеть как-то так

Пример использования

Я буду тут прикреплять скриншоты, чтобы было немного понятно, что где и как.

Тут будут показаны программы pgAdmin и sublime-merge

У нас есть функция и таблица в БД:

Нам ставят вот такую задачу:

  1. Создать новую таблицу user_cards

  2. Добавить в users новое поле count_cards

  3. Создать новую функцию get_user_count_cards($user_id), которая будет выводить количество карт у пользователя

  4. В существующей функции my_func переделать ответ с a+a на a*a

Создать новую таблицу user_cards :

Ну тут всё просто:

create table user_cards(
  id serial not null primary key,
  user_id int not null, -- тут бы стоило добавить FK, но я уже прикрепляю старые скрины и не хочу там ничего править :)
  card_number text,
  other_data jsonb
);

Добавить в users новое поле count_cards :

Думаю, тут даже запрос писать не надо, всё понятно ( да можно хоть ручками в pgAdmin-е создать колонку)

Создать новую функцию get_user_count_cards($user_id), которая будет выводить количество карт у пользователя:

В существующей функции my_func переделать ответ с a+a на a*a :

И так, задача сделана, теперь мы хотим слить схему БД в виде файлов-миграций для каждого объекта. Запустим pg_dump + скрипт для парсинга sql-код по файлам.

Ну что, проверим, что изменилось в репозитории?

Изменения в файле public/function/my_func.sql
Изменения в файле public/function/my_func.sql
Изменения в файле с таблицей users
Изменения в файле с таблицей users
Новый файл с функцией get_user_count_cards
Новый файл с функцией get_user_count_cards
Созданы миграции для таблицы user_cards :Поскольку там serial - необходимо создать sequence, потом подключить её, поэтому всё в несколько файлов.
Созданы миграции для таблицы user_cards :
Поскольку там serial - необходимо создать sequence, потом подключить её, поэтому всё в несколько файлов.

Ревьюер нас проверяет, задача сделана, создадим "добивочную" миграцию, поскольку в таблице users добавлялось поле count_cards .

Запустим скрипты, которые выполнят работу по созданию такой миграции ( описано было выше ). На выходе получаем файл:

"Добивочная" миграция
"Добивочная" миграция

Если после этого к нам "резко" прилетело исправление ТЗ ( ну бывает же ), то всё что нам надо сделать - удалить из ветки "добивочную" миграцию, разработать, и опять отдать на ревью с последующим созданием "добивочной" миграции.

В случае, если "добивочная" миграция не нужна - её можно и не создавать.

"Добивочные" миграции, которые залиты в на все существующие БД ( я имею в виду ревью-БД и прод-БД ) можно удалять, они нам уже не понадобятся ( грязь надо чистить ).

Вывод, примечания и прочее:

Такой подход позволяет хранить sql-код объектов БД в различных файлах и не допускать повтора скриптов.

Всё это достигается тем, что инструмент для заливки миграций будет перезаливать миграции, которые были модифицированны.

Также создаются " добивочные" миграции специально для тех миграций, которые не могут довести структуру БД до нужной версии ( пример с добавлением колонки был выше ).

Всё это позволяет поддерживать конкурентную разработку одних и тех-же объектов, не дублировать код, удобное ревью и удобный workflow у разработчика и ревьюера.
Правда, за это придётся заплатить нервами на настройку, а также отказаться от откатов миграций ( в стандартном подходе создаётся как миграция на новую версию БД, так и миграция на откат версии, а такие откаты в данный архитектуре не поддерживаются ).


Я хочу ещё раз подчеркнуть, что такая система хорошо подходит именно для проектов, где БД является бэкендом по большей части, и где в основном разработка бекенда - это модификация функций.

Есть закономерный вопрос: Почему бы не использовать pg-codekeeper, ведь он уже умеет хранить структурно файлы с объектами, а также создавать миграции ?

Тут ответов несколько:
Во-первых, если генерировать одну большую миграцию на выходе, сохранять её в файлах миграций или заливать на прод - появляются проблемы с конкурентной разработкой ( то что в начале пример был).
Если хранить в репозитории код объектов, а миграцию генерировать именно в момент заливки - придётся много ревьюить, что будет достаточно неудобно.

Во-вторых, его функционал немного скромный, в нём можно управлять разве-что параметрами include/exclude .

Подход, описанный тут, позволяет сверху сделать достаточное количество доработок. Например: разделение кода по частям ( модули ), подмену некоторых параметров ( например переопределение дефолтного владельца ), сохранение и перенос данных таблиц, управление через контексты ( на примере liquibase ) и т.д. Пока-что прям все плюшки сказать не могу, поскольку мы далеко не всё смогли из неё выжать. Но, то что сейчас поддерживается - меня уже начинает радовать.

Повторюсь ещё раз. Я просто хотел описать подход к хранению миграций, который будет удобен разработчикам и ревьюерам.

И ещё раз повторюсь, что всё находится на этапе тестирования, допиливания фичей и устранения подводных камней.

Зачем я написал эту статью?

Для того, чтобы показать, как можно реализовать разработку.

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

Спасибо за внимание, всем добра !

Комментарии (42)


  1. len2367
    21.03.2024 20:57
    +2

    Больше нечего сказать...
    Больше нечего сказать...


    1. Rion333
      21.03.2024 20:57
      +1

      Тоже такое чувство было при чтении статьи. От журнала миграции БД потенциально зависят все компоненты, которые используют данную БД. Добавление лишнего слоя процессинга в миграции не решит эту принципиальную проблему, а только все усложняет.

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


  1. ALexKud
    21.03.2024 20:57

    Я такую задачу решил просто создав триггер в каждой бд сервера . Этот триггер все события dml пишет в отдельную таблицу отдельной БД. Всё что делается на сервере фиксируется последовательно, модификации таблиц, код хранимых процедур, функций, индексов и тп. Проекты у меня небольшие, максимум по 150 процедур и 40 таблиц. Сейчас 6 проектов ведётся и поддерживается. Не для продаж, корпоративная бд задач производства. Задачи все непростые. Одну из них о описал в своей статье на хабре


    1. miruzzy Автор
      21.03.2024 20:57

      Когда мы обсуждали различные подходы - также обсуждали и этот вариант.

      Но он не подошёл.
      К примеру, бывает и такое, когда за один МР ты насоздаёшь объектов, а потом часть удалишь. Потом смотри весь этот лог и перетирай его.

      А вот, к примеру, фишка, которая у нас сейчас вводится в поддержку ( вот прям сейчас допиливаем и будем тестить ): можно комментарием к объекту управлять его контектом.
      Вот пример: допустим, такая-то таблица должна быть только на тестовом и ревью сервере, а на проде - нет. Всё что мы делаем - просто в комментарии к таблице пишем флаг, а парсер уже всё сам настраивает.

      Ну и ещё раз попробую напомнить про тот подход, который я описывал: если мы в функции с 1к строк ( к примеру ) модифицируем 1-у строку, то ревьюеру будет удобно такое проверять, поскольку в репозитории будет показана модификация одной строки, а не сплошь тысяча новых строк.

      Надеюсь, я донёс мысль :)

      У нас на одном из проектов ( не самом большом ) 700+ функций. Бывает и такое, что одновременно открыто 3 МР на изменение одной и той-же функции.


      1. ALexKud
        21.03.2024 20:57

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

        Hidden text


  1. dyadyaSerezha
    21.03.2024 20:57

    Я не понял в самом начале - как получилось, что слияние изменений функции myfunc в git (очевидно, в файл myfunc.sql) не обнаружило конфликтов между миграциями #100 и #101?


    1. miruzzy Автор
      21.03.2024 20:57

      Ну потому что это разные файлы


      1. dyadyaSerezha
        21.03.2024 20:57
        +1

        То есть, вы хранили изменения объектов, но не сами объекты? Так, может, исправить это и всё?


        1. miruzzy Автор
          21.03.2024 20:57

          100, 101 и 102
          Это как раз пример того, как собирают код в миграции ( это всё разные файлы версии БД )

          Поэтому они даже гит-конфликтом не отловятся


          1. Akina
            21.03.2024 20:57

            Что-то я так и не понял, где и как будет разрешаться конфликт, описанный в примере параллельного изменения функции (те самые миграции с номерами 100, 101 и 102). Не, я, слава богу, не разработчик, и мне это всё не надо, просто любопытно... но я не понимаю, почему при создании вот этих миграций не фиксируется номер той миграции, которая послужила базой. Если бы было зафиксировано, что есть миграция 101 после миграции 100, есть миграция 102 после миграции 100, то такой конфликт никак не мог бы привести к тому, что применение второй миграции трёт изменения, внесённые первой миграцией, потому что миграция 102 не может выполняться после миграции 101, она идёт после миграции 100. И ловится такой конфликт буквально по щелчку пальцев. А разрешается - исключительно вручную. Ведь в примере с функцией требования никак не могут быть реализованы одновременно - либо то, либо другое, а если реализовать их вместе, получится третье, которого вообще никто не просил.

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


          1. dyadyaSerezha
            21.03.2024 20:57
            +1

            Миграции миграциями, а объекты целиком хранить надо в первую очередь - описания таблиц (create table), процедур и прочего. А иначе как??


  1. titan_pc
    21.03.2024 20:57

    Хм. Странно это всё.

    А не проще иметь просто разные контура для этих целей. Ревью, проверка, тестирование. Ну то есть отдельную СУБД.

    Ну и все кто перерос опенсорс. Рано или поздно пилят что-то своё, удобное и полезное.

    Для команды разработки более удобным бывает например делать миграции программным кодом, который в sql превращается. А откатывать миграции удобно снапшотами, если там и данных полно поменялось вместе с метаданными. Ну зачем например пилить целый скрипт отмены, если до даже лютой миграции можно скопировать ВМ, да и всё.


    1. miruzzy Автор
      21.03.2024 20:57

      так вопрос не в том, чтобы иметь разные БД, а в том, как хранить и делать ревью


  1. ptr128
    21.03.2024 20:57

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

    Почему то пропущена ветка test. А извлечение кода функций и процедур из БД лишает возможности их препроцессинга, что уже ни в какие ворота не лезет.


    1. miruzzy Автор
      21.03.2024 20:57
      +1

      Зачем вообще нужны миграции для функций и хранимых процедур? 

      Немного не понял, а как вы предлагаете заливать функции и процедуры в прод ?

      ЗЫ думаю, вы не верно интерпретировали слово "миграция" в контексте этой статьи

      Почему то пропущена ветка test

      Вы можете выполнять тесты как в общей ветке разработки, как в предпроде, так и в фич-ветке и соответствующей ей БД, вас никто не ограничивает


      1. ptr128
        21.03.2024 20:57

        Немного не понял, а как вы предлагаете заливать функции и процедуры в прод ?

        CREATE OR REPLACE FUNCTION или CREATE OR REPLACE PROCEDURE. А как еще?

        Естественно после интеграционного тестирования на staging. А уж история изменений кода функций и процедур пусть остается в GIT.

        Аналогично, и подавляющее большинство изменения метаданных производятся чем то типа ALTER TABLE IF EXISTS SQL_DB_INSTANCE.OBJECT_NAME ADD COLUMN IF NOT EXISTS ...

        Вы можете выполнять тесты как в общей ветке разработки, как в предпроде, так и в фич-ветке и соответствующей ей БД, вас никто не ограничивает

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

        А замораживать на несколько дней тестирования релиза основную ветку разработки - не лучшая идея. Особенно, если релизный цикл всего 1-2 недели.

        И Вы так и не ответили на вопрос об отказе от препроцессинга.


        1. miruzzy Автор
          21.03.2024 20:57

          CREATE OR REPLACE FUNCTION или CREATE OR REPLACE PROCEDURE. А как еще?

          Это понятно, что такие скрипты. Как вы эти скрипты на прод зальёте ?
          Руками или через какой-то софт ? ( я намекаю, что софт кушает как раз миграции ( файлы со скриптами) для БД )

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

          Никто вам не мешает использовать,, условно ZFS и быстро откатываться назад

          А замораживать на несколько дней тестирования релиза основную ветку разработки - не лучшая идея. Особенно, если релизный цикл всего 1-2 недели.

          Вводите промежуточный сервер
          Я не понимаю, при чём тут рассмотрение стейджей ревью, тестирования и т.д. к теме этой статьи ( я напомню, что статья про хранение скриптов для версионирования БД )

          И Вы так и не ответили на вопрос об отказе от препроцессинга.

          Мб я тупой, но не могли бы вы по другому поставить вопрос ?


          1. ptr128
            21.03.2024 20:57

            Руками или через какой-то софт ? ( я намекаю, что софт кушает как раз миграции ( файлы со скриптами) для БД )

            Почему руками? При сборке после препроцессинга получается уже готовый скрипт, который можно заливать напрямую psql. Вот взял первый попавшийся проект. Результирующий скрипт 16 МБ, через psql, если не активизируются миграции (в моем понимании), он заливается за 25-30 секунд. В этом проекте полторы тысячи таблиц и две тысячи процедур и функций. В чем проблемы?

            Никто вам не мешает использовать,, условно ZFS и быстро откатываться назад

            Вообще не понял о чем Вы. Элементарные создания новых индексов на таблице в терабайт-другой сожрет на множестве контуров десятки если не сотни терабайт.

            статья про хранение скриптов для версионирования БД

            Вот это меня и удивляет, так как хранение и версионирование решается средствами GIT. Зачем БД знать что-либо, кроме тега в ветке?

            Мб я тупой, но не могли бы вы по другому поставить вопрос ?

            Попробую примером.

            Вот пример части кода процедуры до препроцессинга
              SVC_LOG_STEP_START(0, proc.step_startup_time, 'Starting parmeters parsing')
              BEGIN
                SVC_LOG_PARAMETER_TABLE(SVC_PARAMETER_loaded_legs, loaded_legs)
                SVC_LOG_PARAMETER_DATE(SVC_PARAMETER_on_date, on_date)
                SVC_LOG_PARAMETER_INT(SVC_PARAMETER_optimal_rows, optimal_rows)
                SVC_LOG_PARAMETER_NUM(SVC_PARAMETER_speed_dev, speed_dev)
              SVC_LOG_SQL_EXCEPTION_LOG_AND_RAISE_MAIN
              SVC_LOG_STEP_FINISH(0, proc.step_startup_time, 'Finished parmeters parsing')
            

            А вот после
              proc.step_startup_time=clock_timestamp();
              INSERT INTO dev.SVC_ExecutionLog (SourceName, LogLevel, SessionId, LogMessageId, LogMessage)
              VALUES ('SVC_PrincipalDirectionOptimizer_sp', 0, session_id, 14, 'Starting parmeters parsing');
              COMMIT AND CHAIN;
              BEGIN
                INSERT INTO dev.SVC_ExecutionLog (SourceName, LogLevel, SessionId, ParameterId, LogMessageId, LogMessage, LogTableContent)
                SELECT 'SVC_PrincipalDirectionOptimizer_sp', 0, proc.session_id, P.ParameterId, 13, P.Name, loaded_legs::text
                FROM dev.SVC_Parameters P
                WHERE P.ParameterId=8;
            
                INSERT INTO dev.SVC_ExecutionLog (SourceName, LogLevel, SessionId, ParameterId, LogMessageId, LogMessage, LogTextData, LogDateData)
                SELECT 'SVC_PrincipalDirectionOptimizer_sp', 0, proc.session_id, P.ParameterId, 13, P.Name, on_date, on_date
                FROM dev.SVC_Parameters P
                WHERE P.ParameterId=16;
            
                INSERT INTO dev.SVC_ExecutionLog (SourceName, LogLevel, SessionId, ParameterId, LogMessageId, LogMessage, LogTextData, LogIntData)
                SELECT 'SVC_PrincipalDirectionOptimizer_sp', 0, proc.session_id, P.ParameterId, 13, P.Name, optimal_rows, optimal_rows
                FROM dev.SVC_Parameters P
                WHERE P.ParameterId=24;
                
                INSERT INTO dev.SVC_ExecutionLog (SourceName, LogLevel, SessionId, ParameterId, LogMessageId, LogMessage, LogTextData, LogNumData)
                SELECT 'SVC_PrincipalDirectionOptimizer_sp', 0, proc.session_id, P.ParameterId, 13, P.Name, speed_dev, speed_dev
                FROM dev.SVC_Parameters P
                WHERE P.ParameterId=48;
                EXCEPTION WHEN OTHERS THEN
                  GET STACKED DIAGNOSTICS 
                    proc.sql_state=RETURNED_SQLSTATE,
                    proc.sql_column=COLUMN_NAME,
                    proc.sql_constraint=CONSTRAINT_NAME,
                    proc.sql_datatype=PG_DATATYPE_NAME,
                    proc.sql_table=TABLE_NAME,
                    proc.sql_schema=SCHEMA_NAME,
                    proc.sql_message=MESSAGE_TEXT,
                    proc.sql_detail=PG_EXCEPTION_DETAIL,
                    proc.sql_hint=PG_EXCEPTION_HINT,
                    proc.sql_context=PG_EXCEPTION_CONTEXT;
            
                  INSERT INTO dev.SVC_ExecutionLog (SourceName, LogLevel, SessionId, LogMessage, SQLState, SQLColumn, SQLConstraint, SQLDataType, SQLTable, SQLSchema, SQLMessage, SQLDetail, SQLHint, SQLContext)
                  VALUES ('SVC_PrincipalDirectionOptimizer_sp', 32, session_id, 'SQL error '||proc.sql_message, proc.sql_state, proc.sql_column, proc.sql_constraint, proc.sql_datatype, proc.sql_table, proc.sql_schema, proc.sql_message, proc.sql_detail, proc.sql_hint, proc.sql_context);
            
                  INSERT INTO dev.SVC_ExecutionLog (SourceName, LogLevel, SessionId, IsFinish, LogMessageId, LogMessage, LogIntData, LogIntervalms)
                  SELECT I.SourceName, L.LogLevel, session_id, TRUE, 2,
                    CASE WHEN L.LogLevel>=12 THEN 'Services finished with errors'
                         WHEN L.LogLevel>4 THEN 'Services finished with warnings'
                         ELSE 'Services finished succesfully' END,
                    I.LogIntData, (EXTRACT(EPOCH FROM clock_timestamp()-I.LogTime)*1000)
                  FROM dev.SVC_ExecutionLog I
                  CROSS JOIN LATERAL ( 
                    SELECT MAX(LogLevel) AS LogLevel
                    FROM dev.SVC_ExecutionLog L
                    WHERE L.SessionID=session_id ) L
                  WHERE I.IsStart AND I.SessionId=session_id;
                  COMMIT;
                  RAISE WARNING 'SQL error %', sql_message;
                  RETURN;
                END;
              INSERT INTO dev.SVC_ExecutionLog (SourceName, LogLevel, SessionId, LogMessageId, LogMessage, LogIntervalms)
              VALUES ('SVC_PrincipalDirectionOptimizer_sp', 0, session_id, 15, 'Finished parmeters parsing', ROUND(EXTRACT(EPOCH FROM clock_timestamp()-proc.step_startup_time)*1000));
              COMMIT AND CHAIN;
            


        1. Mingun
          21.03.2024 20:57

          CREATE OR REPLACE FUNCTION или CREATE OR REPLACE PROCEDURE. А как еще?

          В PostgreSQL не сработает, если типы / количество параметров у функции / процедуры поменялись. Придётся дропать. А это тоже боль, так как зачастую другие объекты от них зависят (другие функции / процедуры, представления), поэтому дропать надо каскадно, а потом как-то восстанавливать дропнутое обратно.


          1. ptr128
            21.03.2024 20:57

            В PostgreSQL не сработает, если типы / количество параметров у функции / процедуры поменялись. Придётся дропать.

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

            А это тоже боль, так как зачастую другие объекты от них зависят (другие функции / процедуры, представления), поэтому дропать надо каскадно, а потом как-то восстанавливать дропнутое обратно.

            Навскидку могу вспомнить только зависимость триггера от его функции. Но в этом случае нет ни параметров ни их типов, а значит и нет проблем с CREATE OR REPLACE.

            Можете более развернуто объяснить о каких зависимостях Вы ведете речь?

            P.S. Ах да, еще VIEW. Но там аналогичная ситуация, так как тип и количество параметров используемых функций не изменить, не пересоздав VIEW.


            1. miruzzy Автор
              21.03.2024 20:57
              +1

              Представления зависят, при дропе надо будет пересоздавать вьюхи


              1. ptr128
                21.03.2024 20:57

                Я вспомнил и указал выше. Но если VIEW не меняется, то не меняется так же тип и количество параметров функций, которые она вызывает.


                1. miruzzy Автор
                  21.03.2024 20:57

                  не не, всё дело в том, что VIEW у PG хранится уже как скомпиленная ( разобран SQL-запрос в свой формат ).
                  Т.е. если иначе представить - ПГ во вьюхе стучится не по названию и входных параметрам, а по oid функции ( удалили функцию, потеряли oid )


                  1. ptr128
                    21.03.2024 20:57

                    При CREATE OR REPLACE FUNCTION oid уже существующей такой функции не меняется. А удалить функцию, используемую во VIEW, PostgreSQL не даст.


            1. Mingun
              21.03.2024 20:57

              Можете более развернуто объяснить о каких зависимостях Вы ведете речь?

              Вроде понятно написал, что от функции может зависеть другая функция (внутри вызывает нашу) и/или представление (внутри представления стоит вызов функции). Меняете тип параметра или даже просто добавляете новый — это создаст вам новую функцию, а не заменит старую. Для именно замены старую придётся удалять через drop function, а если она используется в представлении, то просто так PostgreSQl вам удалить её не даст. Удалите сначала представление, а потом свою функцию. И всё это может по рекурсии как снежный ком накапливаться. Ну или cascade пропишите у дропа, но тогда вы даже не узнаете, сколько системы у вас улетит в трубу при дропе.


              1. ptr128
                21.03.2024 20:57

                Вроде понятно написал, что от функции может зависеть другая функция (внутри вызывает нашу)

                Впервые слышу о такой зависимости. С перепугу даже проверил:

                CREATE OR REPLACE FUNCTION _tst_0(i integer)
                RETURNS integer AS $func$ <<func>> BEGIN
                  RETURN _tst_1(i);
                END; $func$ LANGUAGE plpgsql;
                
                CREATE OR REPLACE FUNCTION _tst_1(i integer)
                RETURNS integer AS $func$ <<func>> BEGIN
                  RETURN i + 1;
                END; $func$ LANGUAGE plpgsql;
                
                DROP FUNCTION IF EXISTS _tst_1;
                DROP FUNCTION IF EXISTS _tst_0;

                Пожалуйста, приведите пример такой зависимости.

                Меняете тип параметра или даже просто добавляете новый — это создаст вам новую функцию, а не заменит старую.

                Это и есть полиморфизм, который широко используется.

                Для именно замены старую придётся удалять через drop function, а если она используется в представлении, то просто так PostgreSQl вам удалить её не даст.

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

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

                И всё это может по рекурсии как снежный ком накапливаться.

                Только если нет архитектора на проекте. Я и так создавать VIEW позволяю очень неохотно и только ради внешних систем, которые не могут при сборке подхватить SQL запрос из отдельного файла. А уж PR с VIEW на VIEW точно не пропущу.


                1. Mingun
                  21.03.2024 20:57

                  Пожалуйста, приведите пример такой зависимости.

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

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

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

                  А вот удалять старую функцию совершенно не обязательно.

                  Функция внутренняя, извне её никто не вызывает. Она сугубо наша. Не вижу ни одной причины, почему я её должен оставлять, если она никем не используется, а вот пониманию мешает весьма сильно (а ещё её по ошибке может кто-то из разработчиков начать использовать вместо правильной).

                  А уж PR с VIEW на VIEW точно не пропущу.

                  В нашей системе представление на представлении — норма жизни, поэтому без этого никак.


                  1. ptr128
                    21.03.2024 20:57

                    К сожалению, иногда PostgreSQL ругается на функции, используемые в других функциях, а иногда нет.

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

                    CREATE OR REPLACE FUNCTION _tst_0(i integer)
                    RETURNS integer AS $func$ <<func>> BEGIN
                      SELECT some_field FROM this_table_not_exist;
                    END; $func$ LANGUAGE plpgsql;

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

                    Это очень опасно из-за полиморфизма. Совершенно в неожиданный момент можете получить SQL Error [42725]: ERROR: function ... is not unique. В документации сказано "If there are two or more such functions in the same schema with identical parameter types in the non-defaulted positions (which is possible if they have different sets of defaultable parameters), the system will not be able to determine which to prefer". Во избежании таких проблем вызов функций без указаний всех параметров в проектах категорически запрещается.

                    Функция внутренняя, извне её никто не вызывает.

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

                    её по ошибке может кто-то из разработчиков начать использовать вместо правильной

                    Это сразу же вылезет при контроле метаданных созданием пустой БД и последующей проверкой линтером.

                    В нашей системе представление на представлении — норма жизни

                    Сколько раз я таким не сталкивался, всегда это приводило к весьма продолжительным и тяжелым оптимизациям. Проблема в том, что когда используются VIEW, разобраться в плане запроса становится затруднительно. А если еще VIEW на VIEW - вообще невозможно без ручного их разворачивания. При этом разработчик, когда пишет запрос, даже не видит, что VIEW может соединять целый ряд не нужных в данном случае таблиц. А лишние INNER JOIN ведут к заметной деградации производительности из-за излишних проверок на наличие записей в связываемых таблицах, даже если связывание выполняется по внешнему ключу. Да и компиляция запроса с VIEW всегда дольше, чем без него.

                    без этого никак.

                    Почему никак? Просто не утверждаются PR с запросами использующими VIEW. Пользуйтесь макроопределениями вместо них. Тогда в диагностике сразу будет виден весь проблемный запрос и его не надо будет вручную разворачивать для анализа.

                    Не вижу ни одного преимущества в использовании VIEW, кроме как для внешних систем.


                    1. Mingun
                      21.03.2024 20:57

                      Это очень опасно из-за полиморфизма. Совершенно в неожиданный момент можете получить SQL Error [42725]: ERROR: function ... is not unique.

                      Так я же об этом как раз и говорю. Я хотел просто добавить параметр в функцию, а вместо этого молча создалась новая (несмотря на фразу or replace — то есть, я явно выразил намерение заменить функцию). И я об этом узнаю только тогда, когда где-то запрос упадёт. Очень не интуитивно это.

                      Во избежании таких проблем вызов функций без указаний всех параметров в проектах категорически запрещается.

                      Это требование делает параметры по умолчанию совершенно бесполезными.

                      В таких редких и частных случаях никто не мешает её действительно убить в скрипте её же создания.

                      Я же говорю, что мешает. PostgreSQL не даст мне просто сделать drop function и создать новую, если она в представлении используется. Придётся сначала запомнить код представления, сделать drop view, сделать drop function, сделать create function, сделать create view по сохранённому ранее DDL. Причём сохранить DDL надо всех объектов рекурсивно, так как это представление может использоваться в другом представлении, а то — в следующем. Это решаемая задача, но огромный геморрой. А удалять функцию каскадно нельзя, потому что кто потом восстановит удалённые по каскаду представления?

                      Пользуйтесь макроопределениями вместо них.

                      Что ещё за макроопределения, впервые слышу о таком?

                      Не вижу ни одного преимущества в использовании VIEW, кроме как для внешних систем.

                      Дело в том, что система потихоньку мигрирует с Oracle, а там представления играют существенную роль. И нельзя просто так от них избавиться.


                      1. miruzzy Автор
                        21.03.2024 20:57

                         то есть, я явно выразил намерение заменить функцию

                        Ну так функция уйдёт под замену, если выполняется 3 условия:

                        1. Совпадает название

                        2. Совпадают типы входных параметров

                        3. Совпадают типы выходных параметров

                        В остальных случаях будет создание новой функции

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

                        Ну так а для чего предлагается хранение в гите ?
                        Как раз для того, чтобы вы точно видели, что у вас меняется в структуре


                      1. Mingun
                        21.03.2024 20:57

                        А гит тут причём? Понятно, что всё лежит в системе контроля версий, но мне же накатить изменение надо, не уронив прод, как это делать? Простой create or replace не работает.


                      1. miruzzy Автор
                        21.03.2024 20:57

                        Ну генерировать скрипты на удаление и создание.
                        Как вариант с другой стороны - не допускать такой архитектуры в системе ( ну или как-то абстрагировать структуры )


                      1. ptr128
                        21.03.2024 20:57

                        Это требование делает параметры по умолчанию совершенно бесполезными.

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

                        Придётся сначала запомнить код представления, сделать drop view, сделать drop function, сделать create function, сделать create view по сохранённому ранее DDL.

                        Исходим из того, что мы отказались от параметров по умолчанию в пользу полиморфизма. Тогда, если можно было сделать CREATE OR ALTER FUNCTION, то не зачем было убивать VIEW. В противном случае убивать VIEW всё равно необходимо, а после пересоздания функции создать VIEW "по сохранённому ранее DDL" не получится, так как функция будет уже другой. Как видим, если отказаться от параметров по умолчанию, получаем не только полиморфизм, но и одновременно решение и этой проблемы.

                        Что ещё за макроопределения, впервые слышу о таком?

                        Любой исходный код в процессе сборки перед разворачиванием может быть обработан макропроцессором. Я даже тут в комментариях приводил простейший пример. Одновременно это решает проблему формирования единого скрипта разворачивания БД для psql.

                        Мы пользуемся C препроцессором. Я бы предпочел, конечно, m4, но приходится учитывать уровень знаний разработчиков, в большинстве своем с m4 не знакомых.

                        Дело в том, что система потихоньку мигрирует с Oracle, а там представления играют существенную роль. И нельзя просто так от них избавиться.

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

                        Попробую показать на примере, к чему это приводит:

                        CREATE TABLE _tmp_test0 (
                          id int PRIMARY KEY,
                          val varchar NULL
                        );
                        
                        CREATE TABLE _tmp_test1 (
                          id int PRIMARY KEY,
                          ref_id int CONSTRAINT FK_tmp_test1_ref_id REFERENCES _tmp_test0(id),
                          val varchar NULL
                        );
                        
                        CREATE VIEW _tmp_test_vw AS
                        SELECT T.id, T.ref_id, T.val, R.val AS r_val
                        FROM _tmp_test1 T
                        JOIN _tmp_test0 R ON R.id=T.ref_id;
                        
                        INSERT INTO _tmp_test0 (id, val)
                        SELECT G.n, (G.n/100)::text
                        FROM generate_series(0,9999) G(n);
                        
                        INSERT INTO _tmp_test1 (id, ref_id, val)
                        SELECT G.n, G.n/100, (G.n/1000)::text
                        FROM generate_series(0,999999) G(n);
                        

                        Теперь делаем запрос из VIEW, в SELECT, списке которого нет поля r_val:

                        EXPLAIN ANALYZE
                        SELECT val
                        FROM _tmp_test_vw
                        WHERE id BETWEEN 100000 AND 500000;
                        
                        Hash Join  (cost=308.71..12721.15 rows=399695 width=3) (actual time=1.592..104.212 rows=400001 loops=1)
                          Hash Cond: (t.ref_id = r.id)
                          ->  Index Scan using _tmp_test1_pkey on _tmp_test1 t  (cost=0.42..11363.23 rows=399695 width=7) (actual time=0.021..50.201 rows=400001 loops=1)
                                Index Cond: ((id >= 100000) AND (id <= 500000))
                          ->  Hash  (cost=183.28..183.28 rows=10000 width=4) (actual time=1.553..1.555 rows=10000 loops=1)
                                Buckets: 16384  Batches: 1  Memory Usage: 480kB
                                ->  Index Only Scan using _tmp_test0_pkey on _tmp_test0 r  (cost=0.29..183.28 rows=10000 width=4) (actual time=0.010..0.822 rows=10000 loops=1)
                                      Heap Fetches: 0
                        Planning Time: 0.208 ms
                        Execution Time: 114.185 ms

                        Как видим, он всё равно полез в _tmp_test0. А если сделать запрос непосредственно по _tmp_test1, то этого ожидаемо не происходит:

                        EXPLAIN ANALYZE
                        SELECT val
                        FROM _tmp_test0
                        WHERE id BETWEEN 100000 AND 500000;
                        
                        Index Scan using _tmp_test0_pkey on _tmp_test0  (cost=0.29..2.50 rows=1 width=2) (actual time=0.003..0.003 rows=0 loops=1)
                          Index Cond: ((id >= 100000) AND (id <= 500000))
                        Planning Time: 0.117 ms
                        Execution Time: 0.018 ms

                        И эти три порядка разницы в производительности на продуктивной системе ощущаются очень болезненно. Контролировать это на уровне PR сложно и трудоёмко, а сами разработчики на такое редко обращают внимание из-за обманчивого "Index Only Scan". Отсюда и моя позиция категорического запрета на VIEW внутреннего использования.


                  1. miruzzy Автор
                    21.03.2024 20:57

                    К сожалению, иногда PostgreSQL ругается на функции, используемые в других функциях, а иногда нет.

                    Я могу сейчас ошибаться.
                    Но функции на plpgsql у вас кэшируются при первом выполнении ( строится план и кэшируется в локальной памяти процесса )

                    Так вот, как мне кажется, если удалили функцию после кэширования и создали новую - можно получить ошибку ( хотя я такого не встречал, если честно )


              1. miruzzy Автор
                21.03.2024 20:57

                в любом случае, если во время разработки вы дропните что-то - система всё равно делает дамп и показывает на файлах, что случилось ( т.е. в гите в МР вы увидите, что у вас удалилось в итоге )

                Мы пока ещё не дошли до теста удаления вью и 100% не могу сказать, на сколько система сможет полностью восстановиться ( пересоздать вьюшки )

                ЗЫ добавлю, что мы не так часто используем вью. Например, если клиент не гибкий ( например даталенз ).


      1. IVNSTN
        21.03.2024 20:57
        +2

        вы не верно интерпретировали слово "миграция"

        Вы сами всех запутали и себя в первую очередь. Миграциями называется только то, что вы называете изобретенным термином "добивочные миграции".

        Вариантов организации исходников БД и построения Continuous Delivery два:

        • вы храните только миграции (alter table add, alter trigger) и конечное состояние понять можно только прогнав все миграции что есть; это удобно при работе через ORM, когда нет хранимок и триггеров, а таблицы определены классами в ООП

        • вы храните определения объектов, а скрипт миграции для деплоя генерируется утилитой вроде liquibase налету и нигде не хранится; этот вариант удобен, когда у вас полноценная разработка на стороне БД: в GIT лежат финальные версии определений таблиц и хранимок, при возникновении конфликта по изменениям, он, во-первых, очевиден, во-вторых решается штатным образом как с нормальными исходниками

        Если утилита не умеет в alter table, то вариантов нет и нужно для таблиц (дополнительно к определению) создавать и хранить файлы миграций. Для функций у вас хранятся не миграции, а определения (definition), миграция возникает налету, когда liquibase понимает, что файл с CREATE FUNCTION изменился и генерирует ALTER FUNCTION.


        1. ptr128
          21.03.2024 20:57

          На самом деле есть третий вариант, когда в отдельные скрипты миграции выделяются лишь трансформации данных из одного множества таблиц в другое. А остальные миграции ведутся в исходном файле таблицы, начинающегося именно с определения таблицы как CREATE TABLE IF NOT EXISTS. При этом допустимо поддерживать и финальное описание таблицы так как миграции вида ALTER TABLE IF EXISTS SQL_DB_INSTANCE.OBJECT_NAME ADD COLUMN IF NOT EXISTS отрабатывают только по необходимости, а более сложные проверки можно заранее упростить макросами и функциями/процедурами.

          liquibase понимает, что файл с CREATE FUNCTION изменился и генерирует ALTER FUNCTION

          А кто мешает написать сразу CREATE OR REPLACE FUNCTION? PostgreSQL все равно не будет модифицировать функцию, если код её не изменился. Но эта проверка произойдет намного быстрее, чем средствами liquibase.


        1. vlad4kr7
          21.03.2024 20:57

          В добавок к этому есть версионность миграций И типы файлов - mutable (R) / immutable (V).


  1. Protos
    21.03.2024 20:57

    Какие вы рекомендуете валидационные проверки запускать в мерж регвесте, без выполнения которых разработчик не сможет закрыть мерж регвест?


    1. miruzzy Автор
      21.03.2024 20:57

      Уточните, пожалуйста, вы имеете в виду проверку именно правильности выполнения миграций или вообще проверки кода, не связанные с тематикой статьи ?


      1. Protos
        21.03.2024 20:57

        Конечно про проверки правильность миграции, кода, чтобы отловить ошибки, которые могут возникнуть при обновлении бд


        1. miruzzy Автор
          21.03.2024 20:57

          Пока-что для проверки можно взять функционал из раздела генерации "добивочных" миграций, а именно ( первые 2 пункта взяты):
          1) Возьмём клон dev-базы
          2) Накатим туда весь МР
          3.1) Меньше рисков: сравним с фич-БД, путём сравнивания именно генерации файлов ( обработанный выхлоп pg_dump)
          3.2) Альтернативный вариант, но больше рисков: сравнение сделать не через структуру файлов, а через диф-утилиту

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