TRUNCATE в SQL: как очистить таблицу и чем он отличается от DELETE
TRUNCATE TABLE в SQL и PostgreSQL: синтаксис, отличие от DELETE и DROP, RESTART IDENTITY, CASCADE и внешние ключи, откат в транзакции, права. Примеры с реальным выводом.
Содержание статьи
Каждую ночь скрипт пересчитывает витрину выручки по дням: старые строки убрать, новые залить. Убрать можно через 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 года.
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;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 | TRUNCATE | |
|---|---|---|
| Ответ psql | DELETE 35341 | TRUNCATE TABLE |
| Время, три прогона | 20,8 / 9,4 / 26,5 мс | 4,5 / 4,6 / 5,1 мс |
| Размер таблицы до и после | 2120 kB → 2120 kB | 2120 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. Если номера уже попали в другую таблицу, в выгрузку или в чей-то отчёт, сброс опасен: новые строки получат идентификаторы старых.
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 или вставка с обновлением существующих строк.
| Команда | Ответ |
|---|---|
truncate table plans | ERROR: 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 plans | ERROR: 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 cascade | NOTICE: truncate cascades to table "payments_copy" TRUNCATE TABLE |
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 остальные сессии не могут таблицу даже читать: запрос дашборда будет ждать. Очистку и загрузку держите в одной короткой транзакции и не оставляйте её открытой.
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 с условием по дате.
select
count(distinct paid_at) as days, -- 84
count(*) as payments, -- 1251
sum(amount) as revenue -- 30639.00
from payments;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: они работают на временных копиях и заканчиваются откатом, так что учебная база остаётся прежней.
Материалы по теме

DELETE в SQL: как удалить строки и не потерять лишнее
DELETE в SQL и PostgreSQL: удаление по условию, DELETE … USING, удаление дублей, RETURNING, разница DELETE, TRUNCATE и DROP, внешние ключи.

LEFT JOIN в SQL: отличие от INNER JOIN, NULL и условие в ON
LEFT JOIN в SQL на примерах: что возвращает, чем отличается от INNER JOIN, почему условие в WHERE теряет строки и как найти записи без пары.

WHERE в SQL: условия, AND и OR, NULL и фильтр по дате
WHERE в SQL с примерами: несколько условий, приоритет AND и OR, IN, BETWEEN, LIKE, NULL, фильтр по дате, отличие от HAVING и способ проверить результат.