阻止连续的重复值而无需触发器

问题描述

一个组中,我想防止INSERT连续重复的值,其中“连续”由一个简单的ORDER BY子句定义。

想象一下一组定期从传感器采样值的实验。我们只想在该实验的新值 中插入一个值。

请注意,旧值 可以重复。因此,这是允许的:

id    experiment    value
 1             A       10
 2             A       20
 3             A       10

但这不是:

id    experiment    value
 1             A       10
 2             A       10

我知道如何为每个实验找到先前的值:

  SELECT
  *,lag(sample_value) OVER experiment_and_id
  FROM new_samples
  WINDOW experiment_and_id AS (
    PARTITION BY experiment
    ORDER BY id
  );

根据the docs,我知道CHECK约束不允许在检查中使用其他行:

Postgresql不支持CHECK约束,该约束引用表数据而不是被检查的新行或更新行。尽管违反此规则的CHECK约束似乎可以在简单测试中起作用,但它不能保证数据库不会达到约束条件为假(由于所涉及的其他行的后续更改)而导致的状态。这将导致数据库转储和重新加载失败。即使整个数据库状态与约束一致,重装也可能失败,这是因为未按满足约束的顺序加载行。如果可能,请使用UNIQUE,EXCLUDE或FOREIGN KEY约束来表示跨行和跨表的约束。

如果您希望在插入行时一次性检查其他行,而不是连续保持一致性保证,则可以使用自定义触发器来实现这一点。 (这种方法避免了转储/重装问题,因为pg_dump直到重载数据后才重新安装触发器,这样在转储/重装过程中不会强制执行检查。)

EXCLUDE约束看起来很有希望,但主要用于测试不相等的情况。而且我不确定是否可以在其中包含窗口功能

因此,我剩下一个自定义触发器,但这对于一个相当常见的用例来说似乎有点hacking。

任何人都可以改善触发器的使用吗?

理想情况下,我想说:

INSERT ....
ON CONFLICT DO nothing

让Postgres处理其余的事情!


最少的工作示例

BEGIN;
  CREATE TABLE new_samples (
    id INT GENERATED ALWAYS AS IDENTITY,experiment VARCHAR,sample_value INT
  );

  INSERT INTO new_samples(experiment,sample_value)
  VALUES
  ('A',1),-- This is fine because they are for different groups
  ('B',-- This is fine because the value has changed
  ('A',2),-- This is fine because it's different to the prevIoUs value in
  -- experiment A.
  ('A',-- Two is not allowed here because it's the same as the value
  -- before it,within this experiment.
  ('A',1);

  SELECT
  *,lag(sample_value) OVER experiment_and_id
  FROM new_samples
  WINDOW experiment_and_id AS (
    PARTITION BY experiment
    ORDER BY id
  );
ROLLBACK;

解决方法

如果示例不会更改,那么文档中引用的限制将与您的用例无关。

您可以创建一个函数来实现此目的:

create or replace function check_new_sample(_experiment text,_sample_value int)
  returns boolean as 
$$
  select _sample_value != first_value(sample_value) 
                            over (partition by experiment 
                                      order by id desc) 
    from new_samples
   where experiment = _experiment;
$$ language sql;

alter table new_samples add constraint new_samples_ck_repeat 
  check (check_new_sample(experiment,sample_value));

示例插入

insert into new_samples (experiment,sample_value) values ('A',1);
INSERT 0 1

insert into new_samples (experiment,sample_value) values ('B',2);
INSERT 0 1

insert into new_samples (experiment,1);
ERROR:  new row for relation "new_samples" violates check constraint "new_samples_ck_repeat"
DETAIL:  Failing row contains (5,A,1).