Kelvin Santiago
Kelvin Santiago

Reputation: 305

Postgresql select until certain total amount is reached and lock

I have a table of user batches. I only want to select until my amount total reaches a certain amount.

id  | user_id | balance | batch_id 
----|---------|-------- |--------
 1  | 1       |   2     | 1
 2  | 2       |   15    | 2
 3  | 1       |   8     | 3
 4  | 1       |   5     | 4 
 5  | 2       |   7     | 5
 6  | 1       |   1     | 6
 7  | 2       |   5     | 7

Consider the following query:

SELECT * FROM tb_batch_user WHERE user_id = 1 ORDER BY batch_id asc

The result of query is:

    id  | user_id | balance | batch_id 
    ----|---------|-------- |--------
     1  | 1       |   2     | 1
     3  | 1       |   8     | 3
     4  | 1       |   5     | 4 
     6  | 1       |   1     | 6

I want to do a select on the table until the balance total is 6. Then only ids 1, 2 should be returned:

    id  | user_id | balance | batch_id 
    ----|---------|-------- |--------
     1  | 1       |   2     | 1
     3  | 1       |   8     | 3

Another example with balance total 1. Then only ids 1 should be returned:

    id  | user_id | balance | batch_id 
    ----|---------|-------- |--------
     1  | 1       |   2     | 1

Example with balance total 11. Only ids 1,3,4 should be returned:

    id  | user_id | balance | batch_id 
    ----|---------|-------- |--------
     1  | 1       |   2     | 1
     3  | 1       |   8     | 3
     4  | 1       |   5     | 4

So, after that I need to lock those lines with FOR UPDATE ex:

     SELECT * FROM tb_batch_user WHERE user_id = 1 ORDER BY batch_id asc FOR UPDATE

I tried with window function, but it doesn't allow lock (FOR UPDATE). Thanks for any help.

Upvotes: 1

Views: 354

Answers (4)

GMB
GMB

Reputation: 222682

Here is an approach using window functions:

select id, balance, user_id, batch_id
from (
    select t.*, 
        sum(balance) over(partition by user_id order by id) sum_balance
    from mytable t
    where user_id = 1
) t
where sum_balance - balance < 6

You want a cumulative balance until the first one that equals or exceeds the threshold. For this, you can just use a window sum().

You can change the inequality condition to the threshold value that you like. You can also change (or remove) the filtering on user_id in the subquery.

We can easily implement the same logic with a subquery, that would support for update:

select *
from mytable t
where user_id = 1 and (
    select coalesce(sum(balance), 0)
    from mytable t1
    where t1.user_id = t.user_id and t1.id < t.id
) < 6
for update

Demo on DB Fiddle:

id | balance | user_id
-: | ------: | ------:
 1 |       2 |       1
 3 |       8 |       1

Upvotes: 1

Derviş Kayımbaşıoğlu
Derviş Kayımbaşıoğlu

Reputation: 30663

are you looking for this?

with w0 as (
  select id, user_id, balance, batch_id,
     coalesce(lag(running_balance) over (partition by user_id order by batch_id asc), 0) running_balance 
  from (
      SELECT t.* ,
        sum(balance) over (partition by user_id order by batch_id asc) running_balance
      FROM tb_batch_user t 
      --where t.user_id = 1
  ) x 
)
select * from w0
where running_balance < 6

PS: you can add user_id as where clause. see comment

for locking,

select * from tb_batch_user tb
where tb.id in (select w0.id from w0 where running_balance < 6)
for update 

Upvotes: 1

Mike Organek
Mike Organek

Reputation: 12494

I am able to select. . . for update using window functions:

with inparms as (
  select 1 as user_id, 6 as target
), rtotal as (
  select t.id, i.target,
         sum(t.balance) over (partition by t.user_id
                                  order by t.id
                              rows between unbounded preceding
                                       and 1 preceding) as runbalance
    from tb_batch_user t
         join inparms i 
           on i.user_id = t.user_id
)
select t.*
  from rtotal r
       join tb_batch_user t
         on t.id = r.id
 where coalesce(r.runbalance, 0) < r.target
for update of t;

Fiddle here

Upvotes: 1

sticky bit
sticky bit

Reputation: 37497

Assuming that (user_id, batch_id) is a key, you can use correlated subqueries to avoid window functions. The outer subquery gets the minimal batch_id where the sum of balance reaches or exceeds 6 for the given user id. That sum is fetched in the inner one.

SELECT *
       FROM tb_batch_user bu1
            WHERE bu1.user_id = 1
                  AND bu1.batch_id <= (SELECT min(bu2.batch_id) batch_id
                                              FROM tb_batch_user bu2
                                              WHERE bu2.user_id = bu1.user_id
                                                    AND (SELECT sum(bu3.balance)
                                                                FROM tb_batch_user bu3
                                                                WHERE bu3.user_id = bu2.user_id
                                                                      AND bu3.batch_id <= bu2.batch_id) >= 6)
       FOR UPDATE;

With the pgrowlocks extension installed, we can check that the right rows are locked.

SELECT *
       FROM pgrowlocks('tb_batch_user');

returns:

 locked_row | locker   | multi | xids       | modes          | pids
------------+----------+-------+------------+----------------+---------
 (0,1)      | 10847645 | f     | {10847645} | {"For Update"} | {11996}
 (0,3)      | 10847645 | f     | {10847645} | {"For Update"} | {11996}

Upvotes: 0

Related Questions