We have been writing blog posts about how to write simple extensions in C language and a little more complex one by Ibrar which were well received by PostgreSQL user community. Then we observed that many PostgreSQL users create simple triggers for small auditing requirements, and then feel the pain of trigger on transactions. So we were discussing how simple/lightweight and faster a trigger function is when written in C. Generally, Trigger functions are written in high-level languages like PlpgSQL, but it has a higher overhead during execution and it can impact the transactions – and thereby application performance.
This blog post is an attempt to create a simple trigger function to address one of the common use-cases of triggers, which is to update auditing columns in a table.
In this post, we are going to introduce SPI (Server Programming Interface) functions for novice users. Towards the end of the blog, we share some of the quick benchmark results for understanding the benefits.
Let’s proceed with taking up a case and assume that we have a table to hold transaction details. But auditing requirements say that there should be a timestamp on each tuple when the tuple is inserted and when it was last updated.
|
1 |
CREATE TABLE transdtls(<br> transaction_id int,<br> cust_id int,<br> amount int,<br>...<br> insert_ts timestamp,<br> update_ts timestamp<br>); |
For demonstration purpose, let’s remove and trim the other columns and create a table with only 3 essential columns.
|
1 |
CREATE TABLE transdtls(<br> transaction_id int,<br> insert_ts timestamp,<br> update_ts timestamp<br>); |
The trigger function can also be developed and packaged as an extension, which we discussed in s previous blog post here. So we are not going to repeat those steps here. The difference is that file names are named as “trgr” instead of “addme” in the previous blog. Makefile is also modified to refer “trgr” files. This need not be same as the function name “trig_test” in the C source detailed below.
In the end, the following files are available in the development folder:
|
1 |
$ ls<br>Makefile trgr--0.0.1.sql trgr.c trgr.control |
The trgr.c is the main source files with the following content:
|
1 |
#include <stdio.h><br>#include <time.h><br>#include "postgres.h"<br>#include "utils/rel.h"<br>#include "executor/spi.h"<br>#include "commands/trigger.h"<br>#include "utils/fmgrprotos.h"<br>#ifdef PG_MODULE_MAGIC<br>PG_MODULE_MAGIC;<br>#endif<br><br>extern Datum trig_test(PG_FUNCTION_ARGS);<br><br>PG_FUNCTION_INFO_V1(trig_test);<br><br>Datum<br>trig_test(PG_FUNCTION_ARGS)<br>{<br> TriggerData *trigdata = (TriggerData *) fcinfo->context;<br> //TupleDesc tupdesc;<br> HeapTuple tuple;<br> HeapTuple rettuple;<br> int attnum = 0;<br> Datum datumVal;<br><br> //Get the structure of the tuple in the table.<br> //tupdesc = trigdata->tg_relation->rd_att;<br><br> //Make sure that the function is called from a trigger<br> if (!CALLED_AS_TRIGGER(fcinfo))<br> elog(ERROR, "are you sure you are calling from trigger manager?");<br><br> //If the trigger is part of an UPDATE event<br> if (TRIGGER_FIRED_BY_UPDATE(trigdata->tg_event))<br> {<br> //attnum = SPI_fnumber(tupdesc,"update_ts");<br> attnum = 3;<br> tuple = trigdata->tg_newtuple;<br> }<br> else //If the trigger is part of INSERT event<br> {<br> //attnum = SPI_fnumber(tupdesc,"insert_ts");<br> attnum = 2;<br> tuple = trigdata->tg_trigtuple;<br> }<br> //Get the current timestamp using "now"<br> datumVal = DirectFunctionCall3(timestamp_in, CStringGetDatum("now"), ObjectIdGetDatum(InvalidOid), Int32GetDatum(-1));<br><br> //Connect to Server and modify the tuple<br> SPI_connect();<br> rettuple = SPI_modifytuple(trigdata->tg_relation, tuple, 1, &attnum, &datumVal, NULL);<br> if (rettuple == NULL)<br> {<br> if (SPI_result == SPI_ERROR_ARGUMENT || SPI_result == SPI_ERROR_NOATTRIBUTE)<br> elog(ERROR, "SPI_result failed! SPI_ERROR_ARGUMENT or SPI_ERROR_NOATTRIBUTE");<br> elog(ERROR, "SPI_modifytuple failed!");<br> }<br> SPI_finish(); /* don't forget say Bye to SPI mgr */<br> return PointerGetDatum(rettuple);<br>}<br> |
and trgr--0.0.1.sql with the following content:
|
1 |
CREATE OR REPLACE FUNCTION trig_test() RETURNS trigger<br> AS 'MODULE_PATHNAME','trig_test'<br>LANGUAGE C STRICT;<br> |
Now it is a matter of building, installing, and creating the extension.
|
1 |
$ make<br>$ sudo make install<br>psql> create extension trgr; |
In case you don’t want to develop it as an extension, you may compile it to generate a shared object file (.so) file. Copy the same to the library folder of PostgreSQL binaries, which on my Ubuntu laptop is : /usr/lib/postgresql/11/lib/, and then define the function. You can even specify the full path of the shared object file like this:
|
1 |
CREATE FUNCTION trig_test() RETURNS trigger <br> AS '/usr/lib/postgresql/11/lib/trgr.so'<br>LANGUAGE C; |
Usage of trigger function is not different from regular PLpgSQL functions. You just need to attach the function to the table for all INSERT and UPDATE events.
|
1 |
CREATE TRIGGER transtrgr<br> BEFORE INSERT OR UPDATE ON public.transdtls <br>FOR EACH ROW EXECUTE PROCEDURE public.trig_test();<br> |
For a fair comparison with trigger function written in PLpgSQL, a similar function is created as follows:
|
1 |
CREATE OR REPLACE FUNCTION transtrgr_pl()<br> RETURNS TRIGGER AS $$<br> BEGIN<br> if (TG_OP = 'UPDATE') then<br> NEW.update_ts = now();<br> else <br> NEW.insert_ts = now();<br> end if;<br> RETURN NEW;<br> END;<br> $$ language 'plpgsql'; |
The number of lines and the readability of the code is in favor of PLpgSQL. The development and debugging time required is much less.
Regarding the performance benchmarking, three cases are compared.
Here are the performance numbers in milliseconds for 1 million bulk inserts, obviously a smaller number is better.


Resources
RELATED POSTS