PostgreSQL,触发器和并发来强制执行临时密钥

时间:2010-06-09 07:20:38

标签: postgresql concurrency triggers

我想在PostgreSQL中定义一个触发器来检查通用表上的插入行是否具有属性:“在同一有效时间内没有其他行存在相同的键”(键是顺序键) 。事实上,我已经实现了它。但由于触发器必须扫描整个表,现在我想知道:是否需要表级锁?或者这是由PostgreSQL本身管理的?

这是一个例子。 在即将发布的PostgreSQL 9.0中,我会以这种方式定义表:

CREATE TABLE medicinal_products
(
aic_code CHAR(9), -- sequenced key
full_name VARCHAR(255),
market_time PERIOD,
    EXCLUDE USING gist
    (aic_code CHECK WITH =,
    market_time CHECK WITH &&)
);

但实际上我的定义是这样的:

CREATE TABLE medicinal_products
(
PRIMARY KEY (aic_code, vs),
aic_code CHAR(9), -- sequenced key
full_name VARCHAR(255),
vs DATE NOT NULL,
ve DATE,
CONSTRAINT valid_time_range
        CHECK (ve > vs OR ve IS NULL)
);

然后,我写了一个检查费用的触发器:“两种不同的药品可以在两个不同的时间段内具有相同的代码,但不能在同一时间内”。

所以代码:

INSERT INTO medicinal_products VALUES ('1','A','2010-01-01','2010-04-01');
INSERT INTO medicinal_products VALUES ('1','A','2010-03-01','2010-06-01');

返回错误。

3 个答案:

答案 0 :(得分:1)

一种解决方案是使用第二个表来检测碰撞,并用触发器填充它。使用您添加到问题中的架构:

CREATE TABLE medicinal_product_date_map(
   aic_code char(9) NOT NULL,
   applicable_date date NOT NULL,
   UNIQUE(aic_code, applicable_date));

(注意:这是第二次尝试,因为第一次误读你的要求。希望这次是正确的。)

维护此表的一些功能:

CREATE FUNCTION add_medicinal_product_date_range(aic_code_in char(9), start_date date, end_date date)
RETURNS void STRICT VOLATILE LANGUAGE sql AS $$
  INSERT INTO medicinal_product_date_map
  SELECT $1, $2 + offset
  FROM generate_series(0, $3 - $2)
$$;
CREATE FUNCTION clr_medicinal_product_date_range(aic_code_in char(9), start_date date, end_date date)
RETURNS void STRICT VOLATILE LANGUAGE sql AS $$
  DELETE FROM medicinal_product_date_map
  WHERE aic_code = $1 AND applicable_date BETWEEN $2 AND $3
$$;

首次填写表格:

SELECT count(add_medicinal_product_date_range(aic_code, vs, ve))
FROM medicinal_products;

现在创建触发器以在更改medicinal_products后填充日期映射:插入调用add_后,更新后调用clr_(旧值)和add_(新值),删除后调用clr _。

CREATE FUNCTION sync_medicinal_product_date_map()
RETURNS trigger LANGUAGE plpgsql AS $$
BEGIN
  IF TG_OP = 'UPDATE' OR TG_OP = 'DELETE' THEN
    PERFORM clr_medicinal_product_date_range(OLD.aic_code, OLD.vs, OLD.ve);
  END IF;
  IF TG_OP = 'UPDATE' OR TG_OP = 'INSERT' THEN
    PERFORM add_medicinal_product_date_range(NEW.aic_code, NEW.vs, NEW.ve);
  END IF;
  RETURN NULL;
END;
$$;
CREATE TRIGGER sync_date_map
  AFTER INSERT OR UPDATE OR DELETE ON medicinal_products
  FOR EACH ROW EXECUTE PROCEDURE sync_medicinal_product_date_map();

medicinal_product_date_map上的唯一性约束将捕获在同一天使用相同代码添加的任何产品:

steve@steve@[local] =# INSERT INTO medicinal_products VALUES ('1','A','2010-01-01','2010-04-01');
INSERT 0 1
steve@steve@[local] =# INSERT INTO medicinal_products VALUES ('1','A','2010-03-01','2010-06-01');
ERROR:  duplicate key value violates unique constraint "medicinal_product_date_map_aic_code_applicable_date_key"
DETAIL:  Key (aic_code, applicable_date)=(1        , 2010-03-01) already exists.
CONTEXT:  SQL function "add_medicinal_product_date_range" statement 1
SQL statement "SELECT add_medicinal_product_date_range(NEW.aic_code, NEW.vs, NEW.ve)"
PL/pgSQL function "sync_medicinal_product_date_map" line 6 at PERFORM

这取决于检查具有离散空间的值 - 这就是我询问日期与时间戳的原因。虽然时间戳在技术上是离散的,因为Postgresql只存储微秒分辨率,但是产品适用的每微秒在地图表中添加一个条目是不切实际的。

话虽如此,你可能还会得到一些比全表扫描更好的东西,以检查重叠的时间戳间隔,但是在寻找不是之前或之后的第一个间隔时会有一些诡计......但是,对于容易离散的空间我更喜欢这种方法,IME也可以用于其他方面(例如需要快速找到哪些产品在某一天适用的报告)。

我也喜欢这种方法,因为以这种方式利用数据库的唯一性约束机制是正确的。此外,我觉得在主表的并发更新的上下文中它会更可靠:没有锁定表以防止并发更新,验证触发器可能看不到冲突并允许插入两个并发会话,即当交易的影响都可见时,就会发现冲突。

答案 1 :(得分:0)

只是想一想,如果有效时间块可以用数字或类似的东西编码,在Id + TimeBlock上创建一个UNIQUE索引会非常快,并解决所有表锁问题。

它由PostgreSQL本身管理。在select上它获取了一个ACCESS_SHARE锁,这意味着您可以查询表但不执行更新。

一个可以帮助你的根本解决方案是使用像ehcache或memcached这样的缓存来存储id / timeblock信息,而根本不使用postgresql。许多可以被持久化,因此它们可以在服务器重启后继续存在,并且它们不会表现出这种锁定行为。

答案 2 :(得分:-1)

为什么不能使用UNIQUE约束?将更快(它是一个索引)并且更容易。