如何确保一个表中的金额总和小于另一个表的数量?

发布于 2025-02-10 01:22:41 字数 1336 浏览 1 评论 0 原文

的表格

假设我有一张大理石ID 颜色 总计
1 蓝色 5
2 红色 10
3 Swirly 3

,我需要将它们放入上唯一约束(BAG_ID,MARBLE_ID)的袋子中:

bag_id marble_id
1 1 1 1 1 (蓝色) 2
1 2(红色) 3
2 1(蓝色) 2

我有一个查询,最多可以袋装剩余的大理石数量

WITH unbagged AS (
  SELECT
    marble.total - COALESCE( SUM( bag.quantity ), 0 ) AS quantity
  FROM marble
    LEFT JOIN bag ON marble.id = bag.marble_id
  WHERE marble.id = :marble_id
  GROUP BY marble.id )
  
INSERT INTO bag (bag_id, marble_id, quantity)
SELECT
  :bag_id,
  :marble_id,
  LEAST( :quantity, unbagged.quantity )
FROM unbagged
ON CONFLICT (bag_id, marble_id) DO UPDATE SET
  quantity = bag.quantity
    + LEAST(
        EXCLUDED.quantity,
        (SELECT quantity FROM unbagged) )

,直到一天,它的剩余大理石数量很棒,它与同一物品同时被调用了两次我最终在一个袋子里放了6个swirly大理石(也许只有3个袋子中的3个),即使总共只有3个。

我想我明白了为什么,但是我不知道如何防止这种情况发生?

Say I have a table of marbles

id color total
1 blue 5
2 red 10
3 swirly 3

and I need to put them into bags with a unique constraint on (bag_id, marble_id):

bag_id marble_id quantity
1 1 (blue) 2
1 2 (red) 3
2 1 (blue) 2

I have a query for bagging at most the number of remaining marbles

WITH unbagged AS (
  SELECT
    marble.total - COALESCE( SUM( bag.quantity ), 0 ) AS quantity
  FROM marble
    LEFT JOIN bag ON marble.id = bag.marble_id
  WHERE marble.id = :marble_id
  GROUP BY marble.id )
  
INSERT INTO bag (bag_id, marble_id, quantity)
SELECT
  :bag_id,
  :marble_id,
  LEAST( :quantity, unbagged.quantity )
FROM unbagged
ON CONFLICT (bag_id, marble_id) DO UPDATE SET
  quantity = bag.quantity
    + LEAST(
        EXCLUDED.quantity,
        (SELECT quantity FROM unbagged) )

which works great until one day, it gets called twice at exactly the same time with the same item and I end up with 6 swirly marbles in a bag (or maybe 3 each in 2 bags), even though there are only 3 total.

I think I understand why, but I don't know how to prevent this from happening?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(1

偏闹i 2025-02-17 01:22:41

您的算法对我来说并不清楚,但是核心问题是 /strong>

手动锁定

查询过程一次在表大理石中的一个给定行。最便宜的解决方案是服用大理石和 bag 的唯一查询编写)。然后,下一个试图与同一种大理石混乱的交易必须等到当前的交易(或回滚)。

BEGIN;

SELECT FROM marble WHERE id = :marble_id FOR UPDATE;  -- row level lock

WITH unbagged AS ( ...

COMMIT;

serializable

或使用,这是更昂贵的“接收”解决方案 - 并准备在序列化误差时重复交易。喜欢:

BEGIN ISOLATION LEVEL SERIALIZABLE;

WITH unbagged AS ( ...

COMMIT;

相关:

Your algorithm isn't exactly clear to me, but the core issue is concurrency.

Manual locking

Your query processes a single given row in table marble at a time. The cheapest solution is to take an exclusive lock on that row (assuming that's the only query writing to marble and bag). Then the next transaction trying to mess with the same kind of marble has to wait until the current one has committed (or rolled back).

BEGIN;

SELECT FROM marble WHERE id = :marble_id FOR UPDATE;  -- row level lock

WITH unbagged AS ( ...

COMMIT;

SERIALIZABLE

Or use serializable transaction isolation, that's the more expensive "catch-all" solution - and be prepared to repeat the transaction in case of a serialization error. Like:

BEGIN ISOLATION LEVEL SERIALIZABLE;

WITH unbagged AS ( ...

COMMIT;

Related:

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文