再帰的な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が翌月にも支払われます。この要件がある場合、すべての支払いを合計して、それまでに予定されている支払いと比較することはできません。
カスタム集計関数を調べた後、それが問題を解決する適切な方法であることに気付きました。他の誰かがこの質問に遭遇し、私がそれをどのように解決したかを知りたい場合:
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 での説明は、このソリューションを組み合わせるときに非常に役立ちました。
これは私のコメントで私が言いたかったことです:
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