检查postgres函数内部的查询

时间:2020-07-10 19:15:30

标签: postgresql

我想将查询结果用作在postgres函数中执行什么操作的条件。这是我的尝试:

CREATE OR REPLACE FUNCTION upsert_bin_effect(operation_id integer, typ text, asset_id integer, qty double precision) RETURNS boolean AS $$
BEGIN
    existing_locked := (SELECT SUM(qty) AS qty INTO existing FROM bin_effect be WHERE be.operation_id = operation_id AND be.type = typ AND be.asset_id = asset_id AND be.locked = true GROUP BY be.operation_id, be.type, be.asset_id);
    qty = qty - existing.locked.qty
    
    existing_unlocked := (SELECT * INTO existing FROM bin_effect be WHERE be.operation_id = operation_id AND be.type = typ AND be.asset_id = asset_id AND (be.locked = false OR be.locked IS NULL));
    IF EXISTS(existing_unlocked)
    THEN            
        UPDATE bin_effect be SET be.qty = qty WHERE be.id = existing_unlocked.id
    ELSE
        INSERT INTO bin_effect be (be.operation_id, be."type", be.asset_id, be.qty) VALUES (operation_id, typ, asset_id, qty);
    END IF;
    
END;
$$ LANGUAGE plpgsql;

existing_locked可以有多行,我想从传入的existing_locked.qty中减去qty的总和。然后,使用网络locked更新不属于existing_unlocked的记录(即qty中的记录,如果存在的话)-否则,使用网络qty插入新行。

如果我们假设有一个包含以下数据的表:

operation_id, type, asset_id, qty, locked
1, 'A', 1, 10, true
1, 'A', 1, 20, true
1, 'A', 2, 5, true
1, 'A', 2, 15, null

以下通话:

upsert_bin_effect(1, 'A', 1, 100)

应导致:

operation_id, type, asset_id, qty, locked
1, 'A', 1, 10, true
1, 'A', 1, 20, true
1, 'A', 2, 5, true
1, 'A', 2, 15, null
1, 'A', 1, 70, null

以下通话:

upsert_bin_effect(1, 'A', 2, 100)

应导致:

operation_id, type, asset_id, qty, locked
1, 'A', 1, 10, true
1, 'A', 1, 20, true
1, 'A', 2, 5, true
1, 'A', 2, 95, null

以下通话:

upsert_bin_effect(1, 'A', 3, 100)

应导致:

operation_id, type, asset_id, qty, locked
1, 'A', 1, 10, true
1, 'A', 1, 20, true
1, 'A', 2, 5, true
1, 'A', 2, 95, null
1, 'A', 3, 100, null

为了更好地描述我希望此功能在这里如何工作,是一些实现所需功能的javascript伪代码:

// these are mock result sets, assume they were queried where operation_id, type, asset_id are equal and locked is true/falsy respectively.
const existingLocked = [];
const existingUnlocked = [];

function upsert_bin_effect(operationId, typ, assetId, qty) {
    const lockedQty = existingLocked.reduce((sum, r) => sum + r.qty, 0);
  
  // incoming qty represents the total qty. lockedQty represents qty for locked rows (rows we cannot update)
  // If there is nonzero lockedQty, we subtract it from qty because when we upsert qty
  // we need to ensure that all rows qty will sum to the incoming qty.
  qty = qty - lockedQty;
  
  // existingUnlocked should only ever contain a single row (because of the upsert logic below)
  if (!!existingUnlocked[0]) {
    // if there is an existing unlocked row, update it with the (incoming qty - any locked qty)
    existingUnlocked[0].update('qty', qty);
  }
  else {
    // otherwise insert a new row with qty = (incoming qty - any locked qty)
    db.binEffect.insert(operationId, typ, assetId, qty)
  }
}

我对sql函数编程非常陌生。这有意义吗?如果没有,我该如何完成我想做的事情?

2 个答案:

答案 0 :(得分:1)

在使用所需功能之前,此功能存在几个问题:

DECLARE existing RESULT

--There is no RESULT type and you do end with ; So:

DECLARE existing RECORD;

existing_locked未声明,因此分配失败。 相同于existing_unlocked。

qty = qty - existing.locked.qty不能以;

结尾

我会花一些时间在这里

https://www.postgresql.org/docs/12/plpgsql-structure.html

从下面的评论中,我看不到新示例与您想要的内容相符:

operation_id, type, asset_id, qty, locked
1, 'A', 1, 10, true
1, 'A', 1, 20, true
1, 'A', 2, 5, true
1, 'A', 2, 15, null
1, 'A', 1, 70, null

--The following call:

upsert_bin_effect(1, 'A', 2, 100)

--should result in:

operation_id, type, asset_id, qty, locked
1, 'A', 1, 10, true
1, 'A', 1, 20, true
1, 'A', 2, 5, true
1, 'A', 2, 95, null

“ existing可以有多行,我想从任何锁定行的传入数量中减去现存.qty的总和。然后,如果存在未锁定的行,则更新任何未用传入数量锁定的记录,否则插入一个新的。”

我原以为结果会是:

operation_id, type, asset_id, qty, locked
1, 'A', 1, 10, true
1, 'A', 1, 20, true
1, 'A', 2, 5, true
1, 'A', 2, 115, null

更新

刺探我想你想要的东西。显然没有经过测试,但这应该是一个合理的起点。

CREATE OR REPLACE FUNCTION upsert_bin_effect(operation_id integer, typ text, asset_id integer, qty double precision) RETURNS boolean AS $$
DECLARE
    existing_locked RECORD;
    existing_unlocked RECORD;
    net_qty float;
    unlocked_ct integer;

BEGIN
    existing_locked := (SELECT SUM(qty) AS qty INTO existing FROM bin_effect be WHERE be.operation_id = operation_id AND be.type = typ AND be.asset_id = asset_id AND be.locked = true GROUP BY be.operation_id, be.type, be.asset_id);
    net_qty = qty - existing.locked.qty;
    
    existing_unlocked := (SELECT * INTO existing FROM bin_effect be WHERE be.operation_id = operation_id AND be.type = typ AND be.asset_id = asset_id AND (be.locked = false OR be.locked IS NULL));
GET DIAGNOSTICS unlocked_ct = ROW_COUNT;
    IF EXISTS(existing_unlocked)
    THEN
        IF unlocked_ct = 1 THEN
           UPDATE bin_effect be SET be.qty = net_qty WHERE be.id = existing_unlocked.id;
        ELSEIF unlocked_ct > 1
            --Not sure if you want this to happen, included as example.
            RAISE EXCEPTION 'Too many unlocked row';
        END IF;
    ELSE
        INSERT INTO bin_effect be (be.operation_id, be."type", be.asset_id, be.qty) VALUES (operation_id, typ, asset_id, qty);
    END IF;
    
END;
$$ LANGUAGE plpgsql;

答案 1 :(得分:1)

让我在开头说一下,这可能有点XY problem。 Postgres具有锁定事务中的行的机制,但无需修改它们。这意味着其他事务无法更新它们,但仍可以读取其预锁定状态。这称为SELECT FOR UPDATE。因此,您的upsert_bin_effect可以读取锁定的行,但无法对其进行修改。

如果在未锁定的行上创建唯一索引,则可以使用INSERT ... ON CONFLICT UPDATE在单个查询中执行此操作。给出以下示例架构:

create table bin_effect (
    operation_id integer not null,
    typ text not null,
    asset_id integer not null,
    qty double precision not null,
    locked boolean not null
    -- don't understand why you were using null to indicated that the row was not locked
);

-- create a partial unique index that guarantees at most one unlocked row per "key"
create unique index bin_effect_unqiue_unlocked
    on bin_effect (operation_id, typ, asset_id) where not locked;

然后给一个从以下插入中初始化的表:

insert into bin_effect values
    (1, 'A', 1, 10, true),
    (1, 'A', 1, 20, true),
    (1, 'A', 2, 5, true),
    (1, 'A', 2, 15, false);

然后,以下查询将为给定的operation_idtypasset_id插入未锁定的行或更新未锁定的行。然后,您可以直接或作为存储函数的一部分将此查询用作参数化查询。注意这是operation_id = 1typ = 'A'asset_id = 1的原始查询,新查询的数量为100。

-- pre-calculate the desired qty, so we only compute it once
with new_value as (
    select
        -- use coalesce for when there no matching rows in table (locked or otherwise)
        100 - coalesce(sum(qty), 0) as qty
    from bin_effect
    where 
        operation_id = 1
        and typ = 'A'
        and asset_id = 1
        and locked
)
-- try to insert a new row
insert into bin_effect (operation_id, typ, asset_id, locked, qty)
    values
        (1, 'A', 1, false, (select qty from new_value))
    -- if the insertion fails, then update the pre-existing row
    on conflict (operation_id, typ, asset_id) where not locked 
        do update set qty = (select qty from new_value) 
;

作为存储函数:

create or replace function upsert_bin_effect(operation_id_ integer, typ_ text, asset_id_ integer, new_qty double precision)
returns double precision as $$
    with new_value as (
        select 
            new_qty - coalesce(sum(qty), 0) as qty
        from bin_effect
        where 
            operation_id = operation_id_
            and typ = typ_
            and asset_id = asset_id_
            and locked
    )
    insert into bin_effect (operation_id, typ, asset_id, locked, qty)
        values
            (operation_id_, typ_, asset_id_, false, (select qty from new_value))
        on conflict (operation_id, typ, asset_id) where not locked
            do update set qty = (select qty from new_value)
        returning qty
    ;
$$
language sql;

用法和输出示例:

postgres=# select upsert_bin_effect(1, 'A', 1, 100);
 upsert_bin_effect
-------------------
                70
(1 row)


postgres=# select upsert_bin_effect(1, 'A', 2, 100);
 upsert_bin_effect
-------------------
                95
(1 row)


postgres=# select upsert_bin_effect(1, 'A', 3, 100);
 upsert_bin_effect
-------------------
               100
(1 row)


postgres=# table bin_effect;
 operation_id | typ | asset_id | qty | locked
--------------+-----+----------+-----+--------
            1 | A   |        1 |  10 | t
            1 | A   |        1 |  20 | t
            1 | A   |        2 |   5 | t
            1 | A   |        1 |  70 | f
            1 | A   |        2 |  95 | f
            1 | A   |        3 | 100 | f
(6 rows)