При попытке прочитать таблицу получил ошибку о переполнении счетчика транзакций:
select * from attachments;
получал ошибку о переполнении счетчика транзакций MultiXactId:
SQL Error [XX000]: ERROR: MultiXactId 3183984 has not been created yet
-- apparent wraparound
Выполнение VACUUM FREEZE или VACUUM FULL привело к ошибке:
found xmax 25973 from before relfrozenxid 106464494
попробовал прочитать одну запись:
select * from attachments limit 1;
Одну запись удалось прочитать, таким образом постепенно увеличивая значения limit прочитал 400 031 записей. Сохранил их в отдельной таблице, и удалил из исходной.
Удалил индекс primary key. Попробовал создать заново primary key, получил новую ошибку:
SQL Error [XX001]: ERROR: failed to find parent tuple for heap-only tuple at (33745,2)
in table "attachments
Это натолкнуло на мысль попробовать прочитать данные по ctid. Данные смог прочитать, значит можно пробовать прочитать и остальные данные из таблицы. Первое значение в ctid до запятой легко может принимать значение до 2 миллиардов в 32-х битной системе, второй значение номер в блоке значение от 0 до 255.Значит по этим значениям можно прогнать всю таблицу во вложенном цикле.
Создал простые таблицы tmp_at_ok, tmp_at_error и с одним полем MCTID:text куда буду собирать все ctid записи по которым удалось прочитать записи, и вторую таблицу куда будут помещены ctid которые не удалось прочитать.
DO
$$
DECLARE
block_num INT;
row_num INT;
max_block_num CONSTANT INT := 2147483647;
max_row_num CONSTANT INT := 255;
ctid_value TEXT;
commit_counter INT := 0;
err_message TEXT;
err_detail TEXT;
err_context TEXT;
last_check TIMESTAMP := CLOCK_TIMESTAMP();
query_text TEXT;
BEGIN
FOR block_num IN 0..max_block_num LOOP
FOR row_num IN 0..max_row_num LOOP
ctid_value := FORMAT('(%s,%s)', block_num, row_num);
IF EXTRACT(EPOCH FROM (CLOCK_TIMESTAMP() - last_check)) > 60 THEN
RAISE NOTICE 'Текущий ctid: %', ctid_value;
last_check := CLOCK_TIMESTAMP();
END IF;
query_text := FORMAT(
'INSERT INTO tmp_at_ok (mctid) '
'SELECT ctid FROM attachments2 WHERE ctid = ''%s'';',
ctid_value
);
BEGIN
EXECUTE query_text;
commit_counter := commit_counter + 1;
EXCEPTION
WHEN OTHERS THEN
GET STACKED DIAGNOSTICS
err_message = MESSAGE_TEXT,
err_detail = PG_EXCEPTION_DETAIL,
err_context = PG_EXCEPTION_CONTEXT;
RAISE NOTICE 'Ошибка при обработке ctid: %, сообщение: %, подробности: %, контекст: %',
ctid_value, err_message, err_detail, err_context;
query_text := FORMAT(
'INSERT INTO tmp_at_error (mctid) VALUES (%L);',
ctid_value
);
EXECUTE query_text;
CONTINUE;
END;
IF commit_counter >= 1000 THEN
COMMIT;
commit_counter := 0;
END IF;
END LOOP;
END LOOP;
COMMIT;
END
$$;
Создал копию оригинальной таблицы, обозвал attachments_restore, и по ID сделал ключевым полем
Запустил еще один цикл уже по собранным ctid, и бинго, при вставке одной из записей получил ошибку:
duplicate key value violates unique constraint "attachments_restore_pkey", подробности:
Key (id)=(3248368) already exists.
код второго цикла:
DO
$$
DECLARE
record RECORD;
total_records INT;
start_time TIMESTAMP;
begin_time TIMESTAMP;
end_time TIMESTAMP;
curr_timestamp TIMESTAMP;
counter INT := 0;
cur_batch INT := 5000;
declare sum_lag INT;
last_output_time TIMESTAMP := NULL;
diff INTERVAL;
hours_i INT;
minutes_i INT;
seconds_i INT;
formatted_diff TEXT;
err_message TEXT;
err_detail TEXT;
err_context TEXT;
last_check TIMESTAMP := CLOCK_TIMESTAMP();
query_text TEXT;
ctid_value TEXT;
BEGIN
start_time := clock_timestamp();
begin_time := clock_timestamp();
truncate table attachments_restore;
truncate table tmp_at_insert_error;
commit;
RAISE NOTICE 'Старт : %', begin_time;
select count(*)INTO total_records from tmp_at_ok;
RAISE NOTICE 'Всего записей для обработки: %', total_records;
FOR record IN
select mctid from tmp_at_ok
LOOP
insert into tmp_at_insert_error (mctid) values(record.mctid) ;
query_text := FORMAT(
'INSERT INTO public.attachments_restore(id, container_id, container_type, filename, disk_filename, filesize, content_type, digest, downloads, author_id, created_on, description, disk_directory, us_group_id)'
'select id, container_id, container_type, filename, disk_filename, filesize, content_type, digest, downloads, author_id, created_on, description, disk_directory, us_group_id '
'from public.attachments2 WHERE ctid = ''%s'';',
record.mctid
);
update tmp_at_insert_error
set ok = 'ok'
where mctid = record.mctid;
BEGIN
ctid_value := record.mctid;
EXECUTE query_text;
EXCEPTION
WHEN OTHERS THEN
GET STACKED DIAGNOSTICS
err_message = MESSAGE_TEXT,
err_detail = PG_EXCEPTION_DETAIL,
err_context = PG_EXCEPTION_CONTEXT;
RAISE NOTICE 'Ошибка при обработке ctid: %, сообщение: %, подробности: %, контекст: %',
ctid_value, err_message, err_detail, err_context;
query_text := FORMAT(
'INSERT INTO tmp_at_error (mctid, type) VALUES (%L , ''i'');',
ctid_value
);
EXECUTE query_text;
CONTINUE;
END;
counter := counter + 1;
IF counter % cur_batch = 0 THEN
start_time := clock_timestamp();
COMMIT;
curr_timestamp := clock_timestamp();
IF last_output_time IS NULL OR curr_timestamp - last_output_time > INTERVAL '120 seconds' THEN
RAISE NOTICE 'Фикс % записей. Время: %', counter, start_time;
last_output_time := curr_timestamp;
END IF;
END IF;
END LOOP;
COMMIT;
end_time := clock_timestamp();
diff := end_time - begin_time;
hours_i := EXTRACT(HOUR FROM diff);
minutes_i := EXTRACT(MINUTE FROM diff);
seconds_i := EXTRACT(SECOND FROM diff);
formatted_diff := LPAD(hours_i::TEXT, 2, '0') || ':' || LPAD(minutes_i::TEXT, 2, '0') || ':' || LPAD(seconds_i::TEXT, 2, '0');
RAISE NOTICE 'Всего % записей. Время выполнения: %', counter, formatted_diff;
END
$$
Сбойный ID известен, прогнал еще раз циклом. нашел второй ctid.

Вот такая интересная ситуация получается, в таблице оказалось две одинаковые ID записи.