1

In my Ruby on Rails 4 app, I have this query to a Postgres 9.4 database:

@chosen_opportunity = Opportunity.find_by_sql(
  " UPDATE \"opportunities\" s
    SET opportunity_available = false
    FROM (
          SELECT \"opportunities\".*
          FROM   \"opportunities\"
          WHERE  ( deal_id = #{@deal.id}
          AND    opportunity_available = true 
          AND    pg_try_advisory_xact_lock(id) )
          LIMIT  1
          FOR    UPDATE
          ) sub
    WHERE       s.id = sub.id
    RETURNING   sub.prize_id, sub.id"
)

Very much inspired by this related answer on dba.SE.

But here (Postgres pg_try_advisory_lock blocks all records) they say, if I'm not mistaken, that I should not use pg_try_advisory_lock() inside the WHERE clause because I would be calling it once per row in the entire set that gets scanned (as part of the filtering that occurs in the where clause).

I just want my query to find and update the first (randomly, with LIMIT) row where available = true and update it to available = false, and I need to lock the row while doing this, but without making new requests waiting for the release of the previous lock so I added advisory locks like suggested here.

Should I place pg_try_advisory_lock() outside the WHERE clause? How to do it?

Community
  • 1
  • 1
Mathieu
  • 4,587
  • 11
  • 57
  • 112
  • 1
    @patrick, no the other question (http://stackoverflow.com/questions/33129132/pg-try-advisory-xact-lock-vs-nowait-in-order-not-to-wait-release-of-lock-rails) is about the same query but on a different topic: should I use pg_try_advisory_lock or NOWAIT option? totally different issue on the same code. – Mathieu Oct 14 '15 at 15:20
  • @patrick, or maybe by pure coincidence, as I'm a postgresql rookie, the NOWAIT is the answer to this present question? – Mathieu Oct 14 '15 at 15:20

1 Answers1

3

I updated my referenced answer with more explanation and links.
In Postgres 9.5 (currently beta) the new SKIP LOCKED is a superior solution:


Let me simplify a few things in your query first:

Straight query

UPDATE opportunities s
SET    opportunity_available = false
FROM  (
   SELECT id
   FROM   opportunities
   WHERE  deal_id = #{@deal.id}
   AND    opportunity_available
   AND    pg_try_advisory_xact_lock(id)
   LIMIT  1
   FOR    UPDATE
   ) sub
WHERE     s.id = sub.id
RETURNING s.prize_id, s.id;
  • All the double quotes were just noise with your legal, lower-case names.
  • Since opportunity_available is a boolean column you can simplify opportunity_available = true to just opportunity_available
  • You don't need to return * from the subquery, just id is enough.

Typically, this works as is. Explanation below.

Avoid advisory lock on unrelated rows

To be sure, you could encapsulate all predicates in a CTE or a subquery with the OFFSET 0 hack (less overhead) before you apply pg_try_advisory_xact_lock() in the next query level:

UPDATE opportunities s
SET    opportunity_available = false
FROM (
   SELECT id
   FROM  ( 
      SELECT id
      FROM   opportunities
      WHERE  deal_id = #{@deal.id}
      AND    opportunity_available
      AND    pg_try_advisory_xact_lock(id)
      OFFSET 0
      ) sub1
   WHERE  pg_try_advisory_xact_lock(id)
   LIMIT  1
   FOR    UPDATE
   ) sub2
WHERE     s.id = sub.id
RETURNING s.prize_id, s.id;

However, this is typically much more expensive.

You probably don't need this

There aren't going to be any "collateral" advisory locks if you base your query on an index covering all predicates, like this partial index:

CREATE INDEX opportunities_deal_id ON opportunities (deal_id)
WHERE opportunity_available;

Check with EXPLAIN to verify Postgres actually uses the index. This way, pg_try_advisory_xact_lock(id) will be a filter condition to the index or bitmap index scan and only qualifying rows are going to be tested (and locked) to begin with, so you can use the simple form without additional nesting. At the same time, your query performance is optimized. I would do that.

Even if a couple of unrelated rows should get an advisory lock once in a while, that typically just doesn't matter. Advisory locks are only relevant to queries that actually use advisory locks. Or do you really have other concurrent transactions that also use advisory locks and target other rows of the same table? Really?

The only other problematic case would be if massive amounts of unrelated rows get advisory locks, which can only happen with a sequential scan and is very unlikely even then.

Community
  • 1
  • 1
Erwin Brandstetter
  • 605,456
  • 145
  • 1,078
  • 1,228
  • I'm at a loss, struggling with the query so I opened a new question: http://stackoverflow.com/questions/33330915/postgresql-9-4-prevent-app-selecting-always-the-latest-updated-rows – Mathieu Oct 25 '15 at 14:42
  • moved to another question : I think the problem comes form the fast, (and you spotted it in your intuition here quite fats hehe) that I need to move away from ARBITRARY pick to truly RANDOM pick while having the fastest select/update query possible and locks: http://stackoverflow.com/questions/33333021/postgresql-9-4-fastest-query-to-select-and-update-on-large-dataset-30m-rows – Mathieu Oct 25 '15 at 18:08
  • @Erwin In tables with intensive insert/update/delete(like queues) such approaches are not reliable. I suppose the main issue is out of date statistic collected for planner. I have "queue" table with thoughput about 300 items/sec. I tuned auto vaccum for table to be : autovacuum_vacuum_threshold=5000, autovacuum_vacuum_scale_factor=0.0, autovacuum_analyze_threshold=5000, autovacuum_analyze_scale_factor=0.0, And in some cases when queue contains about 100 000 items I get "out of shared memory" error. Manuall run of "vacuum full " against the table fix issue for some time. – Oleksandr_DJ May 14 '16 at 10:13
  • @Oleksandr_DJ: I suppose session-level locks and manual unlocking could solve this particular problem. See [instructions in the superior answer I linked to.](http://dba.stackexchange.com/a/69497/3684) But consider the new `SKIP LOCKED` first. – Erwin Brandstetter May 18 '16 at 01:17
  • @ErwinBrandstetter: But what benefits will bring session level lock (pg_try_advisory_lock) versus current transaction lock (pg_try_advisory_xact_lock) in my case? I have short time transactions. BTW: I use PS9.3 and can not use "SKIP LOCKED" – Oleksandr_DJ May 18 '16 at 08:46
  • @Oleksandr_DJ: Manual unlocking is the important part, I would expect that to reduce the needed shared memory. But if you have "short time transactions" that might not be the problem ... – Erwin Brandstetter May 18 '16 at 12:06
  • @ErwinBrandstetter BTW: For now I have one reader with pg_try_advisory_xact_lock, and catching from time to time "out of shared memory" error. So, that error defenetelly is not related with keeping too many(unlimited) transactions with limited set of locked rows in each. "out of shared memory" error arise in scope of one transaction. So, session lock will not bring any benefits here. – Oleksandr_DJ May 18 '16 at 14:29
  • 1
    @Oleksandr_DJ: For only *one* reader (no concurrency) you don't need manual locking at all. If you still need locking consider the manual for the setting [`max_locks_per_transaction`](http://www.postgresql.org/docs/current/interactive/runtime-config-locks.html#GUC-MAX-LOCKS-PER-TRANSACTION). Consider [PGQ](https://wiki.postgresql.org/wiki/PGQ_Tutorial) for a more potent queuing solution. If doubts remain, I suggest you ask a ***new question*** providing all relevant information. Comments are not the place. Preferably on dba.SE. – Erwin Brandstetter May 18 '16 at 15:52
  • @ErwinBrandstetter: Sure, if only one reader is necessary, there is no need for row lock at all. But I only tried to explain that high loaded(300+ items/sec) queue even with one reader has some problems with pg_try_advisory_xact_lock() solution. As for my queue implementation, I allowed only one concurrent reader(because of mentioned problem with locks), then put that data to one free thread for processing. So, I have many writers and only one reader - queue in pure SQL. Fetched data is processed in parallel outside of SQL. Thanks for discussion, Erwin! – Oleksandr_DJ May 18 '16 at 18:28