Postgresql select 直到达到一定的总量并锁定

Postgresql select until certain total amount is reached and lock

我有 table 个用户批次。我只想 select 直到我的总金额达到一定金额。

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

考虑以下查询:

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

查询结果为:

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

我想在 table 上做一个 select,直到余额总数为 6。那么应该只返回 ids 1, 2:

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

另一个余额总计为 1 的例子。那么应该只返回 ids 1:

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

余额总数为 11 的示例。只应返回 ID 1、3、4:

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

因此,之后我需要使用 FOR UPDATE ex:

锁定这些行
     SELECT * FROM tb_batch_user WHERE user_id = 1 ORDER BY batch_id asc FOR UPDATE

我尝试使用 window 函数,但它不允许锁定(更新)。感谢您的帮助。

你在找这个吗?

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:可以添加user_id作为where子句。见评论

用于锁定,

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

这是使用 window 函数的方法:

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

您需要累积余额,直到第一个余额等于或超过阈值。为此,您可以只使用 window sum().

您可以将不等式条件更改为您喜欢的阈值。您还可以更改(或删除)子查询中 user_id 的过滤。

我们可以使用子查询轻松实现相同的逻辑,这将支持 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

假设(user_id, batch_id)是key,可以使用关联子查询来避免window函数。外部子查询获取最小值 batch_id,其中 balance 的总和达到或超过给定用户 ID 的 6。这笔钱是在里面拿的。

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;

安装 pgrowlocks extension 后,我们可以检查是否锁定了正确的行。

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}

我能够 select. . . for update 使用 window 函数:

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