【问题标题】:PostgreSQL 9.6 stored procedure performance improvementPostgreSQL 9.6 存储过程性能提升
【发布时间】:2018-04-19 16:03:41
【问题描述】:

我有两个表usersproducts,它们之间的关联是User has many products。我想将产品数量存储在用户表中,并且应该在每次插入或删除时更新。所以我在数据库中为它编写了一个存储过程并触发它来触发它。问题是当我一次插入数千个产品时,它正在执行触发器per row insertion,并且花费了太多时间。

  CREATE FUNCTION update_product_count()
  RETURNS trigger AS $$
  BEGIN
    IF TG_OP = 'DELETE' THEN
      UPDATE users SET products_count = products_count - 1 WHERE id = OLD.user_id;
    END IF;

    IF TG_OP = 'INSERT' THEN
      UPDATE users SET products_count = products_count + 1 WHERE id = NEW.user_id;
    END IF;

    RETURN NULL;
  END;
  $$ LANGUAGE plpgsql;

  CREATE TRIGGER update_user_products_count
  AFTER INSERT OR UPDATE OR DELETE ON products
  FOR EACH ROW EXECUTE PROCEDURE update_product_count();

更新

  1. 我已添加:SET CONSTRAINTS update_promotion_products_count DEFERRED; 但似乎它没有取得任何进展,因为现在它需要 6100 毫秒,这与以前有点相似。

  2. 试过DEFERRABLE INITIALLY DEFERRED,但还是不行。我认为FOR EACH ROW 是实际问题。但是当我用FOR EACH STATEMENT 尝试它时,它会抛出语句无效错误。

  3. 把上面的程序改写成这样:

    CREATE FUNCTION update_product_count()
     RETURNS trigger AS $$
      BEGIN
        IF TG_OP = 'DELETE' OR TG_OP = 'INSERT' THEN
          UPDATE users SET products_count = (SELECT COUNT(1) FROM products WHERE products.user_id = users.id);
        END IF;
    
        RETURN NULL;
      END;
      $$ LANGUAGE plpgsql;
    
    CREATE TRIGGER update_user_products_count
    AFTER INSERT OR UPDATE OR DELETE ON products
    FOR EACH STATEMENT EXECUTE PROCEDURE update_product_count();
    

但问题是当你有 1000 个用户,每个用户有 10000 个产品时,你重新计算每个用户的计数(即使只是在数据库中插入一个产品)

我使用的是 PostgreSQL 9.6。

【问题讨论】:

  • 您使用的是哪个 Postgres 版本?使用 Postgres 10,您可以使用语句级触发器来做到这一点
  • 版本为9.6。在问题中更新。
  • @a_horse_with_no_name 有什么想法吗?
  • @Ahmad:您可以编写一个将临时表中的更改排队的行级触发器,以及一个在末尾应用更改的语句级触发器:stackoverflow.com/a/47909709
  • @NickBarnes 很抱歉,但实际上我并不完全理解。如果您可以在答案中写下问题的上下文,那就太好了,这样我就可以更好地与您讨论并将其标记为已接受的答案。

标签: postgresql stored-procedures triggers database-trigger


【解决方案1】:

在您的情况下,当产品的 user_id 发生变化时,计数不会更新, 所以,我会推荐counter_cache的rails

class Product < ActiveRecord::Base
  belongs_to :user, counter_cache: true
end

也看看这个gem

注意:- 虽然这不会解决您的per row insertion 问题

然后你必须编写自定义计数器,如下所示

class Product < ApplicationRecord
  has_many :products
  attr_accessor :update_count

  belongs_to :user#, counter_cache: true

  after_save do
    update_counter_cache
  end

  after_destroy do
    update_counter_cache
  end

  def update_counter_cache
    return unless update_count
    user.products_count = user.products.count
    user.save
  end
end

在 Rails 控制台中

10.times{|n| Product.new(name: "Latest New Product #{n}", update_count: n == 9, user_id: user.id).save}

【讨论】:

  • 每行插入是我的问题,因为我一次插入 20k 条记录。
【解决方案2】:

正如 cmets 中提到的 a_horse_with_no_name ,Postgres 10 可以使用 FOR EACH STATEMENT 触发器更有效地执行此操作,该触发器根据语句的 transition table 一次更新所有 users 记录。

在早期版本中,您可以通过在临时表中对更改进行排队并在语句末尾使用单个 UPDATE 应用它们来获得一些好处。

在语句开头初始化队列:

CREATE FUNCTION create_queue_table() RETURNS TRIGGER LANGUAGE plpgsql AS $$
BEGIN
  CREATE TEMP TABLE pending_changes(user_id INT UNIQUE, count INT) ON COMMIT DROP;
  RETURN NULL;
END
$$;

CREATE TRIGGER create_queue_table_if_not_exists
  BEFORE INSERT OR UPDATE OF user_id OR DELETE
  ON products
  FOR EACH STATEMENT
  WHEN (to_regclass('pending_changes') IS NULL)
  EXECUTE PROCEDURE create_queue_table();

记录每一行的变化:

CREATE FUNCTION queue_change() RETURNS TRIGGER LANGUAGE plpgsql AS $$
BEGIN
  IF TG_OP IN ('DELETE', 'UPDATE') THEN
    INSERT INTO pending_changes (user_id, count) VALUES (old.user_id, -1)
    ON CONFLICT (user_id) DO UPDATE SET count = pending_changes.count - 1;
  END IF;

  IF TG_OP IN ('INSERT', 'UPDATE') THEN
    INSERT INTO pending_changes (user_id, count) VALUES (new.user_id, 1)
    ON CONFLICT (user_id) DO UPDATE SET count = pending_changes.count + 1;
  END IF;
  RETURN NULL;
END
$$;

CREATE TRIGGER queue_change
  AFTER INSERT OR UPDATE OF user_id OR DELETE
  ON products
  FOR EACH ROW
  EXECUTE PROCEDURE queue_change();

在语句末尾应用更改:

CREATE FUNCTION process_pending_changes() RETURNS TRIGGER LANGUAGE plpgsql AS $$
BEGIN
  UPDATE users
  SET products_count = products_count + pending_changes.count
  FROM pending_changes
  WHERE users.id = pending_changes.user_id;

  DROP TABLE pending_changes;
  RETURN NULL;
END
$$;

CREATE TRIGGER process_pending_changes
  AFTER INSERT OR UPDATE OF user_id OR DELETE
  ON products
  FOR EACH STATEMENT
  EXECUTE PROCEDURE process_pending_changes();

这可能会或可能不会明显更快,具体取决于您的案例的详细信息,但它在人工测试中的表现要好得多(184ms4073ms)。

正如我在similar answer 中指出的那样,此实现存在一些潜在的死锁,如果您同时运行它,您可能希望解决这些死锁。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2010-09-12
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-01-28
    相关资源
    最近更新 更多