16

In my database I have tasks and comments tables. Each task has many comments.

I'd like to create tasks.comments_count column that would be updated automatically by PostgreSQL, so I can get comments_count (and sort / filter by it) in O(1) time while selecting all tasks.

I know there are language-specific solutions like counter cache of ActiveRecord, but I don't want to use them (I find them fragile). I'd like PostgreSQL to take care of such counter caches.

I also know PostgreSQL supports triggers, but they are hard to write and use (not a solid solution)

Ideally it would be a PostgreSQL extension or some native feature I'm not aware of.

Lazy calculation of such counters would be a great bonus.

4
  • Why not just count the number of comments for each task in a query instead of having to maintain that figure in a database table? One other option may be to use triggers and update the tasks.comments_count with each new comment that is added or deleted. Commented Jul 21, 2014 at 16:51
  • @ydaetskcoR I provided a simple to understand example. In real application there are multiple counters, and need for advanced sorting / filtering / grouping by them, so counting associated records in query is not a performant solution. Let's say it's a client requirement to store counter cache in the table. Commented Jul 21, 2014 at 16:54
  • I've not encountered "counter cache" before but, as mentioned, I'd probably just do this with a trigger on insert/delete of comments if you want to move the performance hit to writes rather than reads. It's also a portable solution. Commented Jul 21, 2014 at 16:58
  • What do you mean by "[triggers] are hard to write and use (not a solid solution)"? Triggers aren't any harder to write or use than anything else. And what do you mean by "lazy calculation"? Commented Jul 21, 2014 at 17:04

1 Answer 1

21

If you want Postgres to automatically do something on the basis of an insert/update/delete - i.e. if you want this operation to trigger some other action - then you need to write a trigger.

It's pretty straightforward. Simple enough that I doubt anyone would bother creating an extension (let alone a language feature) to save you the trouble. And it's certainly simpler (and as you pointed out, safer) than whatever ActiveRecord has going on under the hood.

Something like this is generally all it takes (I haven't tested this, so you might want to do so...):

CREATE FUNCTION maintain_comment_count_trg() RETURNS TRIGGER AS
$$
BEGIN
  IF TG_OP IN ('UPDATE', 'DELETE') THEN
    UPDATE tasks SET comment_count = comment_count - 1 WHERE id = old.task_id;
  END IF;
  IF TG_OP IN ('INSERT', 'UPDATE') THEN
    UPDATE tasks SET comment_count = comment_count + 1 WHERE id = new.task_id;
  END IF;
  RETURN NULL;
END
$$
LANGUAGE plpgsql;

CREATE TRIGGER maintain_comment_count
AFTER INSERT OR UPDATE OF task_id OR DELETE ON comments
FOR EACH ROW
EXECUTE PROCEDURE maintain_comment_count_trg();

If you want it to be airtight, you'd need an additional trigger for TRUNCATEs on comments; whether it's worth the trouble is up to you.

To handle updates to a tasks.id value which is being referenced (either via deferred constraints or ON UPDATE actions) then there's a bit more to it, but this is an uncommon case.

And if your client library / ORM is naive enough to send through every field in every UPDATE statement, you may want a separate UPDATE trigger which fires only when the value has actually changed:

CREATE TRIGGER maintain_comment_count_update
AFTER UPDATE OF task_id ON comments
FOR EACH ROW
WHEN (old.task_id IS DISTINCT FROM new.task_id)
EXECUTE PROCEDURE maintain_comment_count_trg();
Sign up to request clarification or add additional context in comments.

9 Comments

Hello Nick, can you please explain necessity of WHEN (old.task_id IS DISTINCT FROM new.task_id)
@lessless: I've updated my answer to explain this a little better.
Thank you! Pushing CC down to the database level is a very, very sane option :)
doesn't work :( it says the function reached the end without a RETURN
@NickBarnes it needs a DECLARE and a RETURN
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.