5

Like detailed in my first question I have a Postgres 9.1 database with multiple tables that have the exact same column names, they only vary in their column values:

tbl_log_a
tbl_log_b
tbl_log_c
...

26 tables (from a to z). Each table has a trigger that calls a trigger function named trfn_tbl_log_%letter% (from a to z) which does the exact same thing:

CREATE OR REPLACE FUNCTION trfn_tbl_log_a_timetypespan()
  RETURNS trigger AS
$BODY$
DECLARE
v_timetype character varying;
v_timestmp_timetype timestamp without time zone;
v_timetypespan_resume interval;
v_stmtserial real;
v_sumtimetypespan_fnname interval;

BEGIN

IF NEW.timetype = 'lap' THEN
  SELECT timetype, timestmp, timetypespan FROM tbl_log_a WHERE fnname = NEW.fnname AND (timetype = 'start' OR timetype = 'resume') ORDER BY stmtserial DESC LIMIT 1 INTO v_timetype, v_timestmp_timetype, v_timetypespan_resume;
    IF v_timetype = 'start' THEN
      NEW.timetypespan := age(NEW.timestmp, v_timestmp_timetype);
    ELSIF v_timetype = 'resume' THEN
      SELECT timestmp FROM tbl_log_a WHERE fnname = NEW.fnname AND timetype = 'start' ORDER BY stmtserial DESC LIMIT 1 INTO v_timestmp_timetype;
      NEW.timetypespan := age(NEW.timestmp, v_timestmp_timetype) - v_timetypespan_resume;
    ELSE
      RAISE EXCEPTION USING MESSAGE = 'There is not any previous row...';
    END IF;

ELSIF NEW.timetype = 'resume' THEN
  SELECT timestmp FROM tbl_log_a WHERE fnname = NEW.fnname AND (timetype = 'start' OR timetype = 'lap') ORDER BY stmtserial DESC LIMIT 1 INTO v_timestmp_timetype;
    IF FOUND THEN
      NEW.timetypespan := age(NEW.timestmp, v_timestmp_timetype);
    ELSE
      RAISE EXCEPTION USING MESSAGE = 'There is not any previous row...';
    END IF;

ELSIF NEW.timetype = 'total' THEN
  SELECT stmtserial FROM tbl_log_a WHERE fnname = NEW.fnname AND timetype = 'total' ORDER BY stmtserial DESC LIMIT 1 INTO v_stmtserial;
  SELECT SUM(timetypespan) FROM (SELECT DISTINCT ON (floor(timeidx)::int) floor(timeidx)::int timeidx, timetypespan
  FROM tbl_log_a WHERE fnname = NEW.fnname AND timetype = 'lap' AND stmtserial > coalesce(v_stmtserial, 0) ORDER BY 1, 2 DESC) a INTO v_sumtimetypespan_fnname;
    IF v_sumtimetypespan_fnname NOTNULL THEN
      NEW.timetypespan := v_sumtimetypespan_fnname;
    ELSE
      RAISE EXCEPTION USING MESSAGE = 'There is not any previous row...';
    END IF;

END IF;
return NEW;

END
$BODY$
  LANGUAGE plpgsql VOLATILE;

Trigger definition:

CREATE TRIGGER trfn_tbl_log_a_timetypespan
  BEFORE INSERT ON tbl_log_a
  FOR EACH ROW EXECUTE PROCEDURE trfn_tbl_log_a_timetypespan();

So I have to create 26 trigger functions, one for each tbl_log_%letter%. I am trying to replace all of them with a single, generic trigger function.

We worked out a solution with dynamic SQL under my previous question with a simplified function. Can the same technique be extended to this more complex scenario?

EXECUTE format($$...
Natysiu16
  • 245
  • 2
  • 6

1 Answers1

1

Yes, this should work (untested):

CREATE OR REPLACE FUNCTION trfn_tbl_log_timetypespan()  -- generic name
  RETURNS trigger AS
$func$
DECLARE
   _timetype varchar;
   _timetypespan_resume interval;
   _ct int;
BEGIN

CASE NEW.timetype WHEN 'lap' THEN EXECUTE format($$ SELECT timetype, timetypespan, age($1, timestmp) FROM %s WHERE fnname = $2 AND timetype IN ('start', 'resume') ORDER BY stmtserial DESC LIMIT 1$$ , TG_RELID::regclass) USING NEW.timestmp, NEW.fnname INTO _timetype, _timetypespan_resume, NEW.timetypespan;

CASE _timetype WHEN 'start' THEN -- do nothing

WHEN 'resume' THEN EXECUTE format($$ SELECT age($1, timestmp) - _timetypespan_resume FROM %s WHERE fnname = $2 AND timetype = 'start' ORDER BY stmtserial DESC LIMIT 1$$ , TG_RELID::regclass) USING NEW.timestmp, NEW.fnname INTO NEW.timetypespan;

ELSE RAISE EXCEPTION 'There is no previous row.'; END CASE;

WHEN 'resume' THEN EXECUTE format($$ SELECT age($1, timestmp) FROM %s WHERE fnname = $2 AND timetype IN ('start', 'lap') ORDER BY stmtserial DESC LIMIT 1$$ , TG_RELID::regclass) USING NEW.timestmp, NEW.fnname INTO NEW.timetypespan;

GET DIAGNOSTICS _ct = ROW_COUNT;

IF _ct > 0 THEN -- do nothing ELSE RAISE EXCEPTION 'There is no previous row.'; END IF;

WHEN 'total' THEN EXECUTE format($$ SELECT COALESCE(SUM(timetypespan), $1) FROM ( SELECT floor(timeidx)::int, max(timetypespan) AS timetypespan FROM %1$s WHERE fnname = $2 AND timetype = 'lap' AND stmtserial > coalesce( (SELECT stmtserial FROM %1$s WHERE fnname = $2 AND timetype = 'total' ORDER BY stmtserial DESC LIMIT 1), 0) GROUP BY 1 ) sub$$ , TG_RELID::regclass) USING NEW.timetypespan, NEW.fnname INTO NEW.timetypespan;

GET DIAGNOSTICS _ct = ROW_COUNT;

IF _ct > 0 THEN -- do nothing ELSE RAISE EXCEPTION 'There is no previous row.'; END IF; END CASE;

RETURN NEW;

END $func$ LANGUAGE plpgsql;

This is a follow-up to my previous answer to your previous question. Find explanation there:

While being at it, I simplified a couple of things. Like: the second SELECT in your first ELSIF branch was just a repetition of the SELECT one level above. I merged that for no cost.

I also removed a couple of unnecessary intermediary steps and assigned to fields of NEW directly where applicable. That's why I could remove most of your variables.

Aside: If timeidx only has positive numbers you can use the cheaper trunc(timeidx) instead of floor(timeidx).

To understand the dynamic part more easily ...

If you would implement the same just for tbl_log_a - i.e., the effectively executed code for tbl_log_a (after applying format() and EXECUTE) looks like this:

...

CASE NEW.timetype WHEN 'lap' THEN SELECT timetype, timetypespan, age(NEW.timestmp, timestmp) FROM tbl_log_a WHERE fnname = NEW.fnname AND timetype IN ('start', 'resume') ORDER BY stmtserial DESC LIMIT 1 INTO _timetype, _timetypespan_resume, NEW.timetypespan;

CASE _timetype WHEN 'start' THEN -- do nothing

WHEN 'resume' THEN SELECT age(NEW.timestmp, timestmp) - _timetypespan_resume FROM tbl_log_a WHERE fnname = NEW.fnname AND timetype = 'start' ORDER BY stmtserial DESC LIMIT 1 INTO NEW.timetypespan;

...

Erwin Brandstetter
  • 175,982
  • 27
  • 439
  • 600
  • Thanks @Erwin Brandstetter, I wil test it, thanks again. – Natysiu16 Aug 28 '15 at 00:23
  • Thanks @Erwin Brandstetter. I have one doubt though, is there some performance lost by using this "dynamically determined table name" method compared to the set method (SELECT * FROM tbl_name)? thanks advanced. – Natysiu16 Aug 30 '15 at 22:32
  • Hi @Erwin Brandstetter, I have tried replacing floor(timeidx) with trunc(timeidx) in the Trigger Function, but then it shows me an error "near trunc" when running it. I have tried also truncate? am I doing something wrogn? Thanks Advanced. – Natysiu16 Aug 31 '15 at 00:27
  • @Natysiu16: Performance depends on many factors. Details: http://dba.stackexchange.com/a/8189/3684. You should be able to replace floor() with trunc(). What's the data type of timeidx? – Erwin Brandstetter Aug 31 '15 at 00:38
  • Hi @Erwin Brandstetter, the datatype of timeidx is real. Also I have tried to see why the second SELECT in my first ELSIF branch was a repetition of the SELECT one level above, you mean this right? SELECT timetypespan FROM tbl_log_a WHERE fnname = NEW.fnname AND timetype = 'resume' ORDER BY stmtserial DESC LIMIT 1 INTO v_timetypespan_resume; is selecting timetypespan, the one level above line is selecting timetype both are different columns, do you meaned that?. Thanks Advanced. – Natysiu16 Aug 31 '15 at 00:58
  • @Natysiu16: 1. trunc() should just work and is a bit faster. It's really not important, it was just an aside. See fiddle: http://sqlfiddle.com/#!15/9eecb7db59d16c80417c72d1e1f4fbf1/2893. 2. Yes, different columns, but same row. I select both columns at once which costs almost nothing, while executing another query is expensive. – Erwin Brandstetter Aug 31 '15 at 01:17
  • sorry, completed forgot to check it xD – Natysiu16 Aug 31 '15 at 04:50
  • but @Erwin Brandstetter, it the first SELECT it selects WHERE (timetype = 'start' OR timetype = 'resume') and in the second SELECT(the first ELSIF statement) it selects WHERE timetype = resume are they the same row? do you mean those two, right? You mean I could add timetypespan to the first SELECT to replace the second SELECT am I right? Could you please very briefly tell me how would you shrink that part of my script without using the "dynamically determined table name" just using the proper table names Thanks Advanced. – Natysiu16 Aug 31 '15 at 05:59
  • Same row. CASE _timetype ... WHEN 'resume' THEN means that, in this case, the first row according to your sort actually has timetype = 'resume', you don't have to run the query another time. Test it, it should do the same, just faster. You have to test it anyway, you can't just implement a solution from a friendly stranger on the internet. I added yet more explanation. – Erwin Brandstetter Aug 31 '15 at 21:56
  • Thanks @Erwin Brandstetter for pointing out my duplicated SELECT. I replaced it in the first SELECT, tested and it worked very well, though I still had to create a variable to store the timetypespan. (shown in the question). I saw that there are some pros and cons regarded to performance when using this dynamically set tablename method. Given the current escenario which one would you suggest? The dynamically set tablename method or having one Trigger Function for each different table name? (I have joined all my 2 Trigger Functions in only one now) Thanks Advanced. – Natysiu16 Aug 31 '15 at 22:46
  • Dynamic SQL or 26 functions with static SQL? Hard to say. If you INSERT many rows in the same session, static SQL may prove a bit faster, because query plans are reused like with prepared statements. The dynamic variant is more complex, you need someone to understand plpgsql properly. If that is no problem and INSERT performance is either not critical or you do not insert many rows in the same session, the dynamic variant is more elegant and may even be faster. One function instead of 26 has a couple of general benefits. – Erwin Brandstetter Aug 31 '15 at 23:00
  • Thanks @Erwin Brandstetter I run this trigger function every two seconds, for each %letter% tablename, so in two seconds I run it 26 times, I insert only one row each time the Trigger Function is run. I guess that's what you mean by "many rows in the same session". Thanks for your support and help me improve my script, very ilustrative indeed. Thanks. – Natysiu16 Aug 31 '15 at 23:27
  • @Natysiu16: The use case where static SQL can win is if the same trigger function is called many times in the same session. One or two rows for each table is not such a use case. Only if you insert repeatedly in the same session, then prepared statements for static SQL can be reused. I suggest to run tests with your actual environment. – Erwin Brandstetter Sep 01 '15 at 20:05