PLpgsql BEFORE UPDATE with select from same table

限于喜欢 提交于 2019-12-08 09:16:25

问题


I'm trying to create a PL/PGSQL trigger function to check a new row's date range to ensure there are no other rows within the table for which the date ranges overlap (for the same product_id) . I have successully created the function and set it as a BEFORE INSERT trigger, but I'm trying to figure out how to also set it as a BEFORE UPDATE trigger, since the SELECT statement inside the trigger is certain to throw an exception because it fits the criteria of overlapping the date of an updated version of itself.

Here is my function:

CREATE OR REPLACE FUNCTION check_specials_dates() 
RETURNS trigger AS 
$$
DECLARE
BEGIN
  IF EXISTS (SELECT * FROM rar.product_specials 
           WHERE product_id = NEW.product_id 
             AND (
             (NEW.end_time between start_time and end_time) OR
             (NEW.start_time between start_time and end_time) OR
             (start_time between NEW.start_time and NEW.end_time))
  THEN
    RAISE EXCEPTION 
     'Cannot insert overlapping specials date for Product ID#%', NEW.product_id;   
 END IF; 
  RETURN NEW;
END
$$ LANGUAGE plpgsql;

My thought is that the IF EXISTS SELECT statement will return a match because it is going to match on the row it is trying to update.

Is this correct? If so, how can I get around it?


回答1:


Which version of PostgreSQL are you using? From 9.0 you might be able to implement all this using an exclusion constraint and the cube/btree_gist extensions.

To implement this using a trigger, I would generally use an after insert/update trigger which looked at other specials in with the same product ID but with a different primary key to the row being inserted/updated. That is:

IF EXISTS (SELECT 1 FROM rar.product_specials
           WHERE product_specials.product_id = NEW.product_id
                 AND product_specials.product_special_id <> NEW.product_special_id
                 AND overlaps(NEW.start_time, NEW.end_time,
                              product_specials.start_time, product_specials.end_time))

If you don't already have a generated primary key for product_specials, imho this would justify adding one.

Exclusion constraint solution

(Because I keep needing to remind myself how to do it, so I want to write it down somewhere)

(Just a note that if your start/end times are discrete (e.g. dates or you can fix your endpoints to a large enough granularity) then you can use a uniqueness constraint on an auxiliary table populated by triggers instead: PostgreSQL, triggers, and concurrency to enforce a temporal key)

PostgreSQL can use its extensive operators/indexing methods infrastructure to enforce generalised exclusion constraints- refuse to accept a row if any other row satisfies a set of operations. Traditional uniqueness constraints are essentially a special case of this- they cause rows to be refused if some value/set of values from the row are all equal to a value/set of values from some other row.

In your case, you want a row to be refused if, compared to some other row in the table, product_id is equal and the range (start_time,end_time) overlaps.

The indexing method "gist" can be used to build indices to satisfy this kind of request (specifically, the overlapping ranges). The extension "cube" provides a general data type that is gist-indexable for this, and "btree_gist" provides a gist index method for integers, allowing the two types to be combined in a single index.

So in PostgreSQL 9.1:

CREATE EXTENSION cube;
CREATE EXTENSION btree_gist;

(in 9.0, run the scripts from contrib)

Here's the example I tested with:

create table product_specials(product_special_id serial primary key,
    product_id int not null,
    start_time timestamp not null, end_time timestamp not null);
insert into product_specials(product_id, start_time, end_time)
 values(1, '2011-10-31 15:00:00', '2011-11-01 09:00:00'),
 (2, '2011-10-31 12:00:00', '2011-11-01 12:00:00'),
 (1, '2011-11-01 15:00:00', '2011-11-02 09:00:00');

Now, those ranges don't overlap so we can add the constraint:

alter table product_specials add constraint overlapping_times exclude using gist (
  product_id with = ,
  cube(extract(epoch from start_time), extract(epoch from end_time)) with &&
);

cube(n1, n2) creates a one-dimensional "cube" that extends from n1 to n2. extract(epoch from t) converts a timestamp t into a number. If you have two cubes, the "&&" operator returns true if they overlap. So this indexes the product_id and the start_time/end_time "cube" for each row, and every time you insert/update a row, the constraint is tested by looking for an existing row that matches the new row's values: testing product_id with the "=" operator, and the start_time/end_time "cube" with the "&&" operator.

If you try to insert a conflict row now, you will get an error:

insert into product_specials(product_id, start_time, end_time)
  values(2, '2011-10-31 00:00:00', '2011-10-31 13:00:00');
ERROR:  conflicting key value violates exclusion constraint "overlapping_times"
DETAIL:  Key (product_id, cube(date_part('epoch'::text, start_time), date_part('epoch'::text, end_time)))=(2, (1320019200),(1320066000)) conflicts with existing key (product_id, cube(date_part('epoch'::text, start_time), date_part('epoch'::text, end_time)))=(2, (1320062400),(1320148800)).

As you can see, the legibility of the error message detail leaves something to be desired! (The "period" type from the article http://thoughts.j-davis.com/2010/09/25/exclusion-constraints-are-generalized-sql-unique/ that @a_horse_with_no_name mentioned presumably produces better ones) However, the functionality is intact.

Using a constraint exclusion solves some niggly problems to do with locking that I haven't addressed. Strictly, before your "IF EXISTS ..." query in the trigger, you should do a SELECT 1 FROM rar.product_specials WHERE product_specials.product_id = NEW.product_id FOR SHARE to ensure none of the other rows you are testing against can be mutated between the constraint being checked and its transaction committing. However, there is still potentially a race condition when inserting two new specials at the same time, where there's nothing to lock--- this was the motivation for using an auxiliary table to exclude discrete values, but that has scaling issues as the exclusion space becomes more granular.

With PostgreSQL 9.2 there will be a "range" data type that will remove the need to use the cube extension or similar here. The range type also allows proper specification of whether bounds are open or closed at each end, whereas using cube bounds are always closed at both ends (so you need to do some fiddling to avoid errors about date ranges overlapping). Depesz has a good post on this feature, as usual: http://www.depesz.com/index.php/2011/11/07/waiting-for-9-2-range-data-types/

For example:

create table product_specials(product_special_id serial primary key,
    product_id int not null,
    applicable_dates tsrange not null);
insert into product_specials(product_id, applicable_dates)
 values(1, tsrange('2011-10-31 15:00:00', '2011-11-01 09:00:00')),
 (2, tsrange('2011-10-31 12:00:00', '2011-11-01 12:00:00')),
 (1, tsrange('2011-11-01 15:00:00', '2011-11-02 09:00:00'));
alter table product_specials add exclude using gist (
  product_id with =,
  applicable_dates with &&
);

Now if you try to insert a conflicting row, you get a more-readable error message too:

insert into product_specials(product_id, applicable_dates)    
  values(2, tsrange('2011-10-31 00:00:00', '2011-10-31 13:00:00'));
ERROR:  conflicting key value violates exclusion constraint "product_specials_product_id_applicable_dates_excl"
DETAIL:  Key (product_id, applicable_dates)=(2, ["2011-10-31 00:00:00","2011-10-31 13:00:00")) conflicts with existing key (product_id, applicable_dates)=(2, ["2011-10-31 12:00:00","2011-11-01 12:00:00")).

Note that you don't have to change the schema of the table to use this new type, since you can index the result of the function call. So the specifics of using the range type to enforce the constraint don't have to be put into the application or a trigger. That is:

alter table product_specials add exclude using gist (
  product_id with =,
  tsrange(start_time, end_time) with &&
);


来源:https://stackoverflow.com/questions/8182013/plpgsql-before-update-with-select-from-same-table

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!