如何在PostgreSQL中的更新语句中分配局部变量

问题描述

我试图使用在多行上运行的SET语句的update子句来分配局部变量(实际上是两个)。好的,我正在执行ala MysqL

drop table if exists stocks cascade;

create table stocks (
  id              serial,stock_available int,stock_locked    int
);

insert into stocks(stock_available,stock_locked) values 
(150,10),(150,20),0),(100,100),30),50),0);

create or replace function lock_all ()
returns int
language plpgsql as $$
declare
  _amount int;
  _total int;
begin
  -- initialize accumulator
  _total = 0;

  -- update all the stocks table rows
  update stocks
  set    _amount = stock_available,stock_locked = stock_locked + _amount,_total = _total + _amount;
  
  -- returns the units locked 
  return _total;
end;
$$;

不幸的是,这不是Postgresql期望做这种事情的方式。

 sql Error [42703]: ERROR: column "_amount" of relation "stocks" does not exist
  Where: PL/pgsql function lock_all() line 10 at sql statement

这只是一个简单的示例,它说明了对update句子中更新的事物进行计数/求和的实际问题。我相信这个特定示例可能有一些技巧或方法,但是我对这种情况下的通用解决方案感兴趣,因为这种情况下必须计算累加器。

有什么主意吗?


编辑

按照@GMB的建议,我链接了3个ctes

create or replace function lock_all3 ()
returns int
language sql as $$
  with 
    cte1 as (
      select 
        sum(stock_locked)::int as initially_locked 
      from 
        stocks
    ),cte2 as (
      update 
        stocks 
      set 
        stock_locked = stock_locked + stock_available,stock_available = 0
      returning 
        0 as dummy
    ),cte3 as (
      select 
        sum(stock_locked)::int as finally_locked 
      from 
        stocks
    )
  select 
    (cte3.finally_locked - initially_locked - dummy) 
  from 
    cte1,cte2,cte3;
$$;

这应该起作用,但是结果值表明两个选择都在表stocks的初始值上执行,因为两者之差为0。

select lock_all3();

lock_all3|
---------|
        0|

但是,由于最终情况表明所有可用库存都已锁定,因此执行了cte2。

select * from stocks;

id|stock_available|stock_locked|
--|---------------|------------|
 1|              0|         160|
 2|              0|         170|
 3|              0|         150|
 4|              0|         100|
 5|              0|         200|
 6|              0|         130|
 7|              0|         100|
 8|              0|         150|
 9|              0|         100|

在这种近似中,肯定还有些问题。

解决方法

我认为这样的结构不能在Postgres中工作;即使在MySQL中,也无法(或至少安全)以这种方式使用变量。

认为我了解您要在执行更新之前跟踪可用的总库存。仅使用两个不同的查询可能会更简单:

select sum(stock_available) total from stocks returning total into _total;
update stocks set stock_locked = stock_locked + stock_available;

如果要避免竞争条件,可以将其包装在事务中,或将其写为单个语句:

with cte as (update stocks set stock_locked = stock_locked + stock_available)
select sum(stock_available) total from stocks returning total into _total;
,

我认为诀窍是在更新之前 计算总数。

仅使用SQL

  DROP TABLE x;
SELECT sum(stock_available) as total_moved
  INTO TEMP TABLE x
  FROM stocks as total_moved; 
UPDATE stocks 
   SET stock_locked = stock_available + stock_locked,stock_available = 0;
SELECT * from x;

total_moved|
-----------|
       1050|

使用存储过程

create or replace function lock_all ()
returns int
language plpgsql as $$
declare
  _total int;
begin
    --calculate total before update
    SELECT sum(stock_available)
      INTO _total
      FROM stocks;

    UPDATE stocks
       SET stock_locked = stock_locked + stock_available,stock_available = 0;

    return _total;
end;
$$;

从lock_all中选择*;

lock_all|
--------|
    1050|

从股票中选择*;

id|stock_available|stock_locked|
--|---------------|------------|
 1|              0|         160|
 2|              0|         170|
 3|              0|         150|
 4|              0|         100|
 5|              0|         200|
 6|              0|         130|
 7|              0|         100|
 8|              0|         150|
 9|              0|         100|
,

这是一个肮脏解决方案,它执行的updates与表stocks中的行数一样多。它有效,但这是我试图避免的解决方案。

create or replace function lock_all ()
returns int
language plpgsql as $$
declare
  _amount int;
  _total int;
 r record;
begin
  -- initialize counter
  _total = 0;

    -- select stocks rows
    for r in (
        select * from stocks
    ) 
    loop
        _amount = r.stock_available;
    
      -- update the stock_fulfilled column in of_user_order_line_supply
      update stocks
      set        stock_locked = stock_locked + _amount
      where  id = r.id;
     
     _total = _total + _amount;
    end loop;

return _total;
end;
$$;

select lock_all();
select * from stocks;

lock_all|
--------|
    1050|