Большая чистка через теневую таблицу
Как перенести небольшой объём нужных данных в новую таблицу вместо массового DELETE и какие зависимости проверить перед переключением.
PostgreSQLХранениеОбслуживаниеБезопасность данных— Макс, привет! Слушай, у меня беда. Пытаюсь почистить нашу 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 недалеко :)