使用 SQL 从学分中有条件地借记

Conditional Debit from Credits using SQL

我在下面的结构中有一个 table 加密交易存储交易。

ID TRANSACTION_TYPEID TRANSACTION_NAME AMOUNT
1 101 bitcoin-received 5
2 102 bitcoin-mined 20
3 103 bitcoin-transferred -5
4 104 bitcoin-lost -10
5 101 bitcoin-received 55
6 102 bitcoin-mined 8
7 104 bitcoin-lost -16
8 103 bitcoin-transferred -5

我希望比特币转移只从比特币开采中扣除,比特币丢失可以从比特币接收或比特币开采中扣除,以先到者为准。

下面是预期结果

ID TRANSACTION_TYPEID TRANSACTION_NAME AMOUNT
1 101 bitcoin-received 0
2 102 bitcoin-mined 0
5 101 bitcoin-received 49
6 102 bitcoin-mined 3

假设:

  1. 我们不能开采或收到少于 0。
  2. 我们不能转移或丢失我们没有的金额。

不清楚 FIFO 行为涉及什么。一个更好的测试用例可能会有所帮助。

这是一个包含上述数据的更新测试用例,然后是一个稍大的数据集,以及一个尝试引入 FIFO 逻辑的解决方案:

The updated test case with more data and FIFO logic

以下解决方案使用一些计算来完成工作。

cte1中我们得出:

  1. run_mined - 运行 总和(按 id 顺序)类型 = 102(开采量)
  2. tot_xfer - 类型总计=103(转账金额)
  3. tot_lost - 类型总计=104(损失金额)

然后由于转账金额只能从开采金额中扣除,我们接下来在 cte2 中执行此操作,调整开采行的金额。

如果总转账金额大于挖掘行的当前 运行 总和,则该金额将减为 0。我们已转账所有这些金额。

如果总转账金额不大于当前运行挖掘数据总和,我们扣除转账金额,不大于该行当前挖掘金额。

不会触及任何后续挖掘的行,因为没有进一步的传输。

cte1b 中,我们计算 run2_in,这是 minedreceived 金额的更新后 运行 总和。请注意,mined 金额在 cte2 中进行了调整。

cte3 现在执行类似于 cte2 的计算,但这次根据 FIFO 顺序调整类型 receivedmined(101 和 102),基于总剩余 lost 金额。

最后,我们 select 只显示完全调整的 receivedmined 行,以及相应的 id 以指示 FIFO 操作的顺序进行了。

SQL:

WITH cte1 AS (
         SELECT a.*
              , SUM(CASE WHEN (transaction_typeid = 102) THEN 1 ELSE 0 END * amount) OVER (ORDER BY id) AS run_mined   
              , SUM(CASE WHEN (transaction_typeid = 103) THEN 1 ELSE 0 END * amount) OVER ()            AS tot_xfer    
              , SUM(CASE WHEN (transaction_typeid = 104) THEN 1 ELSE 0 END * amount) OVER ()            AS tot_lost    
           FROM cryptotransactionledger a
          ORDER BY id
     )
   , cte2 AS (
         SELECT a.id, a.transaction_typeid,  a.transaction_name
              , CASE WHEN transaction_typeid <> 102        THEN amount
                     WHEN run_mined  <= ABS(tot_xfer )     THEN 0
                     WHEN run_mined  + tot_xfer  >= amount THEN amount
                                                           ELSE run_mined  + tot_xfer 
                 END AS amount
              , run_mined 
              , tot_xfer 
              , tot_lost 
              , amount AS amount1
           FROM cte1 a
     )
   , cte1b AS (
         SELECT a.*
              , SUM(CASE WHEN (transaction_typeid IN (101, 102)) THEN 1 ELSE 0 END * amount) OVER (ORDER BY id) AS run2_in     
           FROM cte2 a
     )
   , cte3 AS (
         SELECT a.id, a.transaction_typeid,  a.transaction_name
              , CASE WHEN transaction_typeid NOT IN (101, 102) THEN amount
                     WHEN run2_in    <= ABS(tot_lost )         THEN 0
                     WHEN run2_in    + tot_lost  >= amount     THEN amount
                                                               ELSE run2_in    + tot_lost 
                 END AS amount
              , run_mined 
              , tot_xfer 
              , tot_lost 
              , run2_in
              , amount1
              , amount AS amount2
           FROM cte1b a
     )
SELECT id, transaction_name, amount
  FROM cte3
 WHERE transaction_typeid IN (101, 102)
 ORDER BY id
;

使用原始问题(简单案例)的数据得出的结果:

+----+------------------+--------+
| id | transaction_name | amount |
+----+------------------+--------+
|  1 | bitcoin-received |      0 |
|  2 | bitcoin-mined    |     10 |
+----+------------------+--------+

在更新的fiddle中,提供了一个包含更多数据的示例:

新数据:

create table cryptotransactionledger as
    select  1 as id, 101 as transaction_typeid, 'bitcoin-received'    as transaction_name,   5 as amount from dual union all
    select  2 as id, 102 as transaction_typeid, 'bitcoin-mined'       as transaction_name,  20 as amount from dual union all
    select  3 as id, 103 as transaction_typeid, 'bitcoin-transferred' as transaction_name,  -5 as amount from dual union all
    select  4 as id, 104 as transaction_typeid, 'bitcoin-lost'        as transaction_name, -10 as amount from dual union all
    select  5 as id, 101 as transaction_typeid, 'bitcoin-received'    as transaction_name,  55 as amount from dual union all
    select 15 as id, 102 as transaction_typeid, 'bitcoin-mined'       as transaction_name,   8 as amount from dual union all
    select 16 as id, 102 as transaction_typeid, 'bitcoin-mined'       as transaction_name,  20 as amount from dual union all
    select 17 as id, 102 as transaction_typeid, 'bitcoin-mined'       as transaction_name,  30 as amount from dual union all
    select 18 as id, 103 as transaction_typeid, 'bitcoin-transferred' as transaction_name,  -5 as amount from dual union all
    select 19 as id, 103 as transaction_typeid, 'bitcoin-transferred' as transaction_name,  -5 as amount from dual union all
    select 20 as id, 103 as transaction_typeid, 'bitcoin-transferred' as transaction_name,  -5 as amount from dual union all
    select 30 as id, 103 as transaction_typeid, 'bitcoin-transferred' as transaction_name,  -5 as amount from dual union all
    select 31 as id, 103 as transaction_typeid, 'bitcoin-transferred' as transaction_name,  -4 as amount from dual union all
    select 40 as id, 104 as transaction_typeid, 'bitcoin-lost'        as transaction_name, -16 as amount from dual union all
    select 99 as id, 103 as transaction_typeid, 'bitcoin-transferred' as transaction_name,  -5 as amount from dual WHERE 1 = 0
;

结果:

+----+------------------+--------+
| id | transaction_name | amount |
+----+------------------+--------+
|  1 | bitcoin-received |      0 |
|  2 | bitcoin-mined    |      0 |
|  5 | bitcoin-received |     34 |
| 15 | bitcoin-mined    |      0 |
| 16 | bitcoin-mined    |     19 |
| 17 | bitcoin-mined    |     30 |
+----+------------------+--------+

以下答案滥用递归来实现循环。

  • 写一个实际的循环可能更好...

这是因为 FIFO 规则意味着不可能事先知道哪些 mined/received 记录将被 lost 记录递减。因此,必须对每个 lost/transferred 记录进行完全处理,然后才能开始分配下一个 lost/transferred 记录。

然后,我使用了以下逻辑...

  • income 记录是当 transaction_typeid101102 时。
  • outgoing 记录是当 transaction_typeid103104 时。
  • 如果outgoing是类型104/lost,它可以应用于any income 类型。否则,outgoing 必须是类型 103/transferred 并且只能应用于 income 类型 102/mined.

然后...

  • 创建包含所有 income 条记录的记录集
  • outgoing 条记录一次添加到该集合 (首先是最低的 id
  • 第一条income记录最多可以分配到LEAST(in.amount, out.amount)
  • 对于第 2 条记录,变为 LEAST(in.amount, out.amount - <amount allocated to row1>)

使用window函数,变成(伪代码)...

LEAST(
  in.amount,
  GREATEST(
    0,
    out.amount - SUM(in.amount) OVER (<all-preceding-rows>)
  )
)
WHERE out.transcation_type_id = 104
   OR  in.transaction_type_id = 102

所以,最后的(相当长)查询是...

WITH
  income
AS
(
  SELECT
    c.id,
    c.transaction_typeid,
    c.amount
  FROM
    cryptotransactionledger  c
  WHERE
    c.transaction_typeid IN (101, 102)
),
  outgoing
AS
(
  SELECT
    o.*,
    ROW_NUMBER() OVER (ORDER BY o.id)  AS seq_num
  FROM
    cryptotransactionledger  o
  WHERE
    o.transaction_typeid IN (103, 104)
),
  fifo(
    depth, id, transaction_typeid, amount
  )
AS
(
  SELECT 0, i.* FROM income i
  ---------
  UNION ALL
  ---------
  SELECT
    f.depth + 1,
    f.id,
    f.transaction_typeid,
    f.amount
    -
    LEAST(
      -- everything remaining
      f.amount,
      -- the remaining available deductions
      GREATEST(
        0,
        CASE WHEN o.transaction_typeid = 104 THEN -o.amount
             WHEN f.transaction_typeid = 102 THEN -o.amount
                                             ELSE 0         END
        -
        -- the total amount from all preceding income rows
        COALESCE(
          SUM(CASE WHEN o.transaction_typeid = 104 THEN f.amount
                   WHEN f.transaction_typeid = 102 THEN f.amount
                                                   ELSE 0         END
          )
          OVER (ORDER BY f.id
                    ROWS BETWEEN UNBOUNDED PRECEDING
                             AND         1 PRECEDING
          ),
          0
        )
      )
    )
  FROM
    fifo     f
  INNER JOIN
    outgoing o
      ON o.seq_num = f.depth + 1
)
SELECT
  f.*
FROM
  fifo  f
WHERE
  f.depth = (SELECT MAX(depth) FROM fifo)
ORDER BY
  f.id
;

这是一个演示,基于您提出的问题。

这里的逻辑和我的递归 CTE 是一样的,但是写成一个纯循环。

  • 递归 CTE 将在 2000 outgoing 条记录后失败。

创建一个临时文件 table 来保存正在处理的值...

CREATE GLOBAL TEMPORARY TABLE temp_cryptotransactionledger (
  id                  INT,
  transaction_typeid  INT,
  transaction_name    VARCHAR2(32),
  amount              INT
);

遍历每个 outgoing 记录,并应用 FIFO 逻辑...

DECLARE
  CURSOR cur_outgoing IS
    SELECT id, transaction_typeid, amount
      FROM cryptotransactionledger
     WHERE transaction_typeid IN (103, 104)
  ORDER BY id;
BEGIN
  INSERT INTO temp_cryptotransactionledger
    SELECT c.*
      FROM cryptotransactionledger c
     WHERE c.transaction_typeid IN (101, 102);

  FOR o
  IN cur_outgoing
  LOOP
    MERGE INTO
      temp_cryptotransactionledger  t
    USING
    (
      SELECT
        i.id,
        LEAST(
          i.amount,
          GREATEST(
            0,
            i.amount - o.amount - SUM(i.amount) OVER (ORDER BY i.id)
          )
        )
          AS amount
      FROM
        temp_cryptotransactionledger  i
      WHERE
            i.id     < o.id
        AND i.amount > 0
        AND (
             o.transaction_typeid = 104
          OR i.transaction_typeid = 102
        )
    )
      f
        ON  (t.id = f.id)
    WHEN MATCHED THEN UPDATE SET
      t.amount = t.amount - f.amount
    ;
  END LOOP;
END;
/

Select 出结果...

SELECT * FROM temp_cryptotransactionledger;

演示...

您可以使用 PIPELINED 函数并且只读取 table 一次:

CREATE FUNCTION process_cryptotransledger
  RETURN cryptotransactionledger_ttype PIPELINED
IS
  transactions cryptotransactionledger_ttype;
  loss_amount INT;
BEGIN
  SELECT cryptotransactionledger_type(
           id,
           transaction_typeid,
           transaction_name,
           amount
         )
  BULK COLLECT INTO transactions
  FROM   cryptotransactionledger
  ORDER BY id;
  
  
  FOR loss IN 1 .. transactions.COUNT
  LOOP
    IF transactions(loss).transaction_name
         IN ('bitcoin-received', 'bitcoin-mined')
    THEN
      CONTINUE;
    END IF;

    loss_amount := transactions(loss).amount;

    FOR gain IN 1 .. transactions.COUNT
    LOOP
      EXIT WHEN loss_amount >= 0;
      
      IF transactions(gain).amount <= 0
      OR (
         transactions(gain).transaction_name <> 'bitcoin-mined'
         AND transactions(loss).transaction_name = 'bitcoin-transferred'
      )
      THEN
        CONTINUE;
      END IF;
      
      IF -loss_amount >= transactions(gain).amount THEN
        loss_amount := loss_amount + transactions(gain).amount;
        transactions(gain).amount := 0;
      ELSE
        transactions(gain).amount := transactions(gain).amount + loss_amount;
        loss_amount := 0;
      END IF;
    END LOOP;
  END LOOP;

  FOR i IN 1 .. transactions.COUNT
  LOOP
    IF transactions(i).transaction_name
         IN ('bitcoin-received', 'bitcoin-mined')
    THEN
      PIPE ROW (transactions(i));
    END IF;
  END LOOP;
END;
/

定义数据类型后:

CREATE TYPE cryptotransactionledger_type AS OBJECT(
  id                 INT,
  transaction_typeid INT,
  transaction_name   VARCHAR2(30),
  amount             INT
);

CREATE TYPE cryptotransactionledger_ttype
  AS TABLE OF cryptotransactionledger_type;

然后,对于示例数据:

CREATE TABLE cryptotransactionledger (
  id, transaction_typeid, transaction_name, amount
) AS
  SELECT 1, 101, 'bitcoin-received',      5 FROM DUAL UNION ALL
  SELECT 2, 102, 'bitcoin-mined',        20 FROM DUAL UNION ALL
  SELECT 3, 103, 'bitcoin-transferred',  -5 FROM DUAL UNION ALL
  SELECT 4, 104, 'bitcoin-lost',        -10 FROM DUAL UNION ALL
  SELECT 5, 101, 'bitcoin-received',     55 FROM DUAL UNION ALL
  SELECT 6, 102, 'bitcoin-mined',         8 FROM DUAL UNION ALL
  SELECT 7, 104, 'bitcoin-lost',        -16 FROM DUAL UNION ALL
  SELECT 8, 103, 'bitcoin-transferred',  -5 FROM DUAL;

查询:

SELECT *
FROM   TABLE(process_cryptotransledger());

输出:

ID TRANSACTION_TYPEID TRANSACTION_NAME AMOUNT
1 101 bitcoin-received 0
2 102 bitcoin-mined 0
5 101 bitcoin-received 49
6 102 bitcoin-mined 3

更新

如果 table 很大,那么更有效的解决方案可能是分批处理它(同样,只从 table 读取一次)并将收益保存在单独的集合中并在它们完全处理后立即将它们作为输出传输:

CREATE OR REPLACE FUNCTION process_cryptotransledger
  RETURN cryptotransactionledger_ttype PIPELINED
IS
  CURSOR transactions_cur IS
    SELECT cryptotransactionledger_type(
             id,
             transaction_typeid,
             transaction_name,
             amount
           )
    FROM   cryptotransactionledger
    ORDER BY id;

  transactions cryptotransactionledger_ttype;
  loss         cryptotransactionledger_type;
  gains        cryptotransactionledger_ttype := cryptotransactionledger_ttype();
  gain         PLS_INTEGER;
BEGIN
  OPEN transactions_cur;
  
  LOOP
    FETCH transactions_cur
    BULK COLLECT INTO transactions
    LIMIT 1000; -- Set the batch size to an appropriate level.
    
    EXIT WHEN transactions.COUNT = 0;
    
    FOR i IN 1 .. transactions.COUNT
    LOOP
      -- Process each item in the batch.

      IF transactions(i).transaction_name
           IN ('bitcoin-received', 'bitcoin-mined')
      THEN
        -- Store the gains.
        gains.EXTEND();
        gains(gains.LAST) :=  transactions(i);
        CONTINUE;
      END IF;

      -- Process each loss.
      loss := transactions(i);

      gain := gains.FIRST;
      WHILE gain IS NOT NULL AND gains(gain).amount = 0
      LOOP
        -- Pipe the fully processed gain rows
        PIPE ROW( gains(gain) );
        gains.DELETE(gain);
        gain := gains.FIRST;
      END LOOP;
      
      -- Update the first appropriate gain row(s) with the loss amount.
      WHILE gain IS NOT NULL AND loss.amount < 0
      LOOP
        IF gains(gain).amount > 0
        AND (
           gains(gain).transaction_name = 'bitcoin-mined'
           OR loss.transaction_name <> 'bitcoin-transferred'
        )
        THEN
          IF -loss.amount >= gains(gain).amount THEN
            loss.amount := loss.amount + gains(gain).amount;
            gains(gain).amount := 0;
          ELSE
            gains(gain).amount := gains(gain).amount + loss.amount;
            loss.amount := 0;
          END IF;
        END IF;
        gain := gains.NEXT(gain);
      END LOOP;
    END LOOP;
  END LOOP;
  
  CLOSE transactions_cur;

  -- Pipe the remaining gain rows.
  FOR i IN gains.FIRST .. gains.LAST
  LOOP
    PIPE ROW (gains(i));
  END LOOP;
END;
/

db<>fiddle here