#sql #oracle #oracle11g
Вопрос:
У меня есть одна таблица криптотранзакций в приведенной ниже структуре, в которой хранятся транзакции.
ID | ТРАНЗАКЦИЯ_ТИПЕИД | ИМЯ ТРАНЗАКЦИИ | сумма |
---|---|---|---|
1 | 101 | биткоин-получено | 5 |
2 | 102 | биткоин-майнинг | 20 |
3 | 103 | биткоин-переведенный | -5 |
4 | 104 | биткойн-потерян | -10 |
5 | 101 | биткоин-получено | 55 |
6 | 102 | биткоин-майнинг | 8 |
7 | 104 | биткойн-потерян | -16 |
8 | 103 | биткоин-переведенный | -5 |
Я хочу, чтобы переведенные биткойны списывались только с добытых биткойнов, а потерянные биткойны можно вычесть из полученных биткойнов или добытых биткойнов, которые когда-либо будут первыми.
Ниже приведен ожидаемый результат
ID | ТРАНЗАКЦИЯ_ТИПЕИД | ИМЯ ТРАНЗАКЦИИ | сумма |
---|---|---|---|
1 | 101 | биткоин-получено | 0 |
2 | 102 | биткоин-майнинг | 0 |
5 | 101 | биткоин-получено | 49 |
6 | 102 | биткоин-майнинг | 3 |
Комментарии:
1. Можете ли вы показать случай, когда поведение FIFO четко определено?
2. Отображение ожидаемых результатов для входного набора данных, НЕ указанного в вашем вопросе, крайне бесполезно. Если вашего первого примера недостаточно для демонстрации всех необходимых действий, отредактируйте свой вопрос, чтобы включить расширенный набор данных. Вопрос должен, по крайней мере, соответствовать самому себе.
3. @MatBailie Я изменил вопрос с более подробной информацией.
4. Разница между ожидаемым и фактическим объясняется порядком операций, как отмечено в ответе. Поведение FIFO (ответа) в настоящее время основано на
received
mined
порядке и, а не наlost
transferred
порядке и, иtransferred
корректировки выполняются в первую очередь (вreceived
mined
порядке и). Когда у меня будет время, я попытаюсь устранить эту разницу.5. Защищены ли данные от того, чтобы конечное состояние стало отрицательным? Например, могут ли данные когда-либо отображаться
start with nothing, mine 10, then immediately transfer 15
?
Ответ №1:
Вы можете использовать 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 ТРАНЗАКЦИЯ_ТИПЕИД ИМЯ ТРАНЗАКЦИИ сумма 1 101 биткоин-получено 0 2 102 биткоин-майнинг 0 5 101 биткоин-получено 49 6 102 биткоин-майнинг 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;
/
бд<>скрипка <>здесь
Комментарии:
1. Спасибо MT0 за то, что поделился этим подходом.
2.@MT0 — Будет ли это являться оптимизацией? dbfiddle.uk/… (Уменьшение количества проверенных и пропущенных записей, что делает вложенный цикл «менее треугольным»?)
3. @MatBailie Может быть…. это было очень похоже на мое первоначальное решение, а затем я переработал его в один запрос. Это может быть или не быть оптимизацией в зависимости от того, что является ограничивающим фактором: если ограничивающим фактором является время, затраченное на ввод-вывод, то выполнение двух вариантов может снизить производительность; если ограничивающим фактором является время обработки, а ввод-вывод не является существенным фактором, то это может быть быстрее. Я предполагаю, что (особенно для небольших наборов данных) время, затраченное на анализ запроса и ввод-вывод, будет более значительным, чем затраты на окончательную обработку. Вам нужно будет составить профиль обоих и проверить.
4. @MatBailie Я обновил его более оптимизированным (и более сложным) решением.
5. @rocketpicks — Я рекомендую протестировать этот обновленный ответ.
Ответ №2:
Допущения:
- Мы не можем добывать или получать меньше 0.
- Мы не можем переводить или терять суммы, которых у нас нет.
Было неясно, что включает в себя поведение FIFO. Здесь мог бы помочь лучший тестовый случай.
Вот обновленный тестовый пример с приведенными выше данными, а затем немного больший набор данных, плюс решение, которое пытается внедрить логику FIFO:
Обновленный тестовый случай с большим количеством данных и логикой FIFO
В следующем решении для выполнения этой работы используется несколько вычислений.
В cte1
мы выводим:
run_mined
— текущая сумма (в порядке идентификаторов) типа = 102 (добытые суммы)tot_xfer
— общая сумма типа = 103 (переведенные суммы)tot_lost
— общая сумма по типу = 104 (потерянные суммы)
Затем, поскольку переведенные суммы могут быть вычтены только из добытых сумм , мы делаем это следующим cte2
шагом, корректируя суммы добытых строк.
Если общая сумма перевода больше текущей текущей суммы добытой строки, эта сумма уменьшается до 0. Мы перевели всю эту сумму.
Если общая сумма перевода не превышает текущую текущую сумму добытых данных, мы вычитаем переведенную сумму, не превышающую текущую добытую сумму для этой строки.
Любая последующая добытая строка не затрагивается, так как дальнейших переводов не происходит.
В cte1b
, мы вычисляем run2_in
, что является обновленной текущей суммой обоих mined
и received
сумм. Обратите внимание, что mined
суммы были скорректированы cte2
.
cte3
теперь выполняет расчет , аналогичный cte2
, но на этот раз корректирует оба типа received
и mined
(101 и 102) в порядке FIFO, исходя из общей оставшейся lost
суммы.
Наконец, мы выбираем только полностью настроенные 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 |
---- ------------------ --------
В обновленной скрипке приведен пример с большим количеством данных:
Новые данные:
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 |
---- ------------------ --------
Комментарии:
1. Спасибо Джону за этот подробный ответ. Это потрясающе, и это решение почти соответствует моему ожидаемому результату. Разница лишь в том, что в конечном результате Сумма для 3-й строки, т. е. ID 5, должна была быть 39 вместо 34, а сумма для 5-й строки, т. е. ID 16, должна была быть 14 вместо 19. Я думаю, что один из биткойн-переводов суммы 5 неправильно вычитается из полученных биткойнов (идентификатор 5) вместо добытых биткойнов (идентификатор 16). Биткоин-перевод разрешается списывать только с биткоин-майнинга. В данных основной таблицы после идентификатора 5 только 16 очков теряются в биткоинах, поэтому, когда мы вычитаем 55-16, мы получим 39.
2. @rocketpicks Это было сделано намеренно, так
transferred
как суммы были выполнены первыми и удалены изmined
строк. Все-5
суммы были связаныtransfers
и должны были быть взяты изmined
строк. Я не уверен, как какая-либо из этих строк повлияла наreceived
строку. Я проверю. Это не должно быть трудно найти. Результаты такие, как я и ожидал. Может быть, вы хотели немного другого порядка операций. Нам нужно будет найти эту разницу.3. @rocketpicks, Чтобы было ясно, это было не совсем FIFO, так как я обработал все
transfers
сначала, независимо отlost
случаев. Если мы хотим выполнить их все сразу, это выполнимо, но это также может привести кlost
тому, что случаи будут вычтены изmined
строк, не оставив возможности обрабатывать некоторыеtransfer
случаи (в порядке FIFO). Если вы предполагаете, что все это всегда безопасно, мы можем настроить все это так, чтобы оно обрабатывалось в течение одного и того жеCTE
срока в строгом порядке FIFO. Однако общий подход должен быть применим.4. В строке № 5 таблицы у нас
received
55. После этого остается только однаlost
сумма-16
, а остальные все естьtransferred
. Итак, если мы взяли всеtransfers
изmined
строк, то строка № 5 не станет34
правильной?received
—lost
то55
есть —16
=39
. Я не уверен, как из этого вычитались дополнительные 5, делая это34
.5. @rocketpicks, я полагаю, что осталось 5 при id = 1, а не только 55 при id = 5. Проверьте результат
cte1b
и проверьте текущийamount
столбец иrun2_in
столбец.SELECT * FROM cte1b ORDER BY id;
… dbfiddle.uk/… (последняя панель)
Ответ №3:
Следующий ответ использует рекурсию для реализации цикла.
- Вероятно, лучше написать реальный цикл…
Это связано с тем, что правила FIFO означают, что невозможно заранее знать, какие mined
/ received
записи будут уменьшены на lost
запись. Поэтому каждая lost
/ transferred
запись должна быть полностью обработана, прежде чем может начаться выделение для следующей lost
/ transferred
записи.
Затем я использовал следующую логику…
income
Запись — это когдаtransaction_typeid
есть101
или102
.outgoing
Запись — это когдаtransaction_typeid
есть103
или104
.- Если
outgoing
это тип104
/lost
, он может быть применен к любому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>)
Используя оконные функции, это становится (псевдокодом)…
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
;
Вот демонстрационная версия, основанная на том, о чем вы спрашиваете.
Комментарии:
1. Спасибо, Матбайли, но я думаю, что этот запрос также дает тот же результат 0,0,44,8. Решение Джона также дает мне этот результат, но мой ожидаемый результат составляет 0,0,49,3.
2. @rocketpicks В моем
CASE
выражении была опечатка, 101 должно было быть 102 (это вычиталосьtransfered
изreceived
, а не изmined
) Я обновил ответ и теперь играю на скрипке.3. @rocketpicks Я был бы не в курсе, с какой настройкой вы это профилировали. Я ожидал, что ответ MT0 будет наиболее эффективным для больших наборов данных.
Ответ №4:
Здесь та же логика, что и в моем рекурсивном 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;
ДЕМОНСТРАЦИЯ…
Комментарии:
1. Спасибо, Матбайли. Мне интересно, почему он потерпит неудачу после 2000 исходящих записей. Не могли бы вы, пожалуйста, поделиться более подробной информацией об этом.
2. Рекурсивные запросы ограничены глубиной рекурсии 2000. Они не предназначены для использования в неопределенных циклах.
3. @MatBailie, Пожалуйста, добавьте цитату для этого, так как на первый взгляд это не похоже на истинную бд<>скрипка<>
4. @MT0 — Я исправляюсь. Я не знаю, где я «выучил» лимит 2000, но ваша скрипка объективно опровергает его. Спасибо.
5. @rocketpicks Согласно комментарию MT0. Мой рекурсивный CTE не ограничен тем, как я думал. Подходит ли вам CTE или цикл лучше всего, будет зависеть от ваших потребностей, попробуйте как с реальными данными, так и посмотрите, как это работает.