Pull to refresh

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

Level of difficultyMedium

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

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 записи.

Tags:
Hubs:
You can’t comment this publication because its author is not yet a full member of the community. You will be able to contact the author only after he or she has been invited by someone in the community. Until then, author’s username will be hidden by an alias.