Quick answer
- In 1bench
- Right-click the table in the sidebar, choose Truncate Table, type TRUNCATE in the confirmation box and click Truncate Table.
- In SQL
- sql
TRUNCATE orders, order_items RESTART IDENTITY;List every table that references the one you are emptying, or add CASCADE, otherwise PostgreSQL refuses with a foreign key error.
Truncate a table visually in 1bench
1bench runs a plain TRUNCATE TABLE on one table from the sidebar, behind a confirmation you have to type. It suits tables nothing else points at, such as logs, staging tables and test data.
Prerequisite: download 1bench. Free 7-day trial, no sign-up.
- 1
Find the table
Connect to your PostgreSQL database in 1bench and expand the schema in the sidebar until you see the table.
- 2
Choose Truncate Table
Right-click the table and choose Truncate Table. The item is not shown for views or materialized views, and it is greyed out on a read-only connection.
- 3
Type TRUNCATE to confirm
The Truncate Table dialog says "This will permanently delete all data in the table. The table structure will be preserved." The Truncate Table button stays disabled until you type TRUNCATE in the box.
- 4
Run it
Click Truncate Table, or press Enter. A toast reads Truncated public.orders (with your table's name), and the open grid and the table list refresh.
- 1bench runs TRUNCATE TABLE "schema"."table" with no CASCADE and no RESTART IDENTITY. Truncating a table that another table references fails, and the toast shows Failed: with PostgreSQL's foreign key error.
- Sequences keep counting after a truncate from the menu, so the next new row gets the next id, not 1. Run TRUNCATE ... RESTART IDENTITY from the Query panel's SQL editor when you want ids to start over.
- The menu empties one table at a time. For several tables, CASCADE or a whole schema, use the SQL below in the Query panel.
Truncate with SQL
removes every row from a table in one step. It does not scan rows the way does; it gives the table new, empty data files, so a table of millions of rows empties in milliseconds. The keyword is optional.
TRUNCATE TABLE audit_log_v2;
The examples use the seed-demo shop schema: , (references ) and (references ). Every statement and message below came from runs on PostgreSQL 16.13 and 18.6, and both versions behaved the same.
TRUNCATE syntax
The full statement takes a list of tables and two optional switches. Defaults are and .
TRUNCATE [ TABLE ] [ ONLY ] name [ * ] [, ...]
[ RESTART IDENTITY | CONTINUE IDENTITY ] [ CASCADE | RESTRICT ];
There is no clause and no . Both are syntax errors, and a missing table fails with "relation "nope" does not exist":
TRUNCATE audit_log WHERE id > 5;
ERROR: syntax error at or near "WHERE"
TRUNCATE TABLE IF EXISTS audit_log;
ERROR: syntax error at or near "EXISTS"
To remove only some rows, use .
TRUNCATE CASCADE and the foreign key error
A table that another table references cannot be truncated on its own, even when the referencing table is already empty. This is the error, word for word:
TRUNCATE TABLE orders;
ERROR: cannot truncate a table referenced in a foreign key constraint
DETAIL: Table "order_items" references "orders".
HINT: Truncate table "order_items" at the same time, or use TRUNCATE ... CASCADE.
follows the foreign keys and empties every table that references the target, and the tables that reference those, in the same statement. PostgreSQL prints a notice for each one.
TRUNCATE orders CASCADE;
-- NOTICE: truncate cascades to table "order_items"
follows the whole chain, so would also empty , , and . Read the notices, or check for its "Referenced by" list first. is the default and only spells out the refusal above.
RESTART IDENTITY vs CONTINUE IDENTITY
By default the sequences behind and columns keep their value, so after emptying a table of 300 orders the next order still gets id 301. Add to start them over at 1.
TRUNCATE orders, order_items RESTART IDENTITY;
INSERT INTO orders (user_id, total_amount) VALUES (1, 5) RETURNING id;
-- id = 1
| Option | What happens to the id sequence |
|---|---|
| (default) | Keeps counting: the next row after 300 gets 301 |
| Resets every sequence owned by a column of the truncated tables |
With , the reset reaches the cascaded tables too: set both and back to 1. A sequence the table does not own, used through , is not reset. Reset it yourself:
SELECT setval('ticket_seq', 1, false);
Truncate multiple tables at once
List the tables separated by commas. They are emptied together in one statement, which is also how you satisfy the foreign key check without : every referencing table is in the list.
TRUNCATE orders, order_items;
Example: clear the shop's order data and start the ids over, leaving untouched.
TRUNCATE order_items, orders RESTART IDENTITY;
TRUNCATE vs DELETE vs DROP
All three get rid of data, but they work differently. Timings are from a 1,000,000 row table on the same machine.
| TRUNCATE | DELETE FROM | DROP TABLE | |
|---|---|---|---|
| Removes | All rows | Rows matching WHERE, or all | Rows and the table itself |
| WHERE clause | No | Yes | No |
| Speed on 1M rows | 18 to 45 ms | 309 to 400 ms | 6 to 7 ms |
| Disk space | Freed at commit | Kept until VACUUM | Freed at commit |
| Row triggers | Not fired; ON TRUNCATE statement triggers fire | ON DELETE row triggers fire | None |
| Lock | ACCESS EXCLUSIVE | ROW EXCLUSIVE, readers keep working | ACCESS EXCLUSIVE |
| Sequences | Kept, unless RESTART IDENTITY | Kept | Owned sequences dropped |
| Rollback | Yes, inside a transaction | Yes | Yes, inside a transaction |
Triggers are the difference that bites. A delete trigger, such as one that writes to an audit table, does not run on . Add a statement trigger, with a trigger function of your own, if you need to react to it:
CREATE TRIGGER orders_truncated
AFTER TRUNCATE ON orders
FOR EACH STATEMENT EXECUTE FUNCTION log_truncate();
is also not MVCC-safe. A transaction that took its snapshot before another session's truncate committed sees the table as empty afterwards, not the rows that were there when it started. does not have this problem.
on a referenced table fails the same way, with "cannot drop table users because other objects depend on it". then drops only the foreign key constraint on ("drop cascades to constraint orders_user_id_fkey on table orders"), and the orders keep their rows.
TRUNCATE in a transaction and rollback
is transactional in PostgreSQL, unlike in MySQL. Run it inside and a brings every row back.
BEGIN;
TRUNCATE audit_log_v2;
SELECT count(*) FROM audit_log_v2; -- 0
ROLLBACK;
SELECT count(*) FROM audit_log_v2; -- original count again
is rolled back as well: the sequence goes back to its old value. Once the transaction commits, there is no undo.
Locks, and why TRUNCATE hangs
takes an lock on every table it empties, the strongest lock there is. It has to wait for every open transaction that has touched the table, even a plain in an idle transaction, and while it waits, new queries on the table queue up behind it. That is the usual reason a seems to take forever on a tiny table.
Set a so it gives up instead of blocking the table:
SET lock_timeout = '2s';
TRUNCATE audit_log_v2;
-- ERROR: canceling statement due to lock timeout
Example: from a second session, find what the waiting truncate is blocked by.
SELECT pid, wait_event_type, pg_blocking_pids(pid) AS blocked_by, query
FROM pg_stat_activity
WHERE query ILIKE 'truncate%';
The column lists the process ids holding the table. Finish or cancel those sessions, for example with . Inside a transaction the lock is held until , so keep a truncate near the end of a long transaction, not the start.
TRUNCATE permission
Truncating needs the privilege on the table, or ownership. rights are not enough:
ERROR: permission denied for table audit_log
Grant it per table, or on every existing table in a schema:
GRANT TRUNCATE ON orders, order_items TO app_user;
GRANT TRUNCATE ON ALL TABLES IN SCHEMA public TO app_user;
SELECT has_table_privilege('app_user', 'orders', 'TRUNCATE');
covers tables that exist now. For tables created later, add .
Truncate all tables in a schema or database
There is no command. Build one with every table in the list, so the foreign keys are satisfied without , and run it in a block:
DO $$
DECLARE
tables text;
BEGIN
SELECT string_agg(format('%I.%I', schemaname, tablename), ', ')
INTO tables
FROM pg_tables
WHERE schemaname = 'public';
EXECUTE 'TRUNCATE ' || tables || ' RESTART IDENTITY';
END $$;
For every table in the database, swap the filter for . To keep a table, such as a migrations table, add . If the filter matches no tables, the block fails with "query string argument of EXECUTE is null", which is a safe way to fail.
Example: in psql, runs each generated statement in turn, which is handy when you want to see them.
SELECT format('TRUNCATE %I.%I RESTART IDENTITY CASCADE', schemaname, tablename)
FROM pg_tables
WHERE schemaname = 'public' \gexec
Partitioned tables
Truncating a partitioned table empties all of its partitions. Truncate a single partition by name to clear one range, which is a fast way to drop old data.
TRUNCATE events_2025;
TRUNCATE events;
is refused on the parent: "cannot truncate only a partitioned table", with the hint to drop or truncate the partitions directly.