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.
And testing it..
Additional Notes
- An operation that modifies zero rows will still result in the execution of a statement trigger.
Instead Of
triggers on views can only befor each row
.- Statement triggers can be called for
Truncate
statements. - The
when
condition is borderline useless in statement level triggers, because you can’t refer to any values in the table. - Transition relations are only available in
After
triggers that are notconstraint
triggers.- If the trigger is an
update
trigger, you can’t specify acolumn_name
list.
- 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:
And then create a few members:
We need to make sure we can handle bulk queries - where multiple rows affect a single referrer. Let’s test that.
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:
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.
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.
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.
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.