使用 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
假设:
- 我们不能开采或收到少于 0。
- 我们不能转移或丢失我们没有的金额。
不清楚 FIFO 行为涉及什么。一个更好的测试用例可能会有所帮助。
这是一个包含上述数据的更新测试用例,然后是一个稍大的数据集,以及一个尝试引入 FIFO 逻辑的解决方案:
The updated test case with more data and FIFO logic
以下解决方案使用一些计算来完成工作。
在cte1
中我们得出:
run_mined
- 运行 总和(按 id 顺序)类型 = 102(开采量)
tot_xfer
- 类型总计=103(转账金额)
tot_lost
- 类型总计=104(损失金额)
然后由于转账金额只能从开采金额中扣除,我们接下来在 cte2
中执行此操作,调整开采行的金额。
如果总转账金额大于挖掘行的当前 运行 总和,则该金额将减为 0。我们已转账所有这些金额。
如果总转账金额不大于当前运行挖掘数据总和,我们扣除转账金额,不大于该行当前挖掘金额。
不会触及任何后续挖掘的行,因为没有进一步的传输。
在 cte1b
中,我们计算 run2_in
,这是 mined
和 received
金额的更新后 运行 总和。请注意,mined
金额在 cte2
中进行了调整。
cte3
现在执行类似于 cte2
的计算,但这次根据 FIFO 顺序调整类型 received
和 mined
(101 和 102),基于总剩余 lost
金额。
最后,我们 select 只显示完全调整的 received
和 mined
行,以及相应的 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_typeid
为 101
或 102
时。
outgoing
记录是当 transaction_typeid
为 103
或 104
时。
- 如果
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
我在下面的结构中有一个 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 |
假设:
- 我们不能开采或收到少于 0。
- 我们不能转移或丢失我们没有的金额。
不清楚 FIFO 行为涉及什么。一个更好的测试用例可能会有所帮助。
这是一个包含上述数据的更新测试用例,然后是一个稍大的数据集,以及一个尝试引入 FIFO 逻辑的解决方案:
The updated test case with more data and FIFO logic
以下解决方案使用一些计算来完成工作。
在cte1
中我们得出:
run_mined
- 运行 总和(按 id 顺序)类型 = 102(开采量)tot_xfer
- 类型总计=103(转账金额)tot_lost
- 类型总计=104(损失金额)
然后由于转账金额只能从开采金额中扣除,我们接下来在 cte2
中执行此操作,调整开采行的金额。
如果总转账金额大于挖掘行的当前 运行 总和,则该金额将减为 0。我们已转账所有这些金额。
如果总转账金额不大于当前运行挖掘数据总和,我们扣除转账金额,不大于该行当前挖掘金额。
不会触及任何后续挖掘的行,因为没有进一步的传输。
在 cte1b
中,我们计算 run2_in
,这是 mined
和 received
金额的更新后 运行 总和。请注意,mined
金额在 cte2
中进行了调整。
cte3
现在执行类似于 cte2
的计算,但这次根据 FIFO 顺序调整类型 received
和 mined
(101 和 102),基于总剩余 lost
金额。
最后,我们 select 只显示完全调整的 received
和 mined
行,以及相应的 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_typeid
为101
或102
时。outgoing
记录是当transaction_typeid
为103
或104
时。- 如果
outgoing
是类型104
/lost
,它可以应用于anyincome
类型。否则,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