Как назначить локальную переменную в предложении обновления в PostgreSQL

#postgresql #local-variables #postgresql-12

#postgresql #локальные переменные #postgresql-12

Вопрос:

Я пытался назначить локальную переменную (ну, фактически две), используя SET предложение update предложения, состоящего из нескольких строк. Хорошо, я делаю это в 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),
(150, 0),
(100, 0),
(100, 100),
(100, 30),
(100, 0),
(100, 50),
(100, 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 cte

 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|
  

В этом приближении все еще должно быть что-то не так.

Комментарии:

1. все CTE s в Postgresql всегда работают с исходным снимком данных, а реальное изменение данных выполняется в конце выполнения. итак, если вы хотите, чтобы второй CTE видел результаты первого, то первый должен быть с ВОЗВРАТОМ, а второй должен выбирать из первого.

2. Спасибо! Я не знал

Ответ №1:

Я думаю, что хитрость заключается в том, чтобы вычислить общее значение перед обновлением.

Используя только 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|
  

Комментарии:

1. Интересный подход @bfris. На мой взгляд, создание временной таблицы для хранения числа звучит дорого, но, безусловно, это идея из древних времен, поскольку я читал в другом месте, что временные таблицы в PostgreSQL дешевы с точки зрения вычислений. Спасибо!

2. Временные таблицы не были моим первым выбором. Но они могут быть ОЧЕНЬ полезны для некоторых проблем. Для непривилегированных пользователей метод SQL Only может быть одним из немногих способов решения этой проблемы.

Ответ №2:

Я не думаю, что такая конструкция может работать в 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;
  

Комментарии:

1. Я пробовал использовать цикл for-in-select, чтобы он был похож на ваше решение.

2. Однако идея состоит в том, чтобы отслеживать запасы, перемещенные из доступных в заблокированные, а не все запасы, доступные в конце, которые должны быть равны 0.

Ответ №3:

Это грязное решение, которое выполняет столько, 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|