2016-05-25 2 views
7

Надеюсь, я могу описать свою задачу понятным образом. У меня есть две таблицы в базе данных 12c Oracle, которые выглядят следующим образом:Oracle SQL суммирует значения до достижения другого значения

имя таблицы «Накладные» Имя

I_ID | invoice_number |  creation_date  | i_amount 
------------------------------------------------------ 
    1 | 10000000000 | 01.02.2016 00:00:00 | 30 
    2 | 10000000001 | 01.03.2016 00:00:00 | 25 
    3 | 10000000002 | 01.04.2016 00:00:00 | 13 
    4 | 10000000003 | 01.05.2016 00:00:00 | 18 
    5 | 10000000004 | 01.06.2016 00:00:00 | 12 

Таблица «платежи»

P_ID | reference |  received_date  | p_amount 
------------------------------------------------------ 
    1 | PAYMENT01  | 12.02.2016 13:14:12 | 12 
    2 | PAYMENT02  | 12.02.2016 15:24:21 | 28 
    3 | PAYMENT03  | 08.03.2016 23:12:00 | 2 
    4 | PAYMENT04  | 23.03.2016 12:32:13 | 30 
    5 | PAYMENT05  | 12.06.2016 00:00:00 | 15 

Так что я хочу, чтобы иметь выбор (возможно, с аналитическими функциями оракула, но я не очень-то знаком с ним), когда платежи суммируются до тех пор, пока сумма счета не будет достигнута, заказана по датам. Если сумма, например, двух платежей больше суммы счета, остаток оставшейся суммы платежа должен использоваться для следующего счета-фактуры.

В этом примере результат должен быть примерно так:

invoice_number | reference | used_pay_amount | open_inv_amount 
---------------------------------------------------------- 
10000000000 | PAYMENT01 |  12  |  18 
10000000000 | PAYMENT02 |  18  |   0 
10000000001 | PAYMENT02 |  10  |  15 
10000000001 | PAYMENT03 |  2  |  13 
10000000001 | PAYMENT04 |  13  |   0 
10000000002 | PAYMENT04 |  13  |   0 
10000000003 | PAYMENT04 |  4  |  14 
10000000003 | PAYMENT05 |  14  |   0 
10000000004 | PAYMENT05 |  1  |  11 

Было бы хорошо, если есть решение с «простым» оператора выбора.

ТНХ заранее за ваше время ...

+0

Это было бы действительно выполнимо в PL/Sql, но, честно говоря, я очень сомневаюсь, что это можно сделать в простом sql – Gar

+0

Ваш пример неверен - для оплаты 4 вы сказали, что сумма была 30, но в ваших выходных результатах, у вас есть 13 + 13 + 14, что составляет до 40, а не 30 ... – Boneist

+1

@Boneist - Я пошел дальше и исправил эту ошибку. Я также удалил тег 'oracle 12' - эта проблема может быть решена в гораздо более низких версиях Oracle. – mathguy

ответ

7

Настройка Oracle:

CREATE TABLE invoices (i_id, invoice_number, creation_date, i_amount) AS 
SELECT 1, 100000000, DATE '2016-01-01', 30 FROM DUAL UNION ALL 
SELECT 2, 100000001, DATE '2016-02-01', 25 FROM DUAL UNION ALL 
SELECT 3, 100000002, DATE '2016-03-01', 13 FROM DUAL UNION ALL 
SELECT 4, 100000003, DATE '2016-04-01', 18 FROM DUAL UNION ALL 
SELECT 5, 100000004, DATE '2016-05-01', 12 FROM DUAL; 

CREATE TABLE payments (p_id, reference, received_date, p_amount) AS 
SELECT 1, 'PAYMENT01', DATE '2016-01-12', 12 FROM DUAL UNION ALL 
SELECT 2, 'PAYMENT02', DATE '2016-01-13', 28 FROM DUAL UNION ALL 
SELECT 3, 'PAYMENT03', DATE '2016-02-08', 2 FROM DUAL UNION ALL 
SELECT 4, 'PAYMENT04', DATE '2016-02-23', 30 FROM DUAL UNION ALL 
SELECT 5, 'PAYMENT05', DATE '2016-05-12', 15 FROM DUAL; 

Запрос:

WITH total_invoices (i_id, invoice_number, creation_date, i_amount, i_total) AS (
    SELECT i.*, 
     SUM(i_amount) OVER (ORDER BY creation_date, i_id) 
    FROM invoices i 
), 
total_payments (p_id, reference, received_date, p_amount, p_total) AS (
    SELECT p.*, 
     SUM(p_amount) OVER (ORDER BY received_date, p_id) 
    FROM payments p 
) 
SELECT invoice_number, 
     reference, 
     LEAST(p_total, i_total) 
     - GREATEST(p_total - p_amount, i_total - i_amount) AS used_pay_amount, 
     GREATEST(i_total - p_total, 0) AS open_inv_amount 
FROM total_invoices 
     INNER JOIN 
     total_payments 
     ON ( i_total - i_amount < p_total 
      AND i_total > p_total - p_amount); 

Объяснение:

В двух подзапросах факторинга (WITH ... AS()) просто добавьте дополнительный виртуальный столбец в таблицы invoices и payments с суммой суммы счета/суммы платежа.

Вы можете связать диапазон с каждым счетом (или оплатой) в качестве совокупной суммы, подлежащей оплате (выплатой) до выставления счета-фактуры (платежа), и совокупной суммы, подлежащей выплате. Затем эти две таблицы можно объединить, где есть перекрытие этих диапазонов.

open_inv_amount является положительной разницей между суммой накопленной суммы и суммой накопленной суммы.

used_pay_amount немного сложнее, но вам нужно найти разницу между нижней суммой текущего счета и суммами платежей и более высокой суммой накопленных счетов-фактур и сумм платежей.

Выход:

INVOICE_NUMBER REFERENCE USED_PAY_AMOUNT OPEN_INV_AMOUNT 
-------------- --------- --------------- --------------- 
    100000000 PAYMENT01    12    18 
    100000000 PAYMENT02    18    0 
    100000001 PAYMENT02    10    15 
    100000001 PAYMENT03    2    13 
    100000001 PAYMENT04    13    0 
    100000002 PAYMENT04    13    0 
    100000003 PAYMENT04    4    14 
    100000003 PAYMENT05    14    0 
    100000004 PAYMENT05    1    11 

Update:

на основе метода mathguy в использовании UNION присоединиться к данным, я придумал другое решение повторно использовать некоторые из моего кода.

WITH combined (invoice_number, reference, i_amt, i_total, p_amt, p_total, total) AS (
    SELECT invoice_number, 
     NULL, 
     i_amount, 
     SUM(i_amount) OVER (ORDER BY creation_date, i_id), 
     NULL, 
     NULL, 
     SUM(i_amount) OVER (ORDER BY creation_date, i_id) 
    FROM invoices 
    UNION ALL 
    SELECT NULL, 
     reference, 
     NULL, 
     NULL, 
     p_amount, 
     SUM(p_amount) OVER (ORDER BY received_date, p_id), 
     SUM(p_amount) OVER (ORDER BY received_date, p_id) 
    FROM payments 
    ORDER BY 7, 
      2 NULLS LAST, 
      1 NULLS LAST 
), 
filled (invoice_number, reference, i_prev, i_total, p_prev, p_total) AS (
    SELECT FIRST_VALUE(invoice_number) IGNORE NULLS OVER (ORDER BY ROWNUM ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING), 
     FIRST_VALUE(reference)  IGNORE NULLS OVER (ORDER BY ROWNUM ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING), 
     FIRST_VALUE(i_total - i_amt) IGNORE NULLS OVER (ORDER BY ROWNUM ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING), 
     FIRST_VALUE(i_total)   IGNORE NULLS OVER (ORDER BY ROWNUM ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING), 
     FIRST_VALUE(p_total - p_amt) IGNORE NULLS OVER (ORDER BY ROWNUM ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING), 
     COALESCE(
      p_total, 
      LEAD(p_total) IGNORE NULLS OVER (ORDER BY ROWNUM), 
      LAG(p_total) IGNORE NULLS OVER (ORDER BY ROWNUM) 
     ) 
    FROM combined 
), 
vals (invoice_number, reference, upa, oia, prev_invoice) AS (
    SELECT invoice_number, 
     reference, 
     COALESCE(LEAST(p_total - i_total) - GREATEST(p_prev, i_prev), 0), 
     GREATEST(i_total - p_total, 0), 
     LAG(invoice_number) OVER (ORDER BY ROWNUM) 
    FROM filled 
) 
SELECT invoice_number, 
     reference, 
     upa AS used_pay_amount, 
     oia AS open_inv_amount 
FROM vals 
WHERE upa > 0 
OR  (reference IS NULL AND invoice_number <> prev_invoice AND oia > 0); 

Объяснение:

Предложение факторинга combined суб-запрос соединяет две таблицы с UNION ALL и генерирует суммарные итоги для сумм выставлением и оплаченных. Последнее, что он делает, - упорядочить строки по возрастающей совокупной сумме (и если есть связи, они будут помещать платежи, созданные по порядку, перед счетами-фактурами).

Предложение факторинга подзапроса должно заполнить ранее сгенерированную таблицу, так что если значение равно null, то оно примет значение из следующей непустой строки (и если есть счет-фактура без платежей, то он будет найдите общее количество предыдущих платежей из предыдущих строк).

Предложение факторизации подзапроса vals использует те же вычисления, что и мой предыдущий запрос (см. Выше). Он также добавляет столбец prev_invoice, чтобы помочь идентифицировать счета-фактуры, которые полностью неоплачиваются.

Окончательный SELECT принимает значения и отфильтровывает ненужные строки.

+0

Это, к сожалению, не совсем корректно, например, оно возвращает '10000000001 \t PAYMENT02 15', когда он должен был использовать только оставшиеся 10 левых от PAYMENT02 (поскольку 18 использовалось для погашения предыдущего счета-фактуры). – Boneist

+0

@Boneist Я тоже это заметил, и это было довольно простое решение. – MT0

+0

"достаточно простой" ?! Я все еще пытаюсь понять ваш ответ! Я думаю, что у тебя очень извилистый мозг действительно * {;-) (и я имею в виду это как комплимент!) – Boneist

1

Это решение, не требующее объединения. Это важно, если объем данных является значительным. Я провел некоторое тестирование на своем ноутбуке (ничего коммерческого), используя бесплатную версию (XE) Oracle 11.2. Используя решение MT0, запрос с соединением занимает около 11 секунд, если есть 10k счетов и 10k платежей. За 50 тыс. Счетов и 50 тыс. Платежей запрос занял 287 секунд (почти 5 минут). Это понятно, поскольку для объединения двух 50 тыс. Таблиц требуется 2,5 млрд сравнений.

В альтернативе ниже используется соединение. Он использует lag() и last_value() для выполнения работы, которую соединение делает в другом решении. Это решение на основе профсоюза с 50 тыс. Счетов и 50 тыс. Платежей заняло менее 0,5 секунды на моем ноутбуке (!)

Я немного упростил настройку; i_id, invoice_number и creation_date используются только для одной цели: заказывать суммы счетов. Я использую только в inv_id (счет-фактура идентификатор) для этой цели, и аналогичные по платежам ..

Для тестирования я создал таблицы invoices и payments так:

create table invoices (inv_id, inv_amt) as 
    (select level, trunc(dbms_random.value(20, 80)) from dual connect by level <= 50000); 
create table payments (pmt_id, pmt_amt) as 
    (select level, trunc(dbms_random.value(20, 80)) from dual connect by level <= 50000); 

Затем, чтобы проверить решения, Я использую запросы для заполнения CTAS, как это:

create table bal_of_pmts as 
    [select query, including the WITH clause but without the setup CTE's, comes here] 

в моем решении, я смотрю, чтобы показать распределение платежей в одной или более фактуру, а также оплату счетов из одного или нескольких платежей; результат, обсуждаемый в исходном сообщении, охватывает только половину этой информации, но для симметрии имеет смысл показать обе половины. Выход (для одних и тех же материалов, что и в оригинальном посте) выглядит следующим образом, с моей версией inv_id и pmt_id:

INV_ID  PAID  UNPAID  PMT_ID  USED AVAILABLE 
---------- ---------- ---------- ---------- ---------- ---------- 
     1   12   18  101   12   0 
     1   18   0  103   18   10 
     2   10   15  103   10   0 
     2   2   13  105   2   0 
     2   13   0  107   13   17 
     3   13   0  107   13   4 
     4   4   14  107   4   0 
     4   14   0  109   14   1 
     5   1   11  109   1   0 
     5   11   0     11 

Обратите внимание, как левая половина является то, что просил исходное сообщение. В конце есть дополнительная строка. Обратите внимание на NULL для идентификатора платежа, для оплаты 11 - это показывает, какая часть последнего платежа оставлена ​​непокрытой.Если бы был счет-фактура с id = 6, на сумму, скажем, 22, тогда была бы еще одна строка - показывая всю сумму (22) этого счета как «оплаченную» из платежа без id - на самом деле не охвачены (пока).

Запрос может быть немного легче понять, чем подход соединения. Чтобы увидеть, что он делает, это может помочь внимательно изучить промежуточные результаты, особенно CTE c (в предложении WITH).

with invoices (inv_id, inv_amt) as (
     select 1, 30 from dual union all 
     select 2, 25 from dual union all 
     select 3, 13 from dual union all 
     select 4, 18 from dual union all 
     select 5, 12 from dual 
    ), 
    payments (pmt_id, pmt_amt) as (
     select 101, 12 from dual union all 
     select 103, 28 from dual union all 
     select 105, 2 from dual union all 
     select 107, 30 from dual union all 
     select 109, 15 from dual 
    ), 
    c (kind, inv_id, inv_cml, pmt_id, pmt_cml, cml_amt) as (
     select 'i', inv_id, sum(inv_amt) over (order by inv_id), null, null, 
       sum(inv_amt) over (order by inv_id) 
      from invoices 
     union all 
     select 'p', null, null, pmt_id, sum(pmt_amt) over (order by pmt_id), 
       sum(pmt_amt) over (order by pmt_id) 
      from payments 
    ), 
    d (inv_id, paid, unpaid, pmt_id, used, available) as (
     select last_value(inv_id) ignore nulls over (order by cml_amt desc), 
       cml_amt - lead(cml_amt, 1, 0) over (order by cml_amt desc), 
       case kind when 'i' then 0 
         else last_value(inv_cml) ignore nulls 
            over (order by cml_amt desc) - cml_amt end, 
       last_value(pmt_id) ignore nulls over (order by cml_amt desc), 
       cml_amt - lead(cml_amt, 1, 0) over (order by cml_amt desc), 
       case kind when 'p' then 0 
         else last_value(pmt_cml) ignore nulls 
            over (order by cml_amt desc) - cml_amt end 
     from c 
    ) 
select inv_id, paid, unpaid, pmt_id, used, available 
from  d 
where paid != 0 
order by inv_id, pmt_id 
; 

В большинстве случаев, КТР d все, что нужно. Однако, если общая сумма для нескольких счетов-фактур точно равна суммарной сумме для нескольких платежей, мой запрос добавит строку с paid = unpaid = 0. (Решение соединения MT0 не имеет этой проблемы.) Чтобы охватить все возможные случаи, и не иметь строк без информации, мне пришлось добавить фильтр для paid != 0.

+0

Для последней строки вашего вывода 'paid' и' used' должно быть 0, а 'unpaid' должно быть 11 - Я знаю, что вы упоминаете это как «особенность», но обратный порядок кажется скорее ошибкой. Другая проблема заключается в том, что если вы измените строку в платежах с «select 107, 30 from dual union all», чтобы выбрать «107», «26» из «Dual Union all» (так что последний счет полностью не оплачен), тогда номера для счета-фактуры 5, Я думаю, не правильно. – MT0

+0

@ MT0 - с 'pmt_amt' изменено с' 30' на '26' за' pmt_id = 107', последние две строки выглядят как последняя строка в моем ответе: 'inv_id = 4' показывает сумму' paid = 3' и 'inv_id = 5' показывает' paid = 12'. '12' - это полная сумма последнего счета, поэтому номер правильный. Оба вопроса являются одним и тем же: обработка «непокрытых» сумм. Они не показаны в идеальном виде (что потребует уточнения запроса); но как я это интерпретирую, то, что показывает таблица, это просто платежи, сделанные с помощью 'pmt_id = NULL', что означает неизвестные будущие платежи. (продолжение ниже) – mathguy

+0

@ MT0 - (продолжение сверху) Я по-прежнему предпочитаю видеть оставшиеся неоплаченные суммы, как с частично оплаченного счета, так и с счетов-фактур, на которые ничего не выплачено. Мне просто нужно помнить (и документировать!), Что 'pmt_id = NULL' означает, что эти платежи еще не сделаны, они выдающиеся. Правильный способ показать их будет состоять в том, чтобы иметь еще больше столбцов за невыплаченные суммы (и симметрично для «кредитов», оставшихся в конце, если платежи превышают счета-фактуры). Это можно сделать, но точка моего решения была другой: использование объединения вместо соединения. – mathguy

Смежные вопросы