By John Doe April 1, 2026
Summary: In this article, we will examine the VACUUM overhead caused by constraint violations when inserting data into PostgreSQL tables, as well as the corresponding solutions.
Table of Contents
Preparing Test Data
First, we create a products table and an orders table in a PostgreSQL database:
CREATE TABLE products (
product_id serial PRIMARY KEY, -- Product ID
product_name varchar(100), -- Product name
price numeric(10,2), -- Unit price
stock int -- Stock quantity
);
CREATE TABLE orders (
order_id serial PRIMARY KEY, -- Order ID
product_id int, -- Associated product ID
quantity int, -- Purchase quantity
amount numeric(12,2), -- Total order amount
order_time timestamp, -- Order time
CONSTRAINT fk_order_product FOREIGN KEY (product_id)
REFERENCES products(product_id)
);
Generate 100 product records with names ranging from prod_001 to prod_100, followed by 10,000 order records:
INSERT INTO products (product_name, price, stock)
SELECT
'prod_' || lpad(seq::text, 3, '0'),
round( (random() * 980 + 19.99)::numeric, 2 ),
floor(random() * 991 + 10)::int
FROM generate_series(1, 100) AS seq;
INSERT INTO orders (product_id, quantity, amount, order_time)
SELECT
t.product_id,
t.quantity,
round((p.price * t.quantity)::numeric, 2),
now() - (random() * INTERVAL '365 days')
FROM (
SELECT
floor(random() * 100 + 1)::int AS product_id,
floor(random() * 10 + 1)::int AS quantity
FROM generate_series(1, 10000)
) t
JOIN products p ON p.product_id = t.product_id;
VACUUM (analyze) products;
VACUUM (analyze) orders;
Unique Constraint Violation on Insert
Let’s execute a simple INSERT statement:
INSERT INTO orders (order_id, product_id,
quantity, amount, order_time)
VALUES (1, 20, 5, 5 * 100, now());
ERROR: duplicate key value violates unique constraint "orders_pkey"
DETAIL: Key (order_id)=(1) already exists.
SELECT schemaname, relname, n_live_tup, n_dead_tup
FROM pg_stat_all_tables WHERE relname = 'orders';
schemaname | relname | n_live_tup | n_dead_tup
------------+---------+------------+------------
public | orders | 10000 | 1
(1 row)
As shown, the error occurs because the insert statement violates the primary key constraint. The tuple that violated the primary key is now a dead tuple.
In mainstream relational databases, developers often use exception code block in stored procedures, to handle errors like primary key constraint violations:
CREATE OR REPLACE FUNCTION sell_product_func
(ord_id int, prod_id int, qty int)
RETURNS void AS $$
DECLARE
p_price numeric(10,2);
BEGIN
SELECT price INTO p_price FROM products
WHERE product_id = prod_id;
BEGIN
INSERT INTO orders (order_id, product_id,
quantity, amount, order_time)
VALUES (ord_id, prod_id,
qty, qty * p_price, now());
EXCEPTION
WHEN unique_violation THEN
UPDATE orders SET
product_id = prod_id,
quantity = qty,
amount = qty * p_price,
order_time = now()
WHERE order_id = ord_id;
END;
END; $$ LANGUAGE plpgsql;
In the stored procedure above, if an insert operation encounters a primary key constraint violation, it falls back to an UPDATE to modify the order.
-- Fall back to update after primary key conflict on insert
SELECT * FROM sell_product_func(1, 20, 5);
SELECT schemaname, relname, n_live_tup, n_dead_tup
FROM pg_stat_all_tables WHERE relname = 'orders';
schemaname | relname | n_live_tup | n_dead_tup
------------+---------+------------+------------
public | orders | 10000 | 3
(1 row)
As shown, two new dead tuples are created: one from the failed INSERT and another from the UPDATE. In such cases, the specific ON CONFLICT UPDATE clause can be used to reduce the occurrence of dead tuples in PostgreSQL.
Foreign Key Constraint Violation on Insert
We perform a simple insert that violates the foreign key constraint and check for dead tuples:
INSERT INTO orders (order_id, product_id,
quantity, amount, order_time)
VALUES (10001, 102, 5, 5 * 100, now());
ERROR: insert or update on table "orders" violates foreign key constraint "fk_order_product"
DETAIL: Key (product_id)=(102) is not present in table "products".
SELECT schemaname, relname, n_live_tup, n_dead_tup
FROM pg_stat_all_tables WHERE relname = 'orders';
schemaname | relname | n_live_tup | n_dead_tup
------------+---------+------------+------------
public | orders | 10000 | 4
(1 row)
As expected, the foreign key violation during insertion results in an additional dead row.
Note: When INSERTs frequently violate foreign key constraints, using the
ON CONFLICTclause can not reduce the occurrence of dead tuples.
Conclusion
From the tests above, frequent and large-scale constraint violations in applications can also result significant VACUUM overhead in PostgreSQL.
In addition, Redrock Postgres introduces an undo log mechanism that immediately rolls back table modifications when an insert violates a constraint, thus avoiding extra storage space consumption.