I'm using PostgreSQL with the following database schema:
CREATE TABLE plans (
slug VARCHAR(500) PRIMARY KEY
);
CREATE TABLE users (
id VARCHAR(16) PRIMARY KEY,
org_id VARCHAR(16) NOT NULL
);
CREATE TABLE orgs (
id VARCHAR(16) PRIMARY KEY,
plan_slug VARCHAR(500) NOT NULL,
last_write_at DOUBLE PRECISION
);
In my case, I want to write a query to update some org's plan_slug
and protect it from other possible concurrent update.
To do this, I use a SELECT
in a subquery and a FOR UPDATE
to lock the rows in a specific order to avoid deadlocks.
Like the following query:
UPDATE orgs
SET plan_slug = 'plan_1'
WHERE id = ANY(
SELECT subquery_orgs.id
FROM orgs AS subquery_orgs
JOIN users ON users.org_id = subquery_orgs.id
WHERE users.id = ANY('{user_1, user_2, user_3}')
ORDER BY subquery_orgs.id
FOR UPDATE
);
I noticed that if this request takes a long time to run, there is a high chance of collision with another query that is trying to update the last_write_at
of an org (an org that is already being updated by the first query).
As follows:
UPDATE orgs
SET last_write_at = 999
FROM plans
WHERE orgs.id = 'org_1';
The query will succeed. But if I add the plan_slug
in the WHERE
clause of the query, it will always fail to update. Postgre return UPDATE 0
.
The query is the following:
UPDATE orgs
SET last_write_at = 999
FROM plans
WHERE orgs.id = 'org_1'
AND plans.slug = orgs.plan_slug;
From what I understand the process should be:
- The first query execute. The
plan_slug
row is locked. - The second query execute. It stops its execution and wait for the row to unlock.
- The first query finishes its update.
- The second query restart and re-evaluate the changes.
- The
plan_slug
has changed but the new one exist in theplans
table, the second query should succeed.
Then why the query is failing to update when I add the plan_slug
to the WHERE
clause ?
orgs.plan_slug
toplans.slug
, there is no guarantee thatplans.slug = orgs.plan_slug
matches anything. In fact, query #1 may have broken that relationship, thus why query #3 finds nothing to update.plans.slug
always matchorgs.plan_slug
. Theslug
is checked in the code to be an existing row in theplans
table. Also I tried your solution by adding a foreign key constraint but unfortunately it did not solve the problem.psql
sessions. Now to figure out what exactly is happening.