Put pg_try_advisory_xact_lock() in a nested subquery?
I updated my referenced answer with more explanation and links.
In Postgres 9.5 (currently beta) the new SKIP LOCKED
is a superior solution:
- Postgres UPDATE … LIMIT 1
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 justopportunity_available
- You don't need to return
*
from the subquery, justid
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.
Postgres pg_try_advisory_lock blocks all records
You're calling pg_try_advisory_lock() once per row in the entire set that gets scanned (as part of the filtering that occurs in the where
clause), whereas you only want it called once per row in table1 returned by the query.
You could try using a subquery or a CTE instead:
with rows as (
SELECT a.id
FROM table1 a
JOIN table2 b ON a.table1_id = b.id
WHERE table2.id = 1
)
select rows.*
from rows
where pg_try_advisory_lock('table1'::regclass::integer, rows.id);
But don't rely on that to necessarily work as expected either: Postgres should be tempted to rewrite it the way your initial query was.
Another possibility is this, since the select
part of a statement is evaluated very late in the query:
with rows as (
SELECT a.id,
pg_try_advisory_lock('table1'::regclass::integer, a.id) as locked
FROM table1 a
JOIN table2 b ON a.table1_id = b.id
WHERE table2.id = 1
)
select rows.id
from rows
where rows.locked;
The real issue in practice is that pg_try_advisory_lock()
is something you'd normally find in app land or in a function, rather than in a query like you're doing. Speaking of which, depending on what you're doing, are you sure you shouldn't be using select … for update
?
Regarding your update:
postgres scans the very first row then stops?
Yes. Due to the limit 1
, it's going to find a match and immediately stop. What is probably happening, though, is that it's not evaluating the where
clause in the same order depending on your queries. SQL offers no guarantee that the a <> 0
part in a <> 0 and b / a > c
gets evaluated first. Applied to your case, it offers no guarantee that the advisory lock is obtained after the row from a is joined with b.
Advisory locks or NOWAIT to avoid waiting for locked rows?
is only a good idea if you insist on locking a particular row, which is not what you need. You just want any qualifying, available (unlocked) row. The important difference is this (quoting the manual for Postgres 9.4):FOR UPDATE NOWAIT
With
NOWAIT
, the statement reports an error, rather than waiting, if a selected row cannot be locked immediately.
Identical queries will very likely try to lock the same arbitrary pick. FOR UPDATE NOWAIT
will just bail out with an exception (which will roll back the whole transaction unless you trap the error) and you have to retry.
The solution in my referenced answer on dba.SE uses a combination of plain FOR UPDATE
in combination with pg_try_advisory_lock()
:
pg_try_advisory_lock
is similar topg_advisory_lock
, except the
function will not wait for the lock to become available. It will
either obtain the lock immediately and return true, or return false if
the lock cannot be acquired immediately.
So your best option is ... the third alternative: the new FOR UPDATE SKIP LOCKED
in Postgres 9.5, which implements the same behavior without additional function call.
The manual for Postgres 9.5 compares the two options, explaining the difference some more:
To prevent the operation from waiting for other transactions to
commit, use either theNOWAIT
orSKIP LOCKED
option. WithNOWAIT
, the
statement reports an error, rather than waiting, if a selected row
cannot be locked immediately. WithSKIP LOCKED
, any selected rows that
cannot be immediately locked are skipped.
On Postgres 9.4 or older your next best option is to use pg_try_advisory_xact_lock(id)
in combination with FOR UPDATE
like demonstrated in the referenced answer:
- Postgres UPDATE … LIMIT 1
(Also with an implementation with FOR UPDATE SKIP LOCKED
.)
Strictly speaking you get arbitrary, not truly random picks. That can be an important distinction.
An audited version of your query is in my answer to your other question.
postgresql 9.4 - prevent app selecting always the latest updated rows
Just an idea: instead of calling random() use it as default value for a column(which can be indexed) A similar way could use a serial with an increment of about 0.7 * INT_MAX.
\i tmp.sql
CREATE TABLE opportunities
( id SERIAL NOT NULL PRIMARY KEY
, deal_id INTEGER NOT NULL DEFAULT 0
, prize_id INTEGER
, opportunity_available boolean NOT NULL DEFAULT False
-- ----------------------------------------
-- precomputed random() , (could be indexed)
, magic DOUBLE precision NOT NULL default RANDOM()
);
INSERT INTO opportunities(deal_id)
SELECT 341
FROM generate_series(1,20) gs
;
VACUUM ANALYZE opportunities;
PREPARE add_three (integer) AS (
WITH zzz AS (
UPDATE opportunities
SET prize_id = 21
, opportunity_available = True
-- updating magic is not *really* needed here ...
, magic = random()
WHERE opportunities.id
IN (
SELECT opportunities.id
FROM opportunities
WHERE (deal_id = $1 AND prize_id IS NULL)
-- ORDER BY RANDOM()
ORDER BY magic
LIMIT 3)
RETURNING id, magic
) --
SELECT * FROM zzz
);
PREPARE draw_one (integer) AS (
WITH upd AS (
UPDATE opportunities s
SET opportunity_available = false
FROM (
SELECT id
FROM opportunities
WHERE deal_id = $1
AND opportunity_available
AND pg_try_advisory_xact_lock(id)
ORDER BY magic
LIMIT 1
FOR UPDATE
) sub
WHERE s.id = sub.id
RETURNING s.prize_id, s.id, magic
)
SELECT * FROM upd
);
SELECT * FROM opportunities;
\echo add3
EXECUTE add_three(341);
SELECT * FROM opportunities;
\echo add3 more
EXECUTE add_three(341);
SELECT * FROM opportunities;
\echo draw1
EXECUTE draw_one(341);
SELECT * FROM opportunities;
\echo draw2
EXECUTE draw_one(341);
SELECT * FROM opportunities;
VACUUM ANALYZE opportunities;
\echo draw3
EXECUTE draw_one(341);
SELECT * FROM opportunities;
\echo draw4
EXECUTE draw_one(341);
SELECT * FROM opportunities;
Rails 4/postgresql - Update model attribute AFTER Read query has been performed on the row
You can set and save the new status directly in the controller in your show_opportunities
method, after the record is read, but before the data is send to the user.
If you want to make sure that at the same time no other is getting the same opportunity, you need to lock the database row when you read it. See "pessimistic locking"
Related Topics
How to Call a User Defined Function to Use with Select, Group By, Order By
Get Every Hour for a Time Range
How to Sum Multiple Lines in SQL
How to Back Up a Postgresql Database from Within Psql
Oracle (11.2.0.1.0) - Recursive Cte with a Date Expression
Access: Create Table If It Does Not Exist
Postgres Column Does Not Exist
Table as an Argument of a Postgresql Function
Create a Unique Index on a Non-Unique Column
Problem with MySQL Insert Max()+1
Redshift Split Single Dynamic Column into Multiple Rows in New Table
SQL Query on Multiple Databases
Activerecord Find - Skipping Records or Getting Every Nth Record