Все материалы
Продуктовая аналитикагайдстарт

TRUNCATE в SQL: как очистить таблицу и чем он отличается от DELETE

TRUNCATE TABLE в SQL и PostgreSQL: синтаксис, отличие от DELETE и DROP, RESTART IDENTITY, CASCADE и внешние ключи, откат в транзакции, права. Примеры с реальным выводом.

КейсПрактика11 октября 2026 г.11 мин

Каждую ночь скрипт пересчитывает витрину выручки по дням: старые строки убрать, новые залить. Убрать можно через DELETE без условия, и на маленькой таблице разницы никто не заметит. На большой очистка начинает занимать больше времени, чем сам расчёт, а файл таблицы продолжает расти. Для такой задачи в SQL есть отдельная команда TRUNCATE. Она быстрее, но о многом не спрашивает и ничего не сообщает.

Коротко

TRUNCATE удаляет из таблицы все строки сразу и оставляет саму таблицу: столбцы, индексы, ограничения и права остаются на месте.

  • Условия нет: truncate table events where … — синтаксическая ошибка. Нужна часть строк — это DELETE.
  • Числа удалённых строк команда не возвращает: psql отвечает TRUNCATE TABLE, а не DELETE 35341.
  • Счётчик идентификаторов по умолчанию продолжает считать дальше. С начала он пойдёт только с restart identity.
  • Таблицу, на которую ссылается внешний ключ, PostgreSQL очистить не даст. cascade снимает запрет и очищает заодно все ссылающиеся таблицы.
  • В PostgreSQL TRUNCATE внутри транзакции откатывается через ROLLBACK. В других базах это не всегда так.
  • Аналитику команда нужна для таблиц, которые целиком пересобираются из источника: витрин и стейджинга.

Что делает TRUNCATE TABLE и как его написать

Полная запись — truncate table имя_таблицы. Слово table в PostgreSQL необязательно, truncate events_copy работает так же. После имени можно дописать restart identity и cascade, о них ниже.

Примеры выполнены в PostgreSQL 14 на копии учебной базы «Маяк». Песочница Симулятора SQL-аналитика принимает только SELECT и WITH, поэтому TRUNCATE запускайте в своём PostgreSQL, и не на рабочей таблице, а на копии: create temp table events_copy as select * from events. В блоках ниже копии временные и живут внутри begin … rollback.

Перед очисткой посмотрите, что именно исчезнет: сколько строк, какой последний идентификатор и за какой период данные. Эти числа пригодятся дважды: чтобы убедиться, что таблица та, и чтобы сверить её после повторной загрузки. В таблице events 35 341 строка, последний event_id — 35341, события с 1 июня по 30 августа 2026 года.

Что посмотреть перед очисткой: строки, последний id, период — 35341 | 35341
select
  count(*)        as rows_total,           -- 35341
  max(event_id)   as max_event_id,         -- 35341
  min(event_time) as first_event,          -- 2026-06-01 08:38
  max(event_time) as last_event            -- 2026-08-30 12:59
from events;
sqlPostgreSQL only: очистка копии — ответ TRUNCATE TABLE, строк 0
begin;
create temp table events_copy as select * from events;   -- SELECT 35341

truncate table events_copy;                               -- TRUNCATE TABLE

select count(*) as rows_left
from events_copy;                                         -- 0
rollback;

Чем TRUNCATE отличается от DELETE без WHERE

Результат одинаковый — пустая таблица. Отличается способ. DELETE проходит по строкам и каждую помечает удалённой; место на диске освободится позже, после очистки (VACUUM). TRUNCATE строки не читает: он заменяет файл таблицы пустым. Отсюда и все различия.

Первое — условие. У TRUNCATE его нет: на truncate table events_copy where event_time < '2026-06-08' PostgreSQL отвечает ERROR: syntax error at or near "where". Второе — ответ. delete from events_copy возвращает DELETE 35341, и это число можно сверить с ожиданием. TRUNCATE отвечает одной строкой TRUNCATE TABLE при любом размере таблицы, а RETURNING у него нет. Сколько строк было, нужно узнать заранее.

Третье — построчные триггеры. Мы повесили на копию событий триггер, который пишет идентификатор каждой удалённой строки в журнал. После DELETE событий первой недели июня в журнале 748 записей, после TRUNCATE всей таблицы — ни одной. Если на удалении строк в вашей базе держится аудит или пересчёт, TRUNCATE пройдёт мимо него.

Четвёртое — скорость и место. Мы трижды очистили свежую копию events каждой командой с включённым \timing. Это ноутбук и таблица на 2 МБ, так что цифры показывают направление, а не масштаб: на маленькой таблице обе команды мгновенные. В прогонах для статьи про DELETE вышло 7,6–10,2 и 1,0–2,3 мс — абсолютные значения плавают, порядок сохраняется. На таблицах в миллионы строк время DELETE растёт вместе с числом строк, TRUNCATE от него почти не зависит.

DELETE без WHERE и TRUNCATE на копии events (35 341 строка), PostgreSQL 14, ноутбук
DELETETRUNCATE
Ответ psqlDELETE 35341TRUNCATE TABLE
Время, три прогона20,8 / 9,4 / 26,5 мс4,5 / 4,6 / 5,1 мс
Размер таблицы до и после2120 kB → 2120 kB2120 kB → 0 bytes
Записей в журнале построчного триггера748 из 748 удалённых0
Условие WHEREестьсинтаксическая ошибка

Чем TRUNCATE отличается от DROP TABLE

TRUNCATE убирает содержимое, DROP TABLE — саму таблицу. После truncate table events_copy запрос select count(*) from events_copy возвращает 0, а описание таблицы показывает те же столбцы и тот же триггер. После drop table events_copy тот же запрос падает: ERROR: relation "events_copy" does not exist.

Для перезаливки это существенно. После DROP таблицу надо создавать заново, вместе с индексами и правами, а дашборд, который читает её в эту минуту, получит ошибку. После TRUNCATE достаточно вставить строки. Сравнение трёх команд по строкам — в статье про DELETE, в разделе о TRUNCATE и DROP TABLE.

RESTART IDENTITY: что происходит со счётчиком идентификаторов

Если в таблице есть столбец с автоматической нумерацией (generated always as identity или serial), номера выдаёт отдельный объект — последовательность. TRUNCATE по умолчанию её не трогает. Это поведение называется continue identity, и писать его явно не нужно.

Проверим на стейджинговой таблице платежей. После загрузки в ней 1251 строка, и последний row_id равен 1251. Очищаем таблицу и вставляем три строки: они получают номера 1252, 1253 и 1254, хотя таблица была пустой. Очищаем ещё раз, теперь с restart identity, и те же три строки получают 1, 2 и 3. Со столбцом serial результат такой же: 4 без сброса и 1 со сбросом.

Что выбрать, зависит от того, ссылается ли кто-то на эти номера. Для стейджинга и витрины, которую пересобирают целиком, обычно удобнее restart identity: номера каждый раз одни и те же, а счётчик не уползает к границе типа int. Если номера уже попали в другую таблицу, в выгрузку или в чей-то отчёт, сброс опасен: новые строки получат идентификаторы старых.

PostgreSQL only: счётчик после TRUNCATE и после TRUNCATE … RESTART IDENTITY
begin;
create temp table stg_payments (
  row_id     bigint generated always as identity,
  payment_id int,
  amount     numeric(10, 2)
);
insert into stg_payments (payment_id, amount)
select payment_id, amount from payments order by payment_id;      -- INSERT 0 1251, max(row_id) = 1251

truncate table stg_payments;                                      -- счётчик не сброшен
insert into stg_payments (payment_id, amount)
select payment_id, amount from payments order by payment_id limit 3;
select min(row_id) as first_id, max(row_id) as last_id
from stg_payments;                                                -- 1252 | 1254

truncate table stg_payments restart identity;                     -- счётчик с начала
insert into stg_payments (payment_id, amount)
select payment_id, amount from payments order by payment_id limit 3;
select min(row_id) as first_id, max(row_id) as last_id
from stg_payments;                                                -- 1 | 3
rollback;

TRUNCATE CASCADE: что делать с внешними ключами

Пусть есть справочник тарифов plans из трёх строк и таблица платежей payments_copy, где столбец plan — внешний ключ на справочник. Справочник хочется перезалить. PostgreSQL отказывает, причём даже если бы в платежах не было ни одной строки: TRUNCATE не проверяет строки, ему достаточно самого факта ссылки.

Подсказка в ошибке предлагает два выхода. Первый — очистить обе таблицы одной командой: truncate table plans, payments_copy. Второй — cascade: PostgreSQL сам найдёт все таблицы, которые ссылаются на очищаемую, и очистит их тоже, а затем и те, что ссылаются на них.

Мы выполнили truncate table plans cascade. Команда ответила уведомлением NOTICE: truncate cascades to table "payments_copy" и той же строкой TRUNCATE TABLE. До неё в таблицах было 3 тарифа и 1251 платёж, после — 0 и 0. Чтобы обновить три строки справочника, мы стёрли все платежи.

В этом и опасность cascade: список таблиц определяете не вы, а схема базы. Сегодня на справочник ссылается одна таблица, через месяц коллега добавит вторую, и тот же скрипт очистит и её. Уведомление NOTICE в логе ночной задачи никто не читает. Надёжнее перечислять таблицы явно: тогда появление новой ссылки остановит скрипт ошибкой, а не увеличит потери. А для справочника чаще всего правильный ответ вообще не TRUNCATE, а UPDATE или вставка с обновлением существующих строк.

Ответ PostgreSQL 14: на plans ссылается внешний ключ из payments_copy
КомандаОтвет
truncate table plansERROR: cannot truncate a table referenced in a foreign key constraint DETAIL: Table "payments_copy" references "plans". HINT: Truncate table "payments_copy" at the same time, or use TRUNCATE ... CASCADE.
delete from plansERROR: update or delete on table "plans" violates foreign key constraint "payments_copy_plan_fkey" on table "payments_copy" DETAIL: Key (plan)=(basic) is still referenced from table "payments_copy".
truncate table plans cascadeNOTICE: truncate cascades to table "payments_copy" TRUNCATE TABLE
sqlPostgreSQL only: CASCADE очищает и таблицу со ссылкой — было 3 и 1251, стало 0 и 0
begin;
create temp table plans (plan text primary key, title text);
insert into plans
select distinct plan, initcap(plan) from payments;                -- INSERT 0 3

create temp table payments_copy (
  payment_id int primary key,
  user_id    int,
  amount     numeric(10, 2),
  plan       text references plans (plan)
);
insert into payments_copy
select payment_id, user_id, amount, plan from payments;           -- INSERT 0 1251

truncate table plans cascade;         -- NOTICE: truncate cascades to table "payments_copy"

select
  (select count(*) from plans)         as plans_left,             -- 0
  (select count(*) from payments_copy) as payments_left;          -- 0
rollback;

Откатывается ли TRUNCATE в транзакции

В PostgreSQL — да. Мы открыли транзакцию, очистили копию событий и посчитали строки: 0. После rollback в таблице снова 35 341 строка. Откатывается и сброс счётчика: после отменённого restart identity следующая строка получила очередной номер, а не 1.

Вне транзакции каждая команда фиксируется сразу, и вернуть данные можно только из резервной копии. Поэтому привычка та же, что и с DELETE: сначала begin, потом команда, проверка и только после неё commit. Это не общее свойство SQL: в MySQL, например, TRUNCATE фиксируется сразу и отмене не подлежит. Перед первым запуском в незнакомой базе проверьте её документацию.

У транзакции с TRUNCATE есть цена. Команда берёт на таблицу самую строгую блокировку, и до commit или rollback остальные сессии не могут таблицу даже читать: запрос дашборда будет ждать. Очистку и загрузку держите в одной короткой транзакции и не оставляйте её открытой.

PostgreSQL only: BEGIN → TRUNCATE → ROLLBACK — 0 строк внутри, 35341 после отката
create temp table events_copy as select * from events;   -- SELECT 35341

begin;
truncate table events_copy;
select count(*) as rows_inside from events_copy;          -- 0
rollback;

select count(*) as rows_after_rollback from events_copy;  -- 35341
drop table events_copy;

Какие права нужны для TRUNCATE

В PostgreSQL у TRUNCATE отдельная привилегия. Право DELETE её не заменяет, хотя результат команд похож. Мы создали роль с правами SELECT, INSERT и DELETE на стейджинговую таблицу. delete from stg_payments под ней прошёл, а truncate table stg_payments вернул ERROR: permission denied for table stg_payments. После grant truncate on stg_payments to … команда выполнилась.

Если TRUNCATE в вашем скрипте упал с этой ошибкой, права у вас, скорее всего, выданы осознанно: администратор разрешил удалять строки, но не разрешил очищать таблицу одним словом. Просите привилегию на конкретную таблицу. Владельцу таблицы она дана изначально.

Когда аналитику нужен TRUNCATE, а когда нет

Основной случай — таблица, которую целиком пересобирают из источника. Витрина выручки по дням строится запросом к payments, и её содержимое ничего не стоит: его всегда можно получить заново. Тогда перезаливка — это TRUNCATE и INSERT … SELECT в одной транзакции. Читатели витрины не увидят её пустой: до commit они ждут, после видят новые данные.

После загрузки сверьте витрину с источником. Запрос ниже считает по payments то же, что должно получиться в сумме по витрине: 84 дня, 1251 платёж, выручка 30 639. После перезаливки с restart identity в витрине те же три числа и номера строк от 1 до 84.

TRUNCATE не подходит, когда нужно убрать часть строк, когда таблицу нельзя восстановить из источника, когда на удаление завязаны триггеры или когда на таблицу ссылаются другие. И ещё один случай: витрина дополняется по дням, а не пересобирается. Там удаляют и заливают заново один день через DELETE с условием по дате.

Сверка после перезаливки: что должно получиться в витрине — 84 | 1251 | 30639.00
select
  count(distinct paid_at) as days,         -- 84
  count(*)                as payments,     -- 1251
  sum(amount)             as revenue       -- 30639.00
from payments;
sqlPostgreSQL only: перезаливка витрины — TRUNCATE и INSERT в одной транзакции
begin;
create temp table mart_revenue_daily (
  load_id  bigint generated always as identity,
  paid_at  date,
  payments int,
  revenue  numeric(12, 2)
);
insert into mart_revenue_daily (paid_at, payments, revenue)
select paid_at, count(*), sum(amount)
from payments group by paid_at order by paid_at;                  -- вчерашняя версия, INSERT 0 84

-- ночная перезаливка: очистить и собрать заново
truncate table mart_revenue_daily restart identity;
insert into mart_revenue_daily (paid_at, payments, revenue)
select paid_at, count(*), sum(amount)
from payments group by paid_at order by paid_at;                  -- INSERT 0 84

select count(*) as days, max(load_id) as max_load_id,
       sum(payments) as payments, sum(revenue) as revenue
from mart_revenue_daily;                                          -- 84 | 84 | 1251 | 30639.00
rollback;   -- в рабочем скрипте при совпадении чисел — commit

Что проверить перед запуском TRUNCATE

Пять вопросов, на которые стоит ответить до того, как нажать Enter.

  • Таблица та? Имя и схема проверены, а count(*), последний идентификатор и период данных совпадают с ожиданием.
  • Данные можно получить заново? Есть источник и запрос, которым таблица наполняется, или свежая копия.
  • Кто ссылается на таблицу? Внешние ключи известны, таблицы перечислены в команде явно, cascade не используется без необходимости.
  • Что со счётчиком? Решено, нужен ли restart identity, и номера строк нигде не сохранены.
  • Команда идёт после begin, в той же транзакции стоит загрузка, а перед commit результат сверен с источником.

Частые вопросы

Что быстрее — TRUNCATE или DELETE?

TRUNCATE: он не читает строки, и его время почти не зависит от размера таблицы. На учебной таблице в 35 341 строку разница — миллисекунды, на таблицах в миллионы строк — минуты против долей секунды. Зато DELETE умеет удалять часть строк и сообщает, сколько удалил.

Можно ли в TRUNCATE указать WHERE?

Нет, команда очищает таблицу целиком. Для удаления по условию нужен DELETE. Если удалить надо почти всё, иногда проще сохранить нужные строки во временную таблицу, очистить исходную и вставить их обратно.

Сбрасывает ли TRUNCATE счётчик идентификаторов?

В PostgreSQL по умолчанию нет: нумерация продолжается с того места, где остановилась. Сброс включается словами restart identity. В других базах поведение по умолчанию может быть обратным.

Можно ли очистить несколько таблиц одной командой?

Да: truncate table plans, payments_copy. Имена перечисляют через запятую, а restart identity и cascade действуют на все таблицы списка. Так очищают связанные внешним ключом таблицы без cascade: на учебных копиях после этой команды в обеих таблицах 0 строк.

Освобождает ли TRUNCATE место на диске?

Да, сразу. В нашей проверке таблица занимала 2120 kB и до, и после DELETE, а после TRUNCATE — 0 bytes. После DELETE место становится доступным для новых строк этой же таблицы только после очистки.

Что почитать дальше

Оба запроса на чтение из статьи выполняются в песочнице Симулятора SQL-аналитика как есть. Блоки с TRUNCATE запускайте в своём PostgreSQL: они работают на временных копиях и заканчиваются откатом, так что учебная база остаётся прежней.

Продолжить чтение
Вся библиотека