Хочу поделиться решением проблемы «duplicate key value violates unique constraint «pg_class_relname_nsp_index»» в PostgreSQL, с которой я столкнулся при добавлении новых секций в секционированные таблицы, с использованием распараллеливания.
Суть проблемы заключается в дублировании имен для индексов секций, автоматически генерируемых сервером Postgres в одновременно исполняемых пользовательских параллельных процессах, для исходных двух и более секционированных таблиц, независимых друг от друга, но имеющих длинные имена, с совпадающей, в значительной мере, начальной частью.
Первоначальной моей реакцией на возникшую ситуацию, была простая попытка заменить слишком длинные имена таблиц на более короткие. А этот exception я посчитал внутренним багом Postgres. Но далее, аналогичная ситуация возникала вновь и вновь, с другими парами, более коротких имен, длиной около 30 символов (а это в два с лишним раза меньше текущего ограничения Postgres в 63 символа). Причем ошибка имела нестабильный характер: на тестовом стенде можно было получить удачную сборку, а при дальнейшем развертывании на продуктивном стенде сборка оказывалась неудачной. Можно, конечно, рассуждать о том, что незачем использовать такие длинные имена — что можно принять условное внутрипроектное соглашение о непревышении какого-то выбранного порога для имен. Но 30 символов — это мало. В свое время, в Oracle (в теперь уже старых версиях), ограничение в 30 символов, меня лично, угнетало. В ряде случаев, достаточно длинные имена не ухудшают читабельность проекта, а улучшают ее, внося большую информативность, особенно, в реальных базах данных с большим количеством сущностей, в которых непросто ориентироваться (тем более, по прошествии длительного времени). Кроме того, я считаю, что подбор приемлемого сокращения для технического имени часто оказывается более сложным занятием, чем простой выбор расширенного наименования. И, в конце концов, выбирая наименования для создаваемых таблиц, мы закладываем основу для последующего зависимого функционала приложения — очевидно, что замена наименований — это болезненная и нежелательная процедура. Т.о. мотивации для поиска решения проблемы у меня было достаточно.
Разберем ситуацию подробно, с воспроизведением ошибки на тестовом примере. Насколько я понял, воспроизвести ошибку легче, если специально выбрать имена подлиннее, и, главное, чтобы значительная передняя часть этих имен была одинаковой (иначе, наверное, вероятность воспроизведения повышается с повышением количества создаваемых секций и/или количества параллельных сессий).
Для начала, посмотрим, что это за unique constraint:
На то, что дублирование возникает в именах индексов это пока явно не указывает — это же уникальное ограничение может сработать и при регистрации в pg_catalog.pg_class других объектов БД (в первую очередь, таблиц):
select relkind, * from pg_catalog.pg_class where not (relkind = any('{i, I}'::char[]))
В обсуждениях встречается похожая ошибка, с которой можно спутать нашу: duplicate key value violates unique constraint «pg_type_typname_nsp_index», — она, как раз, связана с дублированием в именах создаваемых таблиц, и также связана с параллельным исполнением. Она возникает на другом уровне — при регистрации в системном каталоге типа для создаваемой таблицы:
Мне представляется, что попытка регистрации одних и тех же таблиц в параллельных сессиях — это явно не проблема уровня СУБД — это изначально «неправильное» распараллеливание инструкций DDL со стороны пользователя (или со стороны того middleware, которое пользователь использует). В нашем же случае, на первый взгляд, распараллеливание «правильное» — по исходным условиям, таблицы разные, физически друг от друга не зависят, и могут создаваться в независимых параллельных сессиях.
Итак, запланируем создание секционированных таблиц со следующими именами:
-
the_table_with_a_really_long_descriptive_name_1
-
the_table_with_a_really_long_descriptive_name_2
-
the_table_with_a_really_long_descriptive_name_3
В каждую таблицу будет добавлен индекс:
create table the_table_with_a_really_long_descriptive_name_1 ( key_field integer , some_attribute integer ) partition by list(key_field) ; create index i_the_table_with_a_really_long_descriptive_name_1$some_attribute on the_table_with_a_really_long_descriptive_name_1 ( some_attribute ) ;
Как известно, на основе такого индекса, добавляемого в родительскую логическую таблицу, производится автоматическая генерация индексов в добавляемых секциях. Именно в рамках этого процесса и возникает наша проблема.
Составим тестовый shell-скрипт с psql, в котором наши таблицы будут создаваться в цикле, с добавлением трех секций в каждую таблицу. Для сравнения, добавим также генерацию секционированных таблиц с более короткими именами. В конце скрипта выведем из системного каталога перечень созданных индексов.
#!/bin/bash pg_connection_string="postgresql://postgres@localhost:5432/postgres" function create_partitioned_table() { echo " \set ON_ERROR_STOP true \ drop table if exists the_table_with_a_short_name_$1 ; create table the_table_with_a_short_name_$1 ( key_field integer , some_attribute integer ) partition by list(key_field) ; create index i_the_table_with_a_short_name_$1$some_attribute on the_table_with_a_short_name_$1 ( some_attribute ) ; \ do \$plsql\$ begin for i in 1..3 loop execute format( \$dml\$ create table the_table_with_a_short_name_$1\$partition%s partition of the_table_with_a_short_name_$1 for values in (%s) \$dml\$ , i , i ) ; end loop ; end \$plsql\$ ; \ drop table if exists the_table_with_a_really_long_descriptive_name_$1 ; create table the_table_with_a_really_long_descriptive_name_$1 ( key_field integer , some_attribute integer ) partition by list(key_field) ; create index i_the_table_with_a_really_long_descriptive_name_$1$some_attribute on the_table_with_a_really_long_descriptive_name_$1 ( some_attribute ) ; \ do \$plsql\$ begin for i in 1..3 loop execute format( \$dml\$ create table the_table_with_a_really_long_descriptive_name_$1\$partition%s partition of the_table_with_a_really_long_descriptive_name_$1 for values in (%s) \$dml\$ , i , i ) ; end loop ; end \$plsql\$ ; " | psql \ --dbname=$pg_connection_string \ --quiet sleep 1 } execute_in_parallel="$1" # specify "true" to execute in parallel for i in {1..3} do if [[ $execute_in_parallel = "true" ]]; then create_partitioned_table $i & # execution in the background else create_partitioned_table $i fi done wait psql \ --dbname=$pg_connection_string \ --command="\di" echo "All done"
Первый запуск, с последовательным исполнением, показывает результат автоматической генерации со стороны Postgres: у имен физических индексов для секций, составляемых на основе имени логического секционированного индекса и имени секции, отбрасывается префикс «i_», и, при превышении лимита по длине (63 символа), вырезается некая внутренняя часть (где-то, в конце второй трети), а также добавляется постфикс «_idx[N]».
При запуске с параллельным исполнением (для этого предусмотрен параметр), получаем искомую ошибку (номера таблиц могут быть каждый раз разными, т.к. порядок асинхронного исполнения непредсказуем):
Как видно, проблема возникает при составлении имен для внутренних объектов, которые, в итоге, не укладываются в действующее ограничение по длине; пользователь при этом составляет наименования объектов в рамках действующего ограничения и ничего не нарушает. В то же время, exception не возникает из-за какого-то бага Postgres — при последовательном исполнении проблема не возникает, а при распараллеливании требуется необходимое упорядочивание для инструкций по добавлению секций в таблицы с такими проблемными именами — т.е. другого решения, которое можно было бы реализовать внутри СУБД, думаю, нет. Еще стоит отметить, что проблема не возникает при параллельном добавлении секций в одну таблицу — проблема именно с независимыми друг от друга двумя и более таблицами, — т.к. автогенерация индексов для секций происходит в рамках единой для пользователя инструкции по добавлению секции (create table … partition of … или alter table … attach partition …), которая требует эксклюзивной блокировки головной таблицы и, соответственно, появления дубликатов не происходит.
В основе выработанного решения данной проблемы лежит идея использования т.н. логической зависимости, имеющей отношение к необходимости упорядоченной обработки таблиц с проблемными именами. Следующий запрос, локализованный в представлении для дальнейшего использования, позволяет получить перечень таких логических зависимостей:
create or replace view v_orderliness_logical_dependency as with partitioned_table as ( select n.nspname as schema_name , t.relname as table_name , t.oid as table_id from pg_catalog.pg_class t join pg_catalog.pg_namespace n on n.oid = t.relnamespace where t.relkind = 'p'::"char" ) , name_length_limit as ( select s.setting::integer / 2 - 2 as max_len from pg_catalog.pg_settings s where s.name = 'max_identifier_length' ) , orderliness_logical_dependency as ( select t.schema_name , t.group_name , array_agg(t.table_id order by t.table_name) as table_seq from ( select t.schema_name , t.table_name , t.table_id , left(t.table_name, n.max_len) as group_name from partitioned_table t cross join name_length_limit n ) t group by t.schema_name , t.group_name having count(*) > 1 ) , dependency_group_item as ( select dep.schema_name , dep.group_name , t.table_id , pt.table_name , t.ordinal_num from orderliness_logical_dependency dep cross join lateral unnest(dep.table_seq) with ordinality as t(table_id, ordinal_num) join partitioned_table pt on pt.table_id = t.table_id ) select forward_table.schema_name , forward_table.table_name as forward_table_name , forward_table.table_id as forward_table_id , following_table.table_name as following_table_name , following_table.table_id as following_table_id from dependency_group_item forward_table join dependency_group_item following_table on following_table.schema_name = forward_table.schema_name and following_table.group_name = forward_table.group_name and following_table.ordinal_num > forward_table.ordinal_num ;
Подбираются пары секционированных таблиц, имена которых совпадают по первым 29 символам. Далее, в каждой такой паре, на основе сортировки по имени, первая таблица условно определяется как главная, вторая — как зависимая.
Для наших трех таблиц мы получили полный набор логических связей: вторая и третья таблицы зависят от первой, третья зависит от второй.
Используя полученные логические зависимости, создадим конечное представление, с распределением секционированных таблиц по т.н. «цепочкам обработки».
create or replace view v_partitioned_table_processing_chain as with recursive table_dependency as ( select forward_table_id as primary_table_id , following_table_id as dependent_table_id from v_orderliness_logical_dependency ) , selected_table as ( select n.nspname as schema_name , t.relname as table_name , t.oid as table_id from pg_catalog.pg_class t join pg_catalog.pg_namespace n on n.oid = t.relnamespace where t.relkind = 'p'::"char" ) , target_table as ( select t.table_id , dep.primary_table_id , case when dep.primary_table_id is not null then 1 else 0 end as dep_level from selected_table t left join table_dependency dep on dep.dependent_table_id = t.table_id union all select t.table_id , dep.primary_table_id , t.dep_level + 1 as dep_level from target_table t join table_dependency dep on dep.dependent_table_id = t.primary_table_id ) select t.schema_name , t.table_name , t.table_id , t.dep_level , coalesce( t.primary_table_id , t.table_id ) as safe_parallel_processing_chain_id from ( select st.schema_name , st.table_name , t.table_id , t.dep_level , t.primary_table_id , row_number() over ( partition by t.table_id order by t.dep_level desc ) as ordinal_number from target_table t join selected_table st on st.table_id = t.table_id ) t where t.ordinal_number = 1 order by dep_level , table_name ;
Вывод данных из этого представления показывает, что исходные таблицы распределились по четырем «цепочкам обработки»: три проблемные таблицы объединились в одну общую цепочку, которая должна обрабатываться в рамках одного потока; а каждая из оставшихся таблиц с короткими именами получила свою индивидуальную цепочку, каждая из которых будет обрабатываться отдельно.
Модифицируем первоначальный тестовый shell-скрипт, внедрив в него новую логику распараллеливания:
-
На первом этапе будет производиться создание трех секционированных таблиц, в однопоточном режиме;
-
На втором этапе, в параллельном режиме, будет производиться генерация секций — корректное распараллеливание теперь будет обеспечиваться распределением на основе получаемого перечня логических зависимостей.
#!/bin/bash pg_connection_string="postgresql://postgres@localhost:5432/postgres" function create_partitioned_table() { echo " \set ON_ERROR_STOP true \ drop table if exists the_table_with_a_short_name_$1 ; create table the_table_with_a_short_name_$1 ( key_field integer , some_attribute integer ) partition by list(key_field) ; create index i_the_table_with_a_short_name_$1$some_attribute on the_table_with_a_short_name_$1 ( some_attribute ) ; \ drop table if exists the_table_with_a_really_long_descriptive_name_$1 ; create table the_table_with_a_really_long_descriptive_name_$1 ( key_field integer , some_attribute integer ) partition by list(key_field) ; create index i_the_table_with_a_really_long_descriptive_name_$1$some_attribute on the_table_with_a_really_long_descriptive_name_$1 ( some_attribute ) ; " | psql \ --dbname=$pg_connection_string \ --quiet sleep 1 } function create_table_partitions() { echo " \set ON_ERROR_STOP true \ do \$plsql\$ declare l_table_rec record; begin for l_table_rec in ( select table_name from v_partitioned_table_processing_chain where safe_parallel_processing_chain_id = $1 order by dep_level ) loop <<partitions_creation>> for i in 1..3 loop execute format( \$dml\$ create table %s\$partition%s partition of %s for values in (%s) \$dml\$ , l_table_rec.table_name , i , l_table_rec.table_name , i ) ; end loop ; end loop ; end \$plsql\$ ; " | psql \ --dbname=$pg_connection_string \ --quiet sleep 1 } execute_in_parallel="$1" # specify "true" to execute in parallel for i in {1..3} do create_partitioned_table $i done temp_dir="$(mktemp -d)" processing_chains="$temp_dir/processing_chains" echo " select distinct safe_parallel_processing_chain_id from v_partitioned_table_processing_chain " | \ psql $pg_connection_string \ --tuples-only \ --no-align \ --field-separator=$'\t' \ > $processing_chains while IFS=$'\n\t' read -r safe_parallel_processing_chain_id; do if [[ $execute_in_parallel = "true" ]]; then create_table_partitions $safe_parallel_processing_chain_id & # execution in the background else create_table_partitions $safe_parallel_processing_chain_id fi done < $processing_chains wait rm -r "$temp_dir" psql \ --dbname=$pg_connection_string \ --command="\di" echo "All done"
В итоге, при запуске в параллельном режиме, ошибка больше не возникает, и мы получаем корректный результат:
ссылка на оригинал статьи https://habr.com/ru/articles/872990/
Добавить комментарий