web-dev-qa-db-ja.com

自己参照条件を使用したローリング合計集計

再帰的なCTEのユースケースだと私が思うものはありますが、それをどのように構成するかはまだわかりません。要件は、償却スケジュールのローリング合計を作成することですが、支払いはスケジュール内の支払いに直接関連付けられていません。まず、スケジュールオブジェクトがあります。

CREATE TABLE sch AS
SELECT ctr_id::int , mth::date , pmt_amt::numeric
FROM (
    VALUES
        ( 1 , '2019-01-01' , 145.0 ) ,
        ( 1 , '2019-02-01' , 145.0 ) ,
        ( 1 , '2019-03-01' , 145.0 ) ,
        ( 1 , '2019-04-01' , 145.0 ) ,
        ( 1 , '2019-05-01' , 145.0 ) ,
        ( 1 , '2019-06-01' , 145.0 )
) AS sch ( ctr_id , mth , pmt_amt )  ;

このテーブルは、特定の契約(ctr_id)の償却スケジュールを追跡します。契約に対して行われた各支払いを追跡する別の支払いオブジェクトがあります。

CREATE TABLE pmt AS
SELECT ctr_id::int , dt::date , amt::numeric
FROM (
    VALUES
        ( 1 , '2019-01-04' , 145.0 ) ,
        ( 1 , '2019-02-01' , 145.0 ) ,
        ( 1 , '2019-03-01' , 145.0 ) ,
        ( 1 , '2019-03-29' , 145.0 ) ,
        ( 1 , '2019-05-03' , 145.0 ) ,
        ( 1 , '2019-06-07' , 145.0 )
) AS sch ( ctr_id , dt , amt )  ;

このデータを全体的に見ると、3月29日の支払いが4月の支払いに向けられていると特定するのはかなり簡単です。ただし、このシナリオと次のシナリオの両方を説明するロジックの実装に問題があります。

SELECT ctr_id::int , dt::date , amt::numeric
FROM (
    VALUES
        ( 1 , '2019-01-01' , 145.0 ) ,
        ( 1 , '2019-05-01' , 435.0 ) ,
        ( 1 , '2019-05-03' , 145.0 ) ,
        ( 1 , '2019-06-01' , 145.0 )
) AS sch ( ctr_id , dt , amt )  ;

この場合、5月の最初の支払いが追いつき支払いであり、2番目の支払いが5月自体に対するものであることは明らかです。私が始めた分析SQL:

SELECT
    sch.mth ,
    sch.ctr_id ,
    SUM( sch.pmt_amt - pmt.pmts ) OVER ( PARTITION BY sch.ctr_id ) AS outstanding
FROM
    sch
LEFT JOIN
(
    SELECT
        ctr_id ,
        DATE_TRUNC( 'month' , dt ) AS mth ,
        SUM( amt ) OVER ( PARTITION BY ctr_id ) AS pmts ,
        COUNT( * ) AS pmt_cnt
    FROM
        pmt
    GROUP BY
        ctr_id , DATE_TRUNC( 'month' , dt )
)
    AS pmt
        ON  pmt.ctr_id = sch.ctr_id
        AND pmt.mth = sch.mth

最初のシナリオに対処するには、前月に複数の支払いがあり、2つの支払いの合計が予定額を超えている場合、超過分を翌月に適用します。 2番目のシナリオでは、前の期間の残高も考慮する必要があります。疑似SQLの場合:

SELECT
    sch.mth ,
    sch.ctr_id ,
    CASE
        WHEN LAG(pmt.pmt_cnt) OVER (ctr) > 1 AND pmt.pmts > sch.pmt_amt
        THEN pmt.pmts - sch.pmt_amt
        ELSE 0
    END AS carryover ,
    SUM(
        CASE
            WHEN LAG(outstanding) OVER (ctr) > 0
                THEN ( LAG(outstanding) OVER (ctr) + sch.pmt_amt ) - pmt.pmts
            ELSE sch.pmt_amt - pmt.pmts - LAG(carryover) OVER (ctr)
    ) OVER ( ctr ) AS outstanding
FROM
    sch
LEFT JOIN
(
    SELECT
        ctr_id ,
        DATE_TRUNC( 'month' , dt ) AS mth ,
        SUM( amt ) OVER ( PARTITION BY ctr_id ) AS pmts ,
        COUNT( * ) AS pmt_cnt
    FROM
        pmt
    GROUP BY
        ctr_id , DATE_TRUNC( 'month' , dt )
)
    AS pmt
        ON  pmt.ctr_id = sch.ctr_id
        AND pmt.mth = sch.mth
WINDOW
    ctr AS (
        PARTITION BY sch.ctr_id
        ORDER BY sch.mth
    )

ウィンドウ関数メソッドの主な問題は、同じウィンドウ関数の以前の値を参照し直す必要があるが、その戻り値に応じて現在の値を異なる方法で計算することです。これを解決する方法はありますか?再帰的なCTEはそれを解決できますか?

編集:逃したユースケースを指摘してくれたLennartに感謝します。また、元本に直接適用される超過支払いについても説明できる必要があります。たとえば、毎月5ドルの追加料金を支払うか、1000ドルずつ支払う。どちらの場合も、予定された支払い額を変更するにはローンを再償却する必要があるため、同じ$ 145が翌月にも支払われます。この要件がある場合、すべての支払いを合計して、それまでに予定されている支払いと比較することはできません。

3
TH58PZ700U

カスタム集計関数を調べた後、それが問題を解決する適切な方法であることに気付きました。他の誰かがこの質問に遭遇し、私がそれをどのように解決したかを知りたい場合:

CREATE OR REPLACE FUNCTION sfunc_outstanding_amount_with_carry
(
    previous_row    NUMERIC[]   , -- previous row output
    current_row     NUMERIC[]     -- current row input
)
RETURNS NUMERIC[]
AS $$
    DECLARE
        current_total       NUMERIC ;
        carried_amount      NUMERIC ;
        outstanding_amount  NUMERIC ;
        remainder_amount    NUMERIC ;
    BEGIN
        /*
            If the previous outstanding amount is zero, apply the least of the remainder of the
            previous row and the carryover towards the current value.
        */
        IF
            ( previous_row[1] = 0 )
        THEN
            carried_amount := LEAST( previous_row[2] , current_row[2] ) ;
        ELSE
            carried_amount := 0 ;
        END IF ;

        /*
            Calculate the current total and determine the remainder for the next row.
        */
        current_total := previous_row[1] + current_row[1] - carried_amount ;
        outstanding_amount := GREATEST( current_total , 0 ) ;
        remainder_amount := LEAST( current_total , 0 ) ;
        remainder_amount := CASE SIGN(remainder_amount) WHEN 0 THEN 0 ELSE ( remainder_amount / -1 ) END ;

        RETURN ARRAY[ outstanding_amount , remainder_amount ] ;
    END ;
$$ LANGUAGE PLPGSQL IMMUTABLE ;

CREATE OR REPLACE FUNCTION finalfunc_outstanding_amount_with_carry
(
    current_row NUMERIC[]
)
RETURNS NUMERIC
AS $$
    BEGIN
        RETURN current_row[1] ;
    END ;
$$ LANGUAGE PLPGSQL IMMUTABLE ;

CREATE AGGREGATE outstanding_amount_with_carry (NUMERIC[])
(
    SFUNC = sfunc_outstanding_amount_with_carry ,
    STYPE = NUMERIC[] ,
    FINALFUNC = finalfunc_outstanding_amount_with_carry ,
    INITCOND = '{0,0}'
) ;

使用法は次のとおりです。

SELECT
    sch.mth ,
    sch.ctr_id ,
    CASE
        WHEN LAG(pmt.pmt_cnt) OVER (ctr) > 1 AND pmt.pmts > sch.pmt_amt
        THEN pmt.pmts - sch.pmt_amt
        ELSE 0
    END AS carryover ,
    OUTSTANDING_AMOUNT_WITH_CARRY( ARRAY[
        sch.pmt_amt - pmt.pmts ,
        CASE
            WHEN LAG(pmt.pmt_cnt) OVER (ctr) > 1 AND pmt.pmts > sch.pmt_amt
            THEN pmt.pmts - sch.pmt_amt
            ELSE 0
        END 
    ] ) OVER ( ctr ) AS outstanding
FROM
    sch
LEFT JOIN
(
    SELECT
        ctr_id ,
        DATE_TRUNC( 'month' , dt ) AS mth ,
        SUM( amt ) OVER ( PARTITION BY ctr_id ) AS pmts ,
        COUNT( * ) AS pmt_cnt
    FROM
        pmt
    GROUP BY
        ctr_id , DATE_TRUNC( 'month' , dt )
)
    AS pmt
        ON  pmt.ctr_id = sch.ctr_id
        AND pmt.mth = sch.mth
WINDOW
    ctr AS (
        PARTITION BY sch.ctr_id
        ORDER BY sch.mth
    )

https://hashrocket.com/blog/posts/custom-aggregates-in-postgresql での説明は、このソリューションを組み合わせるときに非常に役立ちました。

1
TH58PZ700U

これは私のコメントで私が言いたかったことです:

select ctr_id, mth, cumulative_pmt_amt
     , coalesce(cumulative_amt, 0) as cumulative_amt
     , coalesce(cumulative_amt, 0) - cumulative_pmt_amt as balance
from (     
    select ctr_id, mth
         , sum(pmt_amt) over (partition by ctr_id 
                              order by mth) as cumulative_pmt_amt
         , ( select sum(amt) 
             from pmt y 
             where x.ctr_id = y.ctr_id 
               and y.dt <= x.mth ) as cumulative_amt 
    from sch x
) as t;

負の残高の場合、ケースを使用してそれを0にマッピングできます。

フィドル

優れた最初の投稿BTW

0
Lennart