Единственный по-настоящему масштабируемый способ удалить данные в Postgres — это DROP TABLE. Так утверждает автор статьи и объясняет почему. Большие DELETE не освобождают дисковое пространство сразу, добавляют нагрузку на запись и репликацию, и в итоге не годятся для чистки миллионов строк.
Всё дело в MVCC — Postgres хранит старые версии строк рядом с текущими. Когда строку удаляют, она превращается в «мёртвый кортеж» (dead tuple). Потом приходит autovacuum и помечает место как свободное для перезаписи, но обычно не возвращает байты операционной системе. Это осознанный компромисс: Postgres рассчитывает, что удаление будет смешано с вставками, и отдавать память ядру, а потом запрашивать её обратно — дорого. VACUUM FULL так умеет, но берёт тяжёлую блокировку надолго. Индексы при DELETE вообще не трогаются — читателям приходится самим проверять, жив ли кортеж. Плюс удаление — это полноценная запись, которая попадает в WAL и реплицируется, создавая задержки для других писателей.
DROP TABLE и TRUNCATE работают иначе. Они требуют блокировки AccessExclusiveLock, но зато почти не зависят от объёма данных. Физически Postgres просто удаляет файлы с диска и чистит свой кеш буферов — сканирует только заголовки (каждый BufferDesc по 64 байта на 8КБ страницу). Для 128 ГБ shared buffers это около 1 ГБ последовательного чтения — быстро на современном железе. Никаких мёртвых кортежей, никакого долга перед vacuum, никакой лишней работы для читателей. Место освобождается сразу.
Авторы столкнулись с ситуацией в своём внутреннем инструменте observability: баг записал миллионы мусорных строк, а оставить надо было лишь сотни тысяч. Они сделали транзакционную DDL: залочили таблицу ACCESS EXCLUSIVE, создали временную таблицу с нужными строками, выполнили TRUNCATE исходной и вставили обратно сохранённые данные. В WAL попали только перевставленные строки. Если держать блокировку минутой нельзя — используют триггерный подход: пишут в новую таблицу, потом атомарно переименовывают.
Расширение pg_squeeze (современная версия pg_repack) делает примерно то же самое, но статья призывает предотвращать раздувание заранее. Главный рецепт — избавиться от массовых DELETE с помощью партиционирования. Заведите партиции по датам и вместо чистки просто дропайте старые. Postgres поддерживает рекурсивное партиционирование: можно разбить на LIST (например, «видимые» строки), а внутри — RANGE для устаревания. Так schema превращает «много удалений» в «редкий DROP TABLE» — и база данных работает быстрее, без скачков задержек репликации и с минимумом усилий.