A Deep Dive into PostgreSQL’s TRUNCATE Statement

Most relational database management systems support the TRUNCATE statement, and PostgreSQL is no exception.

That said, PostgreSQL has a few differences in the way its TRUNCATE statement works when compared to many other RDBMSs.

In this article, we’ll explore the various features of PostgreSQL’s implementation of the TRUNCATE statement, along with examples to demonstrate.

What is TRUNCATE?

TRUNCATE is a statement that can be used to quickly remove all rows from a table or set of tables. Unlike DELETE, TRUNCATE removes data without scanning each row. Also, it reclaims disk space immediately, rather than requiring a subsequent VACUUM operation. Because of these characteristics, TRUNCATE can be particularly handy on large tables.

Syntax

In PostgreSQL, the syntax for TRUNCATE goes like this:

TRUNCATE [ TABLE ] [ ONLY ] name [ * ] [, ... ]
    [ RESTART IDENTITY | CONTINUE IDENTITY ] [ CASCADE | RESTRICT ]

This includes quite a few options that we don’t see in many other RDBMSs, so let’s take a look at them.

Basic TRUNCATE

The simplest form of TRUNCATE removes all rows from a single table:

TRUNCATE TABLE planets;

This example removes all rows from the planets table.

Multiple Tables

We can TRUNCATE multiple tables in a single command:

TRUNCATE TABLE planets, explorers;

This removes all rows from the planets and the explorers tables.

The following examples demonstrate the various options we can use when truncating tables in PostgreSQL.

RESTART IDENTITY vs CONTINUE IDENTITY

The RESTART IDENTITY option automatically restarts sequences owned by columns of the truncated table(s), while CONTINUE IDENTITY preserves the current sequence value (so that it continues where it left off):

TRUNCATE TABLE planets RESTART IDENTITY;
TRUNCATE TABLE planets CONTINUE IDENTITY;

Let’s use a working example to demonstrate how each of these options work.

Suppose we create and populate the following table:

CREATE TABLE planets (
    id SERIAL PRIMARY KEY,
    name VARCHAR(50),
    moons INT,
    discovered_year INT
);

INSERT INTO planets (name, moons, discovered_year) VALUES
('Mercury', 0, -3000),
('Venus', 0, -1610),
('Earth', 1, -1543),
('Mars', 2, 1659),
('Jupiter', 79, 1610),
('Saturn', 146, 1610),
('Uranus', 27, 1781),
('Neptune', 14, 1846);

SELECT * FROM planets;

Result:

 id |  name   | moons | discovered_year 
----+---------+-------+-----------------
1 | Mercury | 0 | -3000
2 | Venus | 0 | -1610
3 | Earth | 1 | -1543
4 | Mars | 2 | 1659
5 | Jupiter | 79 | 1610
6 | Saturn | 146 | 1610
7 | Uranus | 27 | 1781
8 | Neptune | 14 | 1846

We can see that there are eight rows, and the id column (i.e. the SERIAL column) goes up to 8.

In the following examples, we’ll truncate the table and then repopulate it with data to demonstrate how the RESTART IDENTITY and CONTINUE IDENTITY options work.

We’ll do CONTINUE IDENTITY first:

TRUNCATE TABLE planets CONTINUE IDENTITY;

INSERT INTO planets (name, moons, discovered_year) VALUES
('Mercury', 0, -3000),
('Venus', 0, -1610),
('Earth', 1, -1543);

SELECT * FROM planets;

Result:

 id |  name   | moons | discovered_year 
----+---------+-------+-----------------
9 | Mercury | 0 | -3000
10 | Venus | 0 | -1610
11 | Earth | 1 | -1543

We can see that the id column continued from where it left off (numbering started at 9 instead of 1).

Now let’s do RESTART IDENTITY:

TRUNCATE TABLE planets RESTART IDENTITY;

INSERT INTO planets (name, moons, discovered_year) VALUES
('Mercury', 0, -3000),
('Venus', 0, -1610),
('Earth', 1, -1543);

SELECT * FROM planets;

Result:

 id |  name   | moons | discovered_year 
----+---------+-------+-----------------
1 | Mercury | 0 | -3000
2 | Venus | 0 | -1610
3 | Earth | 1 | -1543

This time the numbering started back at 1.

CASCADE vs RESTRICT

The CASCADE option allows TRUNCATE to remove data from dependent tables, while RESTRICT (the default) will prevent the operation if there are any dependent tables:

TRUNCATE TABLE planets CASCADE;
TRUNCATE TABLE planets RESTRICT;

Next, we’ll do an example to demonstrate both options.

First, we’ll create two tables, one with a foreign key constraint that references the other table. We’ll populate both with data:

CREATE TABLE employees (
    id SERIAL PRIMARY KEY,
    first_name VARCHAR(50),
    last_name VARCHAR(50),
    department VARCHAR(50)
);

CREATE TABLE employee_projects (
    id SERIAL PRIMARY KEY,
    employee_id INTEGER REFERENCES employees(id),
    project_name VARCHAR(100)
);

INSERT INTO employees (first_name, last_name, department)
VALUES
    ('Mickey', 'Clooney', 'Sales'),
    ('Donald', 'Pitt', 'Marketing'),
    ('Goofy', 'Jolie', 'IT'),
    ('Minnie', 'Aniston', 'HR');

INSERT INTO employee_projects (employee_id, project_name)
VALUES
    (1, 'Project A'),
    (2, 'Project B'),
    (3, 'Project C');

SELECT * FROM employees;
SELECT * FROM employee_projects;

Result:

 id | first_name | last_name | department 
----+------------+-----------+------------
1 | Mickey | Clooney | Sales
2 | Donald | Pitt | Marketing
3 | Goofy | Jolie | IT
4 | Minnie | Aniston | HR
(4 rows)

id | employee_id | project_name
----+-------------+--------------
1 | 1 | Project A
2 | 2 | Project B
3 | 3 | Project C

Now let’s try to truncate the employees table using RESTRICT:

TRUNCATE TABLE employees RESTRICT;

Result:

ERROR:  cannot truncate a table referenced in a foreign key constraint
DETAIL: Table "employee_projects" references "employees".
HINT: Truncate table "employee_projects" at the same time, or use TRUNCATE ... CASCADE.

As mentioned, RESTRICT is the default option, so this is the same error we’d get if we hadn’t specified anything for this option.

In any case, the error message suggests that we either truncate both tables at the same time, or use the CASCADE option.

Let’s use the CASCADE option:

TRUNCATE TABLE employees CASCADE;

SELECT * FROM employees;
SELECT * FROM employee_projects;

Result:

NOTICE:  truncate cascades to table "employee_projects"
TRUNCATE TABLE

The message tells us that the truncate operation has cascaded to the employee_projects table.

Let’s check the contents of both tables:

SELECT * FROM employees;
SELECT * FROM employee_projects;

Result:

 id | first_name | last_name | department 
----+------------+-----------+------------
(0 rows)

id | employee_id | project_name
----+-------------+--------------
(0 rows)

Both tables have been truncated, as expected.

Using ONLY with Inheritance

The ONLY keyword is used when you want to truncate only the specified table and not its child tables in an inheritance hierarchy.

Suppose we create the following tables and insert data:

-- Create the parent table
CREATE TABLE cosmic_entities (
    entity_id SERIAL PRIMARY KEY,
    name VARCHAR(100) NOT NULL,
    age_in_eons NUMERIC(10, 2),
    energy_level INTEGER
);

-- Create the child table that inherits from cosmic_entities
CREATE TABLE celestial_bodies (
    mass_in_solar_masses NUMERIC(15, 5),
    habitable BOOLEAN
) INHERITS (cosmic_entities);

-- Insert one row into the parent table
INSERT INTO cosmic_entities (name, age_in_eons, energy_level)
VALUES ('Azathoth', 13.8, 9999);

-- Insert one row into the child table
INSERT INTO celestial_bodies (name, age_in_eons, energy_level, mass_in_solar_masses, habitable)
VALUES ('Nebulon-5', 2.7, 750, 1.23456, true);

SELECT * FROM cosmic_entities;
SELECT * FROM celestial_bodies;

Output:

 entity_id |   name    | age_in_eons | energy_level 
-----------+-----------+-------------+--------------
1 | Azathoth | 13.80 | 9999
2 | Nebulon-5 | 2.70 | 750
(2 rows)

entity_id | name | age_in_eons | energy_level | mass_in_solar_masses | habitable
-----------+-----------+-------------+--------------+----------------------+-----------
2 | Nebulon-5 | 2.70 | 750 | 1.23456 | t

We can see that the row that was inserted into the child table also appears in the parent table.

Now let’s truncate the parent table only:

TRUNCATE TABLE ONLY cosmic_entities;

SELECT * FROM cosmic_entities;
SELECT * FROM celestial_bodies;

Result:

 entity_id |   name    | age_in_eons | energy_level 
-----------+-----------+-------------+--------------
2 | Nebulon-5 | 2.70 | 750
(1 row)

entity_id | name | age_in_eons | energy_level | mass_in_solar_masses | habitable
-----------+-----------+-------------+--------------+----------------------+-----------
2 | Nebulon-5 | 2.70 | 750 | 1.23456 | t

We can see that only the parent table was truncated. The child table’s data still exists.

Now let’s run it without the ONLY keyword:

TRUNCATE TABLE cosmic_entities;

SELECT * FROM cosmic_entities;
SELECT * FROM celestial_bodies;

Result:

 entity_id | name | age_in_eons | energy_level 
-----------+------+-------------+--------------
(0 rows)

barney=# SELECT * FROM celestial_bodies;
entity_id | name | age_in_eons | energy_level | mass_in_solar_masses | habitable
-----------+------+-------------+--------------+----------------------+-----------
(0 rows)

This time the child table’s data was truncated too.

Asterisk (*)

We also have the option of including an asterisk (*) after the table’s name to get the same effect as the previous example – to remove data from any child tables.

Let’s try that out. First, we’ll repopulate the tables:

-- Insert one row into the parent table
INSERT INTO cosmic_entities (name, age_in_eons, energy_level)
VALUES ('Azathoth', 13.8, 9999);

-- Insert one row into the child table
INSERT INTO celestial_bodies (name, age_in_eons, energy_level, mass_in_solar_masses, habitable)
VALUES ('Nebulon-5', 2.7, 750, 1.23456, true);

SELECT * FROM cosmic_entities;
SELECT * FROM celestial_bodies;

Result:

 entity_id |   name    | age_in_eons | energy_level 
-----------+-----------+-------------+--------------
3 | Azathoth | 13.80 | 9999
4 | Nebulon-5 | 2.70 | 750
(2 rows)

entity_id | name | age_in_eons | energy_level | mass_in_solar_masses | habitable
-----------+-----------+-------------+--------------+----------------------+-----------
4 | Nebulon-5 | 2.70 | 750 | 1.23456 | t
(1 row)

Now let’s try out the * option:

TRUNCATE TABLE cosmic_entities *;

SELECT * FROM cosmic_entities;
SELECT * FROM celestial_bodies;

Result:

 entity_id | name | age_in_eons | energy_level 
-----------+------+-------------+--------------
(0 rows)

barney=# SELECT * FROM celestial_bodies;
entity_id | name | age_in_eons | energy_level | mass_in_solar_masses | habitable
-----------+------+-------------+--------------+----------------------+-----------
(0 rows)

Both tables truncated as expected.

Combining Options

We can combine multiple options in a single TRUNCATE command.

Example:

TRUNCATE TABLE planets, explorers, employees
RESTART IDENTITY
CASCADE;

Output:

NOTICE:  truncate cascades to table "employee_projects"
TRUNCATE TABLE

This truncated all tables as specified, along with any descendent tables. It also restarted any sequences and it cleared out any tables with foreign key references to the named tables.

Conclusion

The TRUNCATE statement in PostgreSQL offers a fast and efficient way to remove all data from one or more tables. By understanding its various options, we can tailor its behaviour to suit our specific needs, whether it’s resetting identity columns, handling foreign key constraints, or working with table inheritance.