Oracle SQL вычисление среднего / начального / конечного сальдо по дискретным данным - PullRequest
0 голосов
/ 12 февраля 2020

У меня есть остатки на счетах, подобные этому

acc_no      balance  balance_date
account1    5000     2020-01-01
account1    6000     2020-01-05
account2    3000     2020-01-01
account1    3500     2020-01-08
account2    7500     2020-01-15

Эффективный баланс за любой день без ввода баланса равен последнему балансу. Например, баланс счета 1 на 2,3,4 января составляет 5000 et c.

Я хотел бы получить среднесуточное значение, начальное и конечное сальдо из этих данных за любой период. Я пришел к следующему запросу, и он работает, но он занимает полчаса, когда я запускаю его с полным набором данных. Мой подход правильный или есть более эффективный метод?

WITH cte_period
AS (
    SELECT '2020-01-01' date_from
        ,'2020-01-31' date_to
    FROM dual
    )
    ,cte_calendar
AS (
    SELECT rownum
        ,(
            SELECT to_date(date_from, 'YYYY-MM-DD')
            FROM cte_period
            ) + rownum - 1 AS balance_day
    FROM dual connect BY rownum <= (
            SELECT to_date(date_to, 'YYYY-MM-DD')
            FROM cte_period
            ) - (
            SELECT to_date(date_from, 'YYYY-MM-DD')
            FROM cte_period
            ) + 1
    )
    ,cte_balances
AS (
    SELECT 'account1' acc_no
        ,5000 balance
        ,to_date('2020-01-01', 'YYYY-MM-DD') sys_date
    FROM dual

    UNION ALL

    SELECT 'account1'
        ,6000
        ,to_date('2020-01-05', 'YYYY-MM-DD')
    FROM dual

    UNION ALL

    SELECT 'account2'
        ,3000
        ,to_date('2020-01-01', 'YYYY-MM-DD')
    FROM dual

    UNION ALL

    SELECT 'account1'
        ,3500
        ,to_date('2020-01-08', 'YYYY-MM-DD')
    FROM dual

    UNION ALL

    SELECT 'account2'
        ,7500
        ,to_date('2020-01-15', 'YYYY-MM-DD')
    FROM dual
    )
    ,cte_accounts
AS (
    SELECT DISTINCT acc_no
    FROM cte_balances
    )
SELECT t.acc_no
    ,(
        SELECT eff_bal
        FROM (
            SELECT cal.balance_day
                ,acc_nos.acc_no
                ,(
                    SELECT balance
                    FROM cte_balances bal
                    WHERE bal.sys_date <= cal.balance_day
                        AND acc_nos.acc_no = bal.acc_no
                    ORDER BY bal.sys_date DESC FETCH first 1 row ONLY
                    ) eff_bal
            FROM cte_calendar cal
            CROSS JOIN cte_accounts acc_nos
            ) t1
        WHERE balance_day = (
                SELECT to_date(date_from, 'YYYY-MM-DD')
                FROM cte_period
                )
            AND t.acc_no = t1.acc_no
        ) opening_bal
    ,(
        SELECT eff_bal
        FROM (
            SELECT cal.balance_day
                ,acc_nos.acc_no
                ,(
                    SELECT balance
                    FROM cte_balances bal
                    WHERE bal.sys_date <= cal.balance_day
                        AND acc_nos.acc_no = bal.acc_no
                    ORDER BY bal.sys_date DESC FETCH first 1 row ONLY
                    ) eff_bal
            FROM cte_calendar cal
            CROSS JOIN cte_accounts acc_nos
            ) t1
        WHERE balance_day = (
                SELECT to_date(date_to, 'YYYY-MM-DD')
                FROM cte_period
                )
            AND t.acc_no = t1.acc_no
        ) closing_bal
    ,round(avg(eff_bal), 2) avg_bal
FROM (
    SELECT cal.balance_day
        ,acc_nos.acc_no
        ,(
            SELECT balance
            FROM cte_balances bal
            WHERE bal.sys_date <= cal.balance_day
                AND acc_nos.acc_no = bal.acc_no
            ORDER BY bal.sys_date DESC FETCH first 1 row ONLY
            ) eff_bal
    FROM cte_calendar cal
    CROSS JOIN cte_accounts acc_nos
    ) t
GROUP BY acc_no
order by acc_no

Ожидаемый результат ACC_NO OPENING_BAL CLOSING_BAL AVG_BAL account1 5000 3500 3935.48 account2 3000 7500 5467.74

Ответы [ 2 ]

1 голос
/ 12 февраля 2020

Да. Вам не нужно выбирать из одной и той же таблицы много раз. Создайте календарь, как вы это сделали, объедините свои данные, разделенные по аккаунту, и используйте функции analyti c для вычислений:

select acc_no, round(avg(bal), 2) av_bal,
       max(bal) keep (dense_rank first order by day) op_bal, 
       max(bal) keep (dense_rank last order by day) cl_bal
  from (
    select acc_no, day, 
           nvl(balance, lag(balance) ignore nulls over (partition by acc_no order by day)) bal
      from (
        select date_from + level - 1 as day
          from (select date '2020-01-01' date_from, date '2020-01-31' date_to from dual)
          connect by date_from + level - 1 <= date_to)
      left join cte_balances partition by (acc_no) on day = sys_date)
  group by acc_no

dbfiddle

Редактировать:

иногда первый день месяца не имеет записи баланса, он должен принимать форму последнего доступного

Мы должны относиться к первому ряду особым образом. Это делается в подзапросе data, где в случае первой строки и нулевого баланса я запускаю коррелированный подзапрос, который ищет баланс с максимальной предыдущей даты.

with 
  cte_calendar as (
    select level lvl, date_from + level - 1 as day
      from (select date '2020-01-01' date_from, date '2020-01-31' date_to from dual)
      connect by date_from + level - 1 <= date_to),
  data as (
    select lvl, day, acc_no, 
           case when balance is null and lvl = 1 
                then (select max(balance) keep (dense_rank last order by sys_date) 
                        from cte_balances a
                        where a.acc_no = b.acc_no and a.sys_date <= day) 
                else balance
           end bal
      from cte_calendar
      left join cte_balances b partition by (acc_no) on day = sys_date)
select acc_no, 
       max(bal) keep (dense_rank first order by day) op_bal, 
       max(bal) keep (dense_rank last order by day) cl_bal, 
       round(avg(bal), 2)
  from (
    select acc_no, day, 
           nvl(bal, lag(bal) ignore nulls over (partition by acc_no order by day)) bal
      from data)
  group by acc_no

dbfiddle

хотя я этого еще не понимаю

Есть вещи, которые здесь не очевидны, и вы должны знать, чтобы понять запрос:

  • разделенное внешнее соединение. Это основная часть решения, которая производит полный период для каждой учетной записи. Вы можете прочитать о них здесь , например,
  • lag() ignore nulls - заполняет нулевые значения баланса, берут их из предыдущего, не нулевого,
  • max(bal) keep (dense_rank first order by day) принимает значение баланса из первая дата для открытия баланса. last - из последней строки для закрытия баланса.
0 голосов
/ 12 февраля 2020

Если вы можете позволить себе использовать функции first_value, last_value analyti c, то это, исходя из моего понимания вашего описания, может помочь:

with data as (
 select           'account1' as acc, 5000 as balance, to_date('2020-01-01', 'YYYY-MM-DD') as d from dual
 union all select 'account1' as acc, 6000 as balance, to_date('2020-01-05', 'YYYY-MM-DD') as d from dual
 union all select 'account2' as acc, 3000 as balance, to_date('2020-01-01', 'YYYY-MM-DD') as d from dual
 union all select 'account1' as acc, 3500 as balance, to_date('2020-01-08', 'YYYY-MM-DD') as d from dual
 union all select 'account1' as acc, 7500 as balance, to_date('2020-01-15', 'YYYY-MM-DD') as d from dual
)

select acc, avg(balance) over (partition by acc order by balance) as average, 
        first_value(balance) over(partition by acc order by balance asc rows unbounded preceding) as first, 
        last_value(balance) over(partition by acc order by balance asc rows unbounded preceding) as last
from data
where d between to_date('2020-01-01', 'YYYY-MM-DD') and to_date('2020-01-06', 'YYYY-MM-DD')
order by acc
ACC      | AVERAGE | FIRST | LAST
:------- | ------: | ----: | ---:
account1 |    5000 |  5000 | 5000
account1 |    5500 |  5000 | 6000
account2 |    3000 |  3000 | 3000

дБ <> скрипка здесь

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...