Добрый день, коллеги!

Сегодня я хочу поделиться некоторыми проблемами (и решениями) при создании большого количества индексов и ограничений целостности (констрейнтов) одновременно.

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

При развёртывании новой крупной базы данных с «нуля» (например путём миграции), возникает необходимость построить также большое количество индексов в весьма ограниченное тех. окно. Как известно, процесс построения индекса это не только ценный мех IO, но и довольно большое количество CPU при достаточно производительной дисковой подсистеме. Чем больше ядер вы сможете задействовать — тем быстрее пойдёт процесс (в общем случае утверждение, конечно, спорное, но в моём случае обоснованное и проверенное).

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

Итак, случай 1: «Функциональный» индекс

Известно, что Postgres умеет «параллелить» читающую часть создания индекса (и не умеет пишущую). Но в некоторых случаях даже читающая часть может не «параллелиться». Давайте рассмотрим простейший тестовый пример.

Создадим тестовую табличку достаточно большого размера, чтобы успеть поймать эффекты,

create table public.test_fidx AS
select g id, repeat('a',100) f1, repeat('b',300) f2, repeat('cd',500) filler from pg_catalog.generate_series(1, 20000000) g; 

и функцию для использования в индексе. Заметим, что функция простейшая и объявлена как immutable.

CREATE OR REPLACE FUNCTION "public"."ftest"(a text, b text )
 RETURNS text
 IMMUTABLE
 LANGUAGE plpgsql
AS $function$
begin
	return a || '.' || b;
end;
$function$
;

Попробуем построить индекс, используя данную функцию:

create index test_fidx_ix1 on public.test_fidx ("public"."ftest"(f1,f2));

В другой сессии подсмотрим, что происходит:

SELECT
  now() - a.query_start as duration,
  p.phase,
  round(p.blocks_done / 
    nullif(p.blocks_total::numeric,0) 
  * 100, 2) AS "% read done",
  round(p.tuples_done / 
    nullif(p.tuples_total::numeric,0) 
  * 100, 2) AS "% write done",
  a.query,
  p.blocks_total,
  p.blocks_done,
  p.tuples_total,
  p.tuples_done,
  ai.schemaname,
  ai.relname,
  ai.indexrelname
FROM pg_stat_progress_create_index p
left JOIN pg_stat_activity a ON p.pid = a.pid
LEFT JOIN pg_stat_all_indexes ai on ai.relid = p.relid AND ai.indexrelid = p.index_relid
order by 2 desc;

duration       |phase                         |% read done|% write done|query                                                                   |blocks_total|blocks_done
---------------+------------------------------+-----------+------------+------------------------------------------------------------------------+------------+-----------
00:00:11.559213|building index: scanning table|       6.89|            |create index test_fidx_ix1 on public.test_fidx ("public"."ftest"(f1,f2))|     4000000|     275693

Видно, что мы находимся в читающей стадии построения индекса (фаза building index: scanning table)

select 
a.pid,
 now()-query_start as dur,wait_event,state, substring(query, 1,160) AS query  
from pg_stat_activity a where 1=1  
and query like 'create%'
order by  query_start ;

pid    |dur            |wait_event  |state |query                                                                   |
-------+---------------+------------+------+------------------------------------------------------------------------+
1303866|00:00:11.559213|DataFileRead|active|create index test_fidx_ix1 on public.test_fidx ("public"."ftest"(f1,f2))|

И видно, что процесс идёт в один поток. Хотя таблица и получающийся индекс довольно большие для выполнения читающей части параллельно (было проверено на простых индексах)

Видно, что построение индекса НЕ ПАРАЛЛЕЛИТСЯ.

Что же можно сделать?

В синтаксисе CREATE FUNCTION (ссылка) есть клауза PARALLEL { UNSAFE | RESTRICTED | SAFE }

Давайте попробуем поменять определение нашей функции: добавим аналогичную, но с клаузой PARALLEL SAFE.

CREATE OR REPLACE FUNCTION "public"."ftest_p"(a text, b text )
 RETURNS text
 immutable
 parallel SAFE --!!!!
 LANGUAGE plpgsql
AS $function$
begin
	return a || '.' || b;
end;
$function$
;

И опять создадим индекс:

create index test_fidx_ix2 on public.test_fidx ("public"."ftest_p"(f1,f2));

Подсмотрим опять, что происходит (в другой сессии):

pid    |dur            |wait_event|state |query                                                                     |
-------+---------------+----------+------+--------------------------------------------------------------------------+
1618020|00:00:10.489583|          |active|create index test_fidx_ix2 on public.test_fidx ("public"."ftest_p"(f1,f2))|
1618021|00:00:10.489583|          |active|create index test_fidx_ix2 on public.test_fidx ("public"."ftest_p"(f1,f2))|
1618022|00:00:10.489583|          |active|create index test_fidx_ix2 on public.test_fidx ("public"."ftest_p"(f1,f2))|
1618023|00:00:10.489583|          |active|create index test_fidx_ix2 on public.test_fidx ("public"."ftest_p"(f1,f2))|
1303866|00:00:10.489583|          |active|create index test_fidx_ix2 on public.test_fidx ("public"."ftest_p"(f1,f2))|

duration       |phase                         |% read done|% write done|query                                                                     |blocks_total|blocks_done
---------------+------------------------------+-----------+------------+--------------------------------------------------------------------------+------------+-----------
00:00:10.489583|building index: scanning table|      20.06|            |create index test_fidx_ix2 on public.test_fidx ("public"."ftest_p"(f1,f2))|     4000000|     802557

Видно, что в данном случае создание индекса замечательно «параллелится», возникает 4 дочерних читающих процесса + координатор.

Резюме: при создании функций, используемых для создания индексов, не забывайте указывать PARALLEL SAFE (если они, конечно, SAFE).

Продолжение следует...