Question Details

No question body available.

Tags

postgresql

Answers (2)

March 10, 2026 Score: 1 Rep: 30,697 Quality: Medium Completeness: 80%

If you do not need to wipe multiple tables at once and you do require MVCC safety, use a plain unqualified delete:

delete from yourtable;

The multi-target aspect of truncate has to be emulated by setting up a sequence of separate deletes like this. If you need to run them all as a single query, wrap them in CTEs.

Delete runs through each file that holds your table data and leaves a marker with your transaction identifier saying all transactions newer than yours should find it freshly emptied. All concurrent live sessions with a still ongoing transaction, are free to ignore that marker and continue working with their earlier snapshot until they commit/rollback.

Truncate instead just creates an empty file and re-links the table to it, leaving its old files to be garbage-collected. You can see it in src/backend/commands/tablecmds.c:2230

/(...)
 Create a new empty storage file for the relation, and assign it
 as the relfilenumber value. The old storage file is scheduled
 for deletion at commit.
/
RelationSetNewRelfilenumber(rel, rel->rd_rel->relpersistence);

It's not MVCC-safe but it is transaction-safe, so while the perceived table wipe is truly immediate, the disk space release is actually scheduled to happen when (and only if) transaction surrounding the truncate is committed, not right after the truncate command itself completes.

/
 Schedule unlinking of the old storage at transaction commit, except
 when performing a binary upgrade, when we must do it immediately.
/
if (IsBinaryUpgrade)
{/(...)/
}
else
{
   / Not a binary upgrade, so just schedule it to happen later. */
   RelationDropStorage(relation);
}

As an alternative to switching to plain deletes, you can make sure the other sessions that use the tables you plan to truncate, request and hold onto some locks on it until they're done. A truncate will attempt to acquire an access exclusive lock, so it'll just wait for everyone that came before it to finish their business with the target objects, and block anyone new.

TRUNCATE acquires an ACCESS EXCLUSIVE lock on each table it operates on, which blocks all other concurrent operations on the table. When RESTART IDENTITY is specified, any sequences that are to be restarted are likewise locked exclusively. If concurrent access to a table is required, then the DELETE command should be used instead.

ACCESS EXCLUSIVE (AccessExclusiveLock)

Conflicts with locks of all modes (ACCESS SHARE, ROW SHARE, ROW EXCLUSIVE, SHARE UPDATE EXCLUSIVE, SHARE, SHARE ROW EXCLUSIVE, EXCLUSIVE, and ACCESS EXCLUSIVE). This mode guarantees that the holder is the only transaction accessing the table in any way.
Acquired by the DROP TABLE, TRUNCATE, REINDEX, CLUSTER, VACUUM FULL, and REFRESH MATERIALIZED VIEW (without CONCURRENTLY) commands. Many forms of ALTER INDEX and ALTER TABLE also acquire a lock at this level. This is also the default lock mode for LOCK TABLE statements that do not specify a mode explicitly.

Tip
Only an ACCESS EXCLUSIVE lock blocks a SELECT (without FOR UPDATE/SHARE) statement.

March 10, 2026 Score: 0 Rep: 1,047 Quality: Low Completeness: 10%

Because MVCC requires to keep the deleted rows for queries which started before to continue to read a consistent state. Truncate removes this versions history. Think of like a drop+recreate, and that's why it is fast.