I have an events
table which is a denormalized table containing item
data. There is a one-to-many relationship between items and events.
CREATE TABLE events
(
id UUID PRIMARY KEY,
item VARCHAR(16) NOT NULL,
created TIMESTAMPTZ NOT NULL...
)
I also have a SELECT DISTINCT
view called items
which essentially takes the last event row for each item
. I would really like to be able to insert into that view as though it were a physical table, including a constraint error if the user-provided item
ID already exists.
I believe I need to create a trigger function, but I'm not sure what the query would look like to return a constraint violation (or maybe some other exception that callers can catch?). I think trigger functions have to be PL/pgSQL, but ideally it would hew as closely as possible to plain SQL.
1 Answer 1
Let's say that the view is defined as
CREATE VIEW event_items AS SELECT DISTINCT item FROM events;
Writing a trigger like that is not difficult; the problem is that such triggers always suffer from race conditions with concurrent INSERT
s.
There are only two ways to avoid race conditions:
Lock all affected rows to serialize operations:
CREATE FUNCTION ins_trig() RETURNS trigger LANGUAGE plpgsql AS $$BEGIN PERFORM FROM events WHERE item = NEW.item FOR NO KEY UPDATE; IF FOUND THEN RAISE EXCEPTION unique_violation USING MESSAGE = 'duplicate key value violates unique constraint', DETAIL = format('Key (item)=(%s) already exists.', NEW.item); END IF; INSERT INTO events (id, item, created) VALUES (gen_random_uuid(), NEW.item, current_timestamp); RETURN NULL; END;$$; CREATE TRIGGER ins_trig INSTEAD OF INSERT ON event_items FOR EACH ROW EXECUTE FUNCTION ins_trig();
Set
default_transaction_isolation = 'serializable'
to use theSERIALIZABLE
isolation level throughout and remove theFOR NO KEY UPDATE
in the above example.For that to work well, you need an index on
events.items
, otherwise you'll get predicate locks on the whole table and very bad concurrency.Note that with
SERIALIZABLE
isolation you have to be prepared to get serialization errors, which require you to retry theINSERT
until it succeeds.
See my article for some background information.
items
table, which will obviously fail if the corresponding unique constraint is violated, so I'm not really sure what is the problem.items
isn't a table, is a view into theevents
table. Consequently there is nounique
constraint to violate (the only unique constraint in the system isevents.id
).events
is obviously designed to hold multiple rows per item. Why not create an actualitem
table with unique items? (Or is that an option?)