0

I have a question on how to elaborate a schema in PostgreSQL which involves:

  • A trigger function used to keep a certain consistency calculation in a table;
  • A foreign key whose value in the original table can change, but we want to avoid unnecessary calculations in that case (that is, we want the change to cascade without recalculating everything somehow).

This is better explained with an example. Consider the following:

-- Contains the ids.
create table keys (
    id int4 not null,
    constraint keys_pk primary key (id)
);

-- Contain values associated with an id.
create table values (
    id int4 not null,
    value int4 not null,
    constraint values_fk foreign key (id) references keys(id) on update cascade
);

-- Keeps track of the sum of all values associated with an id.
create table totals (
    id int4 not null,
    total int4 not null,
    constraint totals_pk primary key (id)
);

The goal here is to have totals contain the sum of all values for every key. We do this by using a trigger such as:

create function calc_totals()
returns trigger language plpgsql as $$
begin
   -- subtract old value
   if not old is null then
       update totals set total = totals.total - old.value where id = old.id;
   end if;

   -- insert/add new value
   if not new is null then
       insert into totals values (new.id, new.value) on conflict (id) do update
       set total = totals.total + new.value;
   end if;

   -- return
   if not new is null then return new; else return old; end if;
end $$;

create or replace trigger calc_totals_trigger
before insert or update or delete on values
for each row execute function calc_totals();

With the above, editing values in any way (inserting, updating, deleting) will trigger calc_totals() and the totals table will be kept consistent.

Now, if for whatever reason we allow id in keys to change (that is, we want to "rename" an entity, and preserve all foreign key associations in other tables), the above will still work; however the database will recalculate the sums for the entire subset of rows which has its id changed in values (and leave a row with a total sum of zero for the previous id).

I would like to do better: somehow make the change in id in keys propagate to totals as well.

The most natural way would be add a foreign key with on cascade update into totals. This will not work, however, because although the update on id will cascade to totals (effectively renaming the row as intended), the trigger will still fire, causing a recalculation and yielding errors because id on totals have changed before the trigger was run.

Given that changing IDs is a bad idea in general but we still want to do it anyway in this case, is it possible to design this in a better way?

4
  • How often is the total accessed vs. how often are the values changed? If you don't look at the total that much, it might be easier to create a view that calculates it on demand. Also I'm not entirely sure your scheme is concurrent-safe, unless you have explicit locking in all of your queries.
    – user234725
    Commented Nov 19, 2022 at 4:01
  • @dwhitemv They are both read from and written to frequently... I have implemented a view using aggregate functions that works very well, but in large datasets it is extremely slow.
    – alecov
    Commented Nov 21, 2022 at 17:45
  • It’s probably worth spending some time figuring out why the view becomes slow. Your running-total scheme will need strict serialization… look at any book about concurrency, managing a running balance is usually the first example of how things go wrong when there is more than one actor.
    – user234725
    Commented Nov 21, 2022 at 20:32
  • @dwhitemv Thanks for the tip, I will definitely look into that. I think the reason why the view is slow is just because the tables are large (which ofc does not rule out an error or inefficient implementation from my part).
    – alecov
    Commented Nov 22, 2022 at 18:27

0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.