由 John Doe 四月 1, 2026
摘要:在本文中,我们将了解插入 PostgreSQL 表违反约束时产生的 VACUUM 开销,以及相关的解决方案。
目录
准备测试数据
下面我们在 PostgreSQL 的数据库中,创建产品表 (products) 和订单表 (orders):
CREATE TABLE products (
product_id serial PRIMARY KEY, -- 产品ID
product_name varchar(100), -- 产品名称
price numeric(10,2), -- 产品单价
stock int -- 库存
);
CREATE TABLE orders (
order_id serial PRIMARY KEY, -- 订单ID
product_id int, -- 关联产品ID
quantity int, -- 购买数量
amount numeric(12,2), -- 订单总金额
order_time timestamp, -- 下单时间
CONSTRAINT fk_order_product FOREIGN KEY (product_id)
REFERENCES products(product_id)
);
生成 100 条产品测试数据,产品名为:prod_001 ~ prod_100,再生成 10000 条订单测试数据:
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;
插入时违反唯一性约束
我们来执行一条简单的插入语句:
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)
如您所见,错误原因是插入操作违反了主键,违反主键的元组现在是一个死元组。
对于主流的关系数据库,开发者经常会在存储过程中使用异常处理,比如用来处理主键约束冲突:
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;
在上面的存储过程中,当插入订单出现主键约束冲突时,转为使用 UPDATE 更新订单。
-- 插入主键冲突后转为更新
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)
如您所见,现在我们得到了两个新的死元组。一个是插入操作产生的,另一个是更新操作产生的。这种情况下,可以考虑使用 PostgreSQL 特定的 ON CONFLICT UPDATE 指令,来减少死元组的出现。
插入时违反外键约束
我们执行一个简单的插入操作,该操作会违反外键,并检查是否生成了死元组:
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)
果然,由于插入操作中存在外键冲突,现在出现了一个死行。
注意:在插入频繁违反外键约束时,使用
ON CONFLICT指令并不能减少死元组的出现。
结论
通过以上测试可知,如果应用程序频繁产生大量约束违规,也会给 PostgreSQL 产生 VACUUM 的开销。
另外,Redrock Postgres 引入了回滚日志,在插入违反约束时,会立即回滚对表进行的更改,这样就不会占用更多的存储空间。