Statement Triggers in Postgres
Postgres can define a trigger for each row, or for each statement.
For a while, statement triggers were not used much (I’ve never seen one in the wild actually), because they couldn’t access the rows modified by the statement.
But in Postgres 10, they introduced something called “transition tables” which allow you to access the rows that were modified by the statement.
I’ll give a quick example, adapted from this article.
1create table books (2  id serial primary key not null,3  name text not null4);5
6create or replace function show_modified_tups() returns trigger as $$7declare8r1 record;9begin10if (tg_op = 'DELETE') then11    for r1 in select * from old12    loop13        raise notice 'old: %', r1;14    end loop;15elsif (tg_op = 'UPDATE') then16    for r1 in select * from new17    loop18        raise notice 'new: %', r1;19    end loop;20    for r1 in select * from old21    loop22        raise notice 'old: %', r1;23    end loop;24elsif (tg_op = 'INSERT') THEN25    for r1 in select * from new26    loop27        raise notice 'new: %', r1;28    end loop;29end if;30return new;31end;32$$ language plpgsql;33
34create trigger log_books_inserted35after insert on books36referencing new table as new37for each statement38execute function show_modified_tups ();39
40create trigger log_books_updated41after update on books42referencing new table as new old table as old43for each statement44execute function show_modified_tups ();45
46create trigger log_books_deleted47after delete on books48referencing old table as old49for each statement50execute function show_modified_tups ();And testing it..
1insert into books (name) values2('The Great Gatsby'),3('The Catcher in the Rye'),4('Moonraker');5
6NOTICE:  new: (1,"The Great Gatsby")7NOTICE:  new: (2,"The Catcher in the Rye")8NOTICE:  new: (3,Moonraker)1update books2  set name = 'THE GREAT GATSBY'3where id = 1;4
5NOTICE:  new: (1,"THE GREAT GATSBY")6NOTICE:  old: (1,"The Great Gatsby")1delete from books2where id = 2;3
4NOTICE:  old: (2,"The Catcher in the Rye")Additional Notes
- An operation that modifies zero rows will still result in the execution of a statement trigger.
- Instead Oftriggers on views can only be- for each row.
- Statement triggers can be called for Truncatestatements.
- The whencondition is borderline useless in statement level triggers, because you can’t refer to any values in the table.
- Transition relations are only available in Aftertriggers that are notconstrainttriggers.- If the trigger is an updatetrigger, you can’t specify acolumn_namelist.
 
- If the trigger is an 
This table from the Postgres documentation is a good summary of the differences between row and statement triggers:
| When | Event | Row-level | Statement-level | 
|---|---|---|---|
| Before | Insert / Update / Delete | Tables and foreign tables | Tables, views, and foreign tables | 
| Before | Truncate | — | Tables and foreign tables | 
| After | Insert / Update / Delete | Tables and foreign tables | Tables, views, and foreign tables | 
| After | Truncate | — | Tables and foreign tables | 
| Instead Of | Insert / Update / Delete | Views | — | 
| Instead Of | Truncate | — | — | 
Revisiting The Pyramid Scheme
In my last blog post, I created an example of triggers killing performance and how to work around it.
Let’s see if statement triggers would have been a useful solution. The full example would look like this:
1drop table if exists members;2
3create table members (4  id serial primary key,5  name text not null,6  revenue numeric(10, 2) not null default 0,7  referred_revenue numeric(10, 2) not null default 0,8
9  referred_by integer references members (id)10);11
12create or replace function update_referred_revenue_on_insert() returns trigger as $$13begin14  update members15    set referred_revenue = referred_revenue + total_revenue16  from (17    select referred_by, sum(revenue) as total_revenue18    from new19    group by referred_by20    having referred_by is not null21  ) as new_referrals22  where members.id = new_referrals.referred_by;23
24  return new;25end;26$$ language plpgsql;27
28create trigger update_referred_revenue_on_insert29after insert on members30referencing new table as new31execute function update_referred_revenue_on_insert ();32
33create or replace function update_referred_revenue_on_update() returns trigger as $$34declare35  count_updated integer;36begin37  select count(*) into count_updated38  from new;39
40  if count_updated = 0 then41    return new;42  end if;43
44  update members45    set referred_revenue = referred_revenue + new_referrals.new_total_revenue46  from (47    select48      n.referred_by,49      sum(n.revenue - o.revenue) + sum(n.referred_revenue - o.referred_revenue) as new_total_revenue50    from new n51    join old o on n.id = o.id52    group by n.referred_by53    having n.referred_by is not null54  ) as new_referrals55  where members.id = new_referrals.referred_by;56
57  return new;58end;59$$ language plpgsql;60
61create trigger update_referred_revenue_on_update62after update on members63referencing new table as new old table as old64execute function update_referred_revenue_on_update ();And then create a few members:
1insert into members (name, revenue) values ('me', 100);2insert into members (name, revenue, referred_by) values ('Stella', 100, 1);3insert into members (name, revenue, referred_by) values ('Cornelius', 100, 1);4insert into members (name, revenue, referred_by) values ('Rita', 100, 2);5insert into members (name, revenue, referred_by) values ('Conrad', 100, 2);6insert into members (name, revenue, referred_by) values ('Helmut', 100, 3);7insert into members (name, revenue, referred_by) values ('Manfried', 100, 3);We need to make sure we can handle bulk queries - where multiple rows affect a single referrer. Let’s test that.
1insert into members (name, revenue, referred_by)2select 'member' || i::text, 10, 33from generate_series(1, 5) as i;Let’s take a look at the data:
| Name | Revenue | Referred Revenue | 
|---|---|---|
| me | 100.00 | 650.00 | 
| Stella | 100.00 | 200.00 | 
| Cornelius | 100.00 | 250.00 | 
| Rita | 100.00 | 0.00 | 
| Conrad | 100.00 | 0.00 | 
| Helmut | 100.00 | 0.00 | 
| Manfried | 100.00 | 0.00 | 
| member1 | 10.00 | 0.00 | 
| member2 | 10.00 | 0.00 | 
| member3 | 10.00 | 0.00 | 
| member4 | 10.00 | 0.00 | 
| member5 | 10.00 | 0.00 | 
Then let’s do another bulk update:
1update members2  set revenue = 03where id > 7;| Name | Revenue | Referred Revenue | 
|---|---|---|
| me | 100.00 | 600.00 | 
| Stella | 100.00 | 200.00 | 
| Cornelius | 100.00 | 200.00 | 
| Rita | 100.00 | 0.00 | 
| Conrad | 100.00 | 0.00 | 
| Helmut | 100.00 | 0.00 | 
| Manfried | 100.00 | 0.00 | 
| member1 | 0.00 | 0.00 | 
| member2 | 0.00 | 0.00 | 
| member3 | 0.00 | 0.00 | 
| member4 | 0.00 | 0.00 | 
| member5 | 0.00 | 0.00 | 
Lastly, let’s add a ton of new members, to show that our new triggers can handle it performance-wise.
1insert into members (name, revenue, referred_by)2select 'member' || i::text, 10, 33from generate_series(1, 1000000) as i;That takes ~6s, which is basically what the insert with no trigger takes.
What makes this complicated?
There are a couple new challenges.
The actual update calls need to handle the case where multiple new rows affect the same referrer. This is why we’re doing group by in the update statements.
e.g.
1update members2  set referred_revenue = referred_revenue + total_revenue3from (4  select referred_by, sum(revenue) as total_revenue5  from new6  group by referred_by7  having referred_by is not null8) as new_referrals9where members.id = new_referrals.referred_by;10
11-- and12update members13  set referred_revenue = referred_revenue + new_referrals.new_total_revenue14from (15  select16    n.referred_by,17    sum(n.revenue - o.revenue) + sum(n.referred_revenue - o.referred_revenue) as new_total_revenue18  from new n19  join old o on n.id = o.id20  group by n.referred_by21  having n.referred_by is not null22) as new_referrals23where members.id = new_referrals.referred_by;Furthermore, the update trigger is susceptible to an infinite loop, because it will get called even if no rows are updated. This is why we’re checking count_updated in the update trigger.
1select count(*) into count_updated2from new;3
4if count_updated = 0 then5  return new;6end if;Conclusion
In summary, statement triggers can be more performant than row level triggers. They’re likely an underused feature in Postgres.
Wow! You read the whole thing. People who make it this far sometimes
        want to receive emails when I post something new. 
 
 I also have an RSS feed.