Материалы / Истории / Большая чистка через теневую таблицу
История

Большая чистка через теневую таблицу

Как перенести небольшой объём нужных данных в новую таблицу вместо массового DELETE и какие зависимости проверить перед переключением.

— Макс, привет! Слушай, у меня беда. Пытаюсь почистить нашу log_table, удалить данные за два года, оставить только два последних месяца. Запустил DELETE… уже третий час жду. Можешь посмотреть - что с базой не так?

— Привет, Денис! О, это же классика. Ты не делаешь ничего «не так», просто ты выбрал самый долгий путь.

— В смысле? DELETE же для этого и создан!

— Ага, но не для промышленных масштабов. Представь, что у тебя есть огромная библиотека, и тебе нужно убрать 95% книг. Твой DELETE — это как заставить библиотекаря вычеркивать каждую старую книгу из картотеки. Он будет бегать по стеллажам, находить карточку, ставить штамп «УДАЛЕНО», потом идти к самой книге, вешать на нее табличку… Работа адская, а книги-то физически все еще на полках стоят и занимают место.

— Хм, то есть база данных так же «вычеркивает» строки, но не освобождает место сразу?

— Именно! Она помечает их как удаленные, а место освободится только после «уборки» — VACUUM. Это уже ночная смена клининга, которая придет и унесет помеченные книги в подвал. Но полки-то останутся. Чтобы реально уменьшить библиотеку, понадобится полная перестройка (VACUUM FULL), а это еще дольше и заблокирует всю работу.

— Звучит ужасно. И что делать?

— Есть путь хитрее. Вместо того чтобы выносить старые книги, мы построим рядом новую библиотеку и быстро перевезем туда только те книги, которые нужны. А старую… взорвем.

Миграция через теневую таблицу

Это рабочий, но сложный метод: создать новый объект, перенести нужные данные, догнать изменения и переключить писателей. Он подходит только после репетиции на копии и полного инвентаря зависимостей.

Шаг 1. Инвентаризация и основной объём

Самую большую скорость мы получим, если будем копировать данные в «голую» таблицу.

  • Сохраняем точный DDL и проверяем входящие внешние ключи, представления, правила, триггеры, RLS, права, публикации логической репликации, replica identity, sequence/identity, комментарии и настройки хранения.

  • Создаём log_table_new с нужной структурой. Какие проверки и индексы отложить, решаем явно: «голая» таблица быстрее, но не защищает от ошибочного копирования.

  • Переливаем свежее: Копируем в log_table_new основной массив данных за последние два месяца. Без индексов и проверок это пройдет на порядок быстрее.

Шаг 2. Индексы и гарантированное догоняние

Пока приложения продолжают работать со старой таблицей, мы спокойно готовим новую.

  • Строим индексы: Теперь, когда данные на месте, создаем на log_table_new все необходимые индексы. Это может занять время, но это никого не блокирует.

  • Заранее выбираем механизм догоняния: монотонный ключ с точной границей, временный capture-триггер или другой проверенный CDC. Простое повторение INSERT без уникального ключа способно дать дубли, а перенос только новых строк потеряет обновления и удаления.

Шаг 3. Проверка и переключение

Это самый ответственный момент, который требует короткого «окна тишины».

  • Навешиваем триггеры и ограничения: Добавляем на log_table_new все недостающие триггеры и внешние ключи.

  • Блокировка и финальный долив: На несколько секунд блокируем старую таблицу, чтобы в нее перестали поступать новые данные. Быстро копируем последние записи, которые успели появиться с момента последнего долива.

  • До переключения сверяем количество и контрольные суммы по диапазонам, ограничения, права и состояние sequence.

  • В коротком окне останавливаем писателей, берём блокировку с lock_timeout, применяем финальный хвост и повторяем сверку.

  • Простое переименование меняет имя, но не внутренний OID. Представления и внешние ключи, которые ссылались на старую таблицу, продолжат ссылаться на неё даже после переименования. Поэтому переключение по имени допустимо только при доказанном отсутствии таких зависимостей; иначе их нужно контролируемо пересоздать или выбрать другой инструмент миграции.

  • Время простоя заранее измеряем на репетиции. Обещать «несколько секунд» без размера финального хвоста и времени блокировок нельзя.

  • Старую таблицу не удаляем сразу. Сначала выдерживаем согласованное окно проверки и сохраняем понятный план возврата; затем удаляем объект отдельным изменением.

— Круто! А что с ID?

— А ты шаришь! Нужно проверить не только текущее значение sequence, но и DEFAULT/identity-связь и владение новым столбцом. Во время конкурентных вставок простой setval(max(id)) сам создаёт гонку, поэтому синхронизацию выполняют внутри окна переключения.

— Макс, спасибо! Я вот что еще подумал - через такую теневую таблицу можно же не только удалять данные, но и делать какие-то масштабные изменения.

— Да, сам принцип универсален, но главное, вовремя остановиться. А то так и до log_table_new3_final_v2_for_real недалеко :)