Postgresql 选择直到达到一定的总量并锁定

Posted

技术标签:

【中文标题】Postgresql 选择直到达到一定的总量并锁定【英文标题】:Postgresql select until certain total amount is reached and lock 【发布时间】:2020-09-23 19:44:05 【问题描述】:

我有一个用户批次表。我只想选择直到我的总金额达到一定金额。

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

我想在表上做一个选择,直到余额总数为 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

我尝试使用窗口功能,但它不允许锁定(用于更新)。感谢您的帮助。

【问题讨论】:

【参考方案1】:

我可以select. . . for update 使用窗口函数:

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

【讨论】:

嗯,if the balance of the row with id 3 was 4 it includes the row with the id of 4...这是@KelvinSantiago 的意图吗? 是的,确实如此。我想这意味着当 id 1 和 id 4 的行已经加起来 6 时,不包括 id 4 的行。但在这种情况下,也许 OP 确实也想要“下”一行。 @stickybit 你是对的。那应该是直的&lt;。谢谢你接听。 工作。拥有数百万条记录的表会出现性能问题吗? @KelvinSantiago 您需要运行它才能找到答案。假设 user_id 被索引,这应该表现得很好。如果没有,那么您可以将 rtotal CTE 更改为子查询。【参考方案2】:

你在找这个吗?

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 

【讨论】:

嗯,当添加FOR UPDATE 时,这似乎没有锁定任何东西。但它不会引发错误。 更新和查询工作正常。我将使用 explain 评估此查询的性能。 @Derviş Kayımbaşıoğlu 有一些方法可以不遍历整个表,即停止直到找到值。在查询 SELECT t.* 中, sum(balance) over (partition by user_id order by batch_id asc) running_balance FROM tb_batch_user t【参考方案3】:

这是一种使用窗口函数的方法:

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

您需要累积余额,直到第一个等于或超过阈值。为此,您可以使用窗口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

编号 |余额|用户身份 -: | ------: | ------: 1 | 2 | 1 3 | 8 | 1

【讨论】:

添加FOR UPDATE 会引发错误“窗口函数不允许FOR UPDATE”。 @stickybit:很好,谢谢。我用与子查询相同的解决方案更新了我的答案。 我可以确认您的编辑似乎正确锁定。 嗯,但另一件事:嗯,if the balance of the row with id 3 was 4 it includes the row with the id of 4 and 6... id 4 的行可能是 OP 在这种情况下想要的,但我无法想象他们也想要与id 6 排在一起。 @GMB 不工作,尝试使用数量 11,只应返回 1、3、4 的 ID【参考方案4】:

假设(user_id, batch_id) 是一个键,您可以使用相关子查询来避免窗口函数。外部子查询获取最小的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');

返回:

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

【讨论】:

以上是关于Postgresql 选择直到达到一定的总量并锁定的主要内容,如果未能解决你的问题,请参考以下文章

PostgreSQL 锁定模式

为啥 INSERT 查询无限期挂起并锁定 PostgreSQL 数据库和 pgAdmin

垂直居中内容区域直到达到一定高度?

执行 PHP 循环直到数组结束或达到一定的迭代次数?

Python爬虫是啥?

一篇了解爬虫技术方方面面