插入违反约束产生的 VACUUM 开销

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 引入了回滚日志,在插入违反约束时,会立即回滚对表进行的更改,这样就不会占用更多的存储空间。