Как стать автором
Поиск
Написать публикацию
Обновить

Ручное восстановление таблицы в PostgreSQL

Уровень сложностиСредний

При попытке прочитать таблицу получил ошибку о переполнении счетчика транзакций:

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
две записи с одним ID

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

Теги:
Хабы:
Данная статья не подлежит комментированию, поскольку её автор ещё не является полноправным участником сообщества. Вы сможете связаться с автором только после того, как он получит приглашение от кого-либо из участников сообщества. До этого момента его username будет скрыт псевдонимом.