【问题标题】:Conditional Debit from Credits using SQL使用 SQL 从贷方进行有条件借记
【发布时间】:2021-11-20 16:11:49
【问题描述】:

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

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

【问题讨论】:

  • 你能展示一个明确定义 FIFO 行为的案例吗?
  • 显示不在您问题中的输入数据集的预期结果是非常无益的。如果您的第一个示例不足以展示所有必要的行为,请编辑您的问题以包含扩展的数据集。这个问题至少应该与它本身保持一致。
  • @MatBailie 我已经用更多细节修改了这个问题。
  • 预期和实际之间的差异是由于操作的顺序,如答案中所述。 FIFO 行为(答案)当前基于receivedmined 顺序,而不是losttransferred 顺序,并且首先完成transferred 调整(在received 和@ 987654329@ 订购)。当我有时间时,我会尝试解决这个差异。
  • 数据是否受到保护以防止最终状态变为负数?例如,数据能否显示start with nothing, mine 10, then immediately transfer 15

标签: sql oracle oracle11g


【解决方案1】:

假设:

  1. 我们不能挖掘或接收小于 0。
  2. 我们不能转移或损失我们没有的金额。

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

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

The updated test case with more data and FIFO logic

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

cte1 中我们得出:

  1. run_mined - type = 102(挖掘量)的运行总和(按 id 顺序)
  2. tot_xfer - 类型总数 = 103(已转移金额)
  3. tot_lost - 类型总数 = 104(丢失数量)

那么由于转移的金额只能从挖出的金额中扣除,我们接下来在cte2 中执行此操作,调整挖出的行数。

如果总转移总和大于挖掘行的当前运行总和,则该金额减少到 0。我们已经转移了所有这些金额。

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

任何后续开采的行都不会被触及,因为没有进一步的转移。

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

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

最后,我们只选择完全调整后的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 |
+----+------------------+--------+

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

新数据:

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 |
+----+------------------+--------+

【讨论】:

  • 感谢 Jon 的详细回答。这太棒了,这个解决方案几乎符合我的预期结果。唯一的区别是,在最终结果中,第 3 行的金额,即 ID 5 应该是 39 而不是 34,第 5 行的金额,即 ID 16 应该是 14 而不是 19。我认为金额 5 的比特币转账之一是错误地从比特币接收(ID 5)而不是比特币开采(ID 16)中扣除。比特币转账只允许从比特币开采中扣除。在 ID 5 之后的主表数据中,只有 16 个点是比特币丢失的,所以当我们减去 55 - 16 时,我们得到 39。
  • @rocketpicks 这是故意的,因为transferred 的金额首先被执行,并从mined 行中删除。所有-5 金额都与transfers 相关,需要从mined 行中获取。我不确定这些行中的任何一个如何影响received 行。我会检讨。这应该不难找到。结果和我预期的一样。也许您想要稍微不同的操作顺序。我们需要找到这种差异。
  • @rocketpicks 需要明确的是,这并不完全是 FIFO,因为我首先处理了所有 transfers,而不考虑 lost 案例。如果我们想一次完成所有这些,这是可行的,但这也可能导致从mined 行中减去lost 案例,从而无法处理某些transfer 案例(按FIFO 顺序)。如果您假设所有这些始终是安全的,我们可以将所有这些调整为在相同的CTE 期限内以严格的 FIFO 顺序处理。 不过,一般方法应该是可用的。
  • 在表格的第 5 行,我们有 received 55。之后,只有一个 lost 的数量为 -16,其余都是 transferred。所以如果我们从mined 行中取出所有transfers,那么第5 行就不会变成34 对吧? received - lost55 - 16 = 39。我不确定是如何从其中扣除额外的 5 使其成为34
  • @rocketpicks 我相信在 id = 1 处还有 5 个,而不仅仅是在 id = 5 处的 55 个。检查cte1b 的结果并检查当前amount 列和run2_in 列。 SELECT * FROM cte1b ORDER BY id; ... dbfiddle.uk/…(最后一个面板)
【解决方案2】:

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

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

这是因为 FIFO 规则意味着无法提前知道哪些 mined/received 记录将被 lost 记录减少。因此,它们每个 lost/transferred 记录必须被完全处理,然后才能开始分配下一个 lost/transferred 记录。

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

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

那么……

  • 创建一个包含所有 income 记录的记录集
  • outgoing 记录加入到该集合中,一次一个(最低id 第一)
  • 最多可以分配给第一条income记录是LEAST(in.amount, out.amount)
  • 对于第二条记录,变为LEAST(in.amount, out.amount - &lt;amount allocated to row1&gt;)

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

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
;

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

【讨论】:

  • 感谢 MatBailie,但我认为这个查询也给出了相同的结果 0,0,44,8。 Jon 的解决方案也给了我这个结果,但我的预期结果是 0,0,49,3。
  • @rocketpicks 我的CASE 表达式中有一个错字,101 应该是 102(它是从 received 而不是从 mined 中扣除 transfered)我已经更新了答案和现在摆弄。
  • @rocketpicks 我很想知道您使用的设置。我本来希望 MT0 的答案在更大的数据集上最有效。
【解决方案3】:

这里的逻辑与我的递归 CTE 相同,但写成纯循环。

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

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

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 * FROM temp_cryptotransactionledger;

演示...

【讨论】:

  • 感谢 MatBailie。我想知道为什么在 2000 条传出记录之后它会失败。能否请您分享有关此的更多详细信息。
  • 递归查询的递归深度限制为 2000。它们不适合用于无限循环。
  • @MatBailie 请为此添加引用,因为乍一看,它似乎不是真的db<>fiddle
  • @MT0 - 我的立场是正确的。我不知道我在哪里“学到”了 2000 的限制,但你的小提琴客观地反驳了它。谢谢。
  • @rocketpicks 根据 MT0 的评论。我的递归 CTE 并不像我想象的那样受到限制。 CTE 或循环是否最适合您将取决于您的需求,使用真实世界的数据进行试验,看看效果如何。
【解决方案4】:

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

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

更新

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

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小提琴here

【讨论】:

  • 感谢 MT0 分享这种方法。
  • @MT0 - 这会构成优化吗? dbfiddle.uk/…(减少检查和跳过的记录数,使嵌套循环“不那么三角形”?)
  • @MatBailie Maybe.... 这与我最初的解决方案非常相似,然后我将其重构为单个查询。它可能是也可能不是优化,具体取决于限制因素:如果限制因素是花费在 IO 上的时间,那么执行两次选择可能会降低性能;如果限制因素是处理时间并且 IO 不是一个重要因素,那么它可能会更快。我的猜测是(特别是对于小型数据集)解析查询和 IO 所花费的时间将比最终处理的开销更重要。您需要对两者进行分析并检查。
  • @MatBailie 我已经用更优化(和更复杂)的解决方案对其进行了更新。
  • @rocketpicks - 我建议测试这个更新的答案。
猜你喜欢
  • 2021-11-18
  • 1970-01-01
  • 2017-01-27
  • 2011-08-10
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多