24

I have the code to fire a trigger only on an update of a single specific column. The trigger is used to fire a function that will raise a postgres "notify" event, which I am listening for and will need to test and validate the newly input details. There are many values on the account_details table which could be change which do not require an account validate, so a trigger on AFTER UPDATE only (without a when) is no good.

    CREATE TRIGGER trigger_update_account_details
    AFTER UPDATE ON account_details
    FOR EACH ROW
    WHEN (OLD.email IS DISTINCT FROM NEW.email) 
    EXECUTE PROCEDURE notify_insert_account_details();

But I want to fire the trigger if one of many columns change, something like

WHEN (OLD.email IS DISTINCT FROM NEW.email OR 
OLD.username IS DISTINCT FROM NEW.username OR 
OLD.password IS DISTINCT FROM NEW.password) 

But OR is not a valid keyword for a trigger. Trying to search for the keyword to use instead of OR doesn't seem to bring up anything due the nature of the word OR :-(

Martin
  • 837
  • 1
  • 10
  • 18

4 Answers4

40

The WHEN clause of the trigger definition expects a boolean expression and you can use OR operators in it. This just works (as long as all columns exist in the table account_details). I am using similar triggers myself:

CREATE TRIGGER trigger_update_account_details
AFTER UPDATE ON account_details
FOR EACH ROW
WHEN (OLD.email    IS DISTINCT FROM NEW.email
   OR OLD.username IS DISTINCT FROM NEW.username
   OR OLD.password IS DISTINCT FROM NEW.password) 
EXECUTE FUNCTION notify_insert_account_details();

In Postgres 10 or older use the (misleading) key word PROCEDURE instead of FUNCTION. See:

Evaluating the expression has a tiny cost, but this is probably more reliable than the alternative:

CREATE TRIGGER ... AFTER UPDATE OF email, username, password ...

Because, quoting the manual:

A column-specific trigger (one defined using the UPDATE OFcolumn_name syntax) will fire when any of its columns are listed as targets in the UPDATE command's SET list. It is possible for a column's value to change even when the trigger is not fired, because changes made to the row's contents by BEFORE UPDATE triggers are not considered. Conversely, a command such as UPDATE ... SET x = x ... will fire a trigger on column x, even though the column's value did not change.

ROW type syntax is shorter to check on many columns (doing the same):

...
WHEN ((OLD.email, OLD.username, OLD.password, ...)
       IS DISTINCT FROM
      (NEW.email, NEW.username, NEW.password, ...))
...

Or, to check for every visible user column in the row:

...
WHEN (OLD IS DISTINCT FROM NEW)
...
Erwin Brandstetter
  • 605,456
  • 145
  • 1,078
  • 1,228
  • I wonder if there is a difference in performance between using the `when` condition and explicitly listing the columns in the `update on ...` clause –  Aug 21 '14 at 21:41
  • @a_horse_with_no_name: the `WHEN` condition is slightly more expensive (the expression has to be evaluated), but the results are also subtly different in a number of ways. I added a bit to my answer. – Erwin Brandstetter Aug 21 '14 at 21:46
  • If someone worries about performance with the `WHEN` clause, but wants their triggers to be executed only if there was an actual change: these 2 solutions can be combined. – pozs Aug 22 '14 at 08:25
  • @pozs: Good idea. But correctness is the first concern: the above trigger may fire where the alternative would not (and the other way round) - as explained in the quote. When applying both, both conditions must be met. It depends on exact requirements. – Erwin Brandstetter Aug 22 '14 at 08:32
  • @ErwinBrandstetter you're right other triggers can also change those columns (I didn't noticed it first, maybe some emphasize can help in the quote) – pozs Aug 22 '14 at 08:44
  • Many thanks. Not sure what I was doing wrong before, but as you say, it just works. Working when tired is not the one.... – Martin Aug 23 '14 at 19:01
  • `WHEN row(new.*) IS DISTINCT FROM row(old.*)` – see https://stackoverflow.com/a/3084254/647151 – malthe Apr 09 '18 at 16:26
  • 2
    @malthe: Not applicable to this particular question, but I added it to make it complete. I suggest the shorter, equivalent syntax, though: `WHEN (OLD IS DISTINCT FROM NEW)` - and the parentheses are required. While being at it, I also added another answer over there ... – Erwin Brandstetter Apr 09 '18 at 21:49
  • Once again I learn something from @ErwinBrandstetter! It feels like `WHEN (OLD IS DISTINCT FROM NEW)` should be default behavior... I always start out my trigger functions with this check, didn't even know you could have it as part of the definition! – Jeff Dec 01 '18 at 21:38
  • what if i want to send only the updated value in a row , pg_notify(row_to_json), not the whole row. when a cell is updated , i want to send that cell value to my node.js, without doing if conditions in my pg function – The pyramid Dec 29 '18 at 01:12
  • @ErwinBrandstetter I am wondering how sophisticated this procedure/function notify_insert_account_details() Can become? so I asked an question. https://stackoverflow.com/questions/69631945/create-a-procedure-based-on-column-data-change-in-postgrersql – jian Oct 19 '21 at 13:43
11

I don't think you need the WHEN clause. You can specify the columns in question in the UPDATE clause:

CREATE TRIGGER trigger_update_account_details
    AFTER UPDATE OF email, username, password ON account_details
    FOR EACH ROW
    EXECUTE PROCEDURE notify_insert_account_details();
  • 2
    I have a feeling that this fires when the values are set via an UPDATE sql statement, irrespective if they are what may be thought of as "updated" i.e. different. – Martin Aug 23 '14 at 19:00
2

The above solutions were not working for me properly. So after reading through documentation again. I found few things to take note of. BEFORE UPDATE ON - AFTER UPDATE ON triggers are executed differently. Since my procedure was returning the NEW record with updated value. It was not working in AFTER trigger and in BEFORE trigger, the OR statements inside WHEN clause needed to be enclosed by braces.

CREATE TRIGGER check_update
BEFORE UPDATE ON some_table
FOR EACH ROW
WHEN ((OLD.colum_name_1 IS DISTINCT FROM NEW.colum_name_1) OR (OLD.colum_name_2 IS DISTINCT FROM NEW.colum_name_2))
EXECUTE PROCEDURE update_updated_at_column();

And the procedure

CREATE OR REPLACE FUNCTION update_updated_at_column()
  RETURNS TRIGGER AS $$
  BEGIN
      NEW.updated_at = now();
      RETURN NEW;
  END;
  $$ language 'plpgsql';
Sandeep PC
  • 797
  • 8
  • 12
  • 2
    This is basically the solution I went with (posted more info below). But it seems you should use `BEFORE UPDATE OF column_name_1, column_name_2 ON some_table` so that this trigger doesn't have to evaluate the `WHEN` clause on every single update – Tim Mar 16 '21 at 00:30
0

I'm in a bit of a rush but here's the solution I went with. I wanted to updated a column named "receivedAt" anytime the "answer" column changes (yes, my columns are camel case and my tables are capitalized... don't ask...). I also wanted it to null out if the answer was nulled (edge case that shouldn't ever really happen). But I didn't want this trigger to fire every time any row updates at all, as that could be costly.

I decided to combine the strategies used above, trusting in Postgres to do what it's supposed to in a performant way. I think some of them kind of reinvent the wheel and are inefficient in that they will fire any time any update is made.

I use knex migrations to manage my database, so I'll just go ahead and paste the whole thing in here.

import { Knex } from 'knex';


export async function up(knex: Knex): Promise<void> {
  await knex.raw(`
  CREATE OR REPLACE FUNCTION question_update_received_at_when_answer_changes()
  RETURNS TRIGGER
  LANGUAGE PLPGSQL
  AS
  $$
  BEGIN
    NEW."receivedAt" = NOW();
    IF NEW."answer" IS NULL THEN
      NEW."receivedAt" = NULL;
    END IF;
    RETURN NEW;
  END;
  $$;

  DROP TRIGGER IF EXISTS trigger_question_answer_received_at ON "Question";
  CREATE TRIGGER trigger_question_answer_received_at
    BEFORE UPDATE OF "answer" ON "Question"
    FOR EACH ROW
    WHEN (OLD."answer" IS DISTINCT FROM NEW."answer")
    EXECUTE PROCEDURE question_update_received_at_when_answer_changes();
  `)
}


export async function down(knex: Knex): Promise<void> {
  await knex.raw(`
  DROP TRIGGER trigger_question_answer_received_at on "Question";
  DROP FUNCTION question_update_received_at_when_answer_changes;
  `)
}
Tim
  • 6,265
  • 5
  • 29
  • 24
  • Downvote for the random javascript stuff. – cpursley Jan 12 '22 at 19:50
  • @cpursley AHHH!! JAVASCRIPT! KILL IT WITH FIRE! Thanks for saving the world from a valid solution with the vile stench of javascript on it. haha. I mentioned in the post I was in a rush. Taking a look at the embedded SQL isn't that hard for any developer that's not so afraid of JS that they can't stand to look at it. And it by no means is it so detrimental to the value of the post that it'd be better if this post didn't exist. – Tim Mar 09 '22 at 21:35