我能够找到一个给出一系列开始和结束日期的脚本,它将根据日期范围创建新行。我遇到的问题是,对于每条记录,我都有 AMOUNT 字段,我需要在整个日期范围内正确按比例分配。
CREATE TABLE #TempData (Company VARCHAR(6), InvoiceDate DATE, StartPeriod DATE, EndPeriod DATE, SchoolDistrict VARCHAR(100), Amount NUMERIC(10,2))
INSERT INTO #TempData (Company,InvoiceDate,StartPeriod,EndPeriod,SchoolDistrict,Amount)
SELECT '000123','1/1/2016','12/1/2015','12/31/2015','School District 123',140 UNION ALL
SELECT '000123','12/1/2016','6/15/2015','11/30/2015','School District 123',500
;WITH Recurse AS (
SELECT Company,InvoiceDate, StartPeriod
,CAST(DATEADD(DAY,-1,DATEADD(MONTH,DATEDIFF(MONTH,0,StartPeriod)+1,0)) AS DATE) EOM,EndPeriod
,SchoolDistrict,Amount
FROM #TempData
UNION ALL
SELECT Company,InvoiceDate
,CAST(DATEADD(MONTH,DATEDIFF(MONTH,0,StartPeriod)+1,0) AS DATE) StartPeriod
,CAST(DATEADD(DAY,-1,DATEADD(MONTH,DATEDIFF(MONTH,0,StartPeriod)+2,0)) AS DATE)
,EndPeriod
,SchoolDistrict,Amount
FROM Recurse
WHERE EOM<EndPeriod
)
SELECT Company,InvoiceDate,StartPeriod
,CASE WHEN EndPeriod<EOM THEN EndPeriod ELSE EOM END EndPeriod
,SchoolDistrict,Amount
FROM Recurse
DROP TABLE TempData
我的输出如下:
Company InvoiceDate StartPeriod EndPeriod SchoolDistrict Amount
000123 2016-01-01 2015-12-01 2015-12-31 School District 123 140.00
000123 2016-12-01 2015-06-15 2015-06-30 School District 123 500.00
000123 2016-12-01 2015-07-01 2015-07-31 School District 123 500.00
000123 2016-12-01 2015-08-01 2015-08-31 School District 123 500.00
000123 2016-12-01 2015-09-01 2015-09-30 School District 123 500.00
000123 2016-12-01 2015-10-01 2015-10-31 School District 123 500.00
000123 2016-12-01 2015-11-01 2015-11-30 School District 123 500.00
至于第一次记录返回,不需要做任何按比例分配,因为它只有1个月,但其他记录,我需要帮助我如何正确按比例分配 AMOUNT 500 正确地返回了6条记录。
注意更新:在完整月份是平均分配,然后任何不是完整期间的StartPeriod和/或EndPeriod月份,将获得部分按比例分配。
答案 0 :(得分:2)
这是从原始输入日期和金额派生的表达链。你可以很容易地将它提供给Recurse
方法,尽管我建议使用其他一种方法来生成月份,例如使用数字表,特别是如果日期可以超过多年。
对于部分月份,它会根据当月所涵盖的天数计算分数。除数是该月的总天数。有时会计师将一个月视为有30天,因此您必须确定这是否合适。
全部金额在整个月内分配,无论长度如何均等加权,加上两个部分按各自月份的个别比例加权。首先计算全月金额,并将结果四舍五入;部分月份取决于该计算,并在最后注意我关于四舍五入的后果的评论。最后的结果需要注意正确分配最后一分钱,以便总和正确。
with Expr1 as (
select *,
StartPeriod as RangeStart, EndPeriod as RangeEnd,
case when datediff(month, StartPeriod, EndPeriod) < 1 then null else
datediff(month, StartPeriod, EndPeriod) + 1
- case when datepart(day, StartPeriod) <> 1
then 1 else 0 end
- case when month(EndPeriod) = month(dateadd(day, 1, EndPeriod))
then 1 else 0 end
end as WholeMonths,
case when datepart(day, StartPeriod) <> 1
then 1 else 0 end as IsPartialStart,
case when month(EndPeriod) = month(dateadd(day, 1, EndPeriod))
then 1 else 0 end as IsPartialEnd,
datepart(day, StartPeriod) as StartPartialComplement,
datepart(day, EndPeriod) as EndPartialOffset,
datepart(day,
dateadd(day, -1, dateadd(month, datediff(month, 0, StartPeriod) + 1, 0))
) as StartPartialDaysInMonth,
datepart(day,
dateadd(day, -1, dateadd(month, datediff(month, 0, EndPeriod) + 1, 0))
) as EndPartialDaysInMonth
from #TempData
),
Expr2 as (
select *,
case when IsPartialStart = 1
then StartPartialDaysInMonth - StartPartialComplement + 1
else 0 end as StartPartialDays,
case when IsPartialEnd = 1
then EndPartialOffset else 0 end as EndPartialDays
from Expr1
),
Expr3 as (
select *,
cast(round(Amount / (
WholeMonths
+ StartPartialDays / cast(StartPartialDaysInMonth as float)
+ EndPartialDays / cast(EndPartialDaysInMonth as float)
), 2) as numeric(10, 2)) as WholeMonthAllocation,
StartPartialDays / cast(StartPartialDaysInMonth as float) as StartPartialFraction,
EndPartialDays / cast(EndPartialDaysInMonth as float) as EndPartialFraction
from Expr2
),
Expr4 as (
select *,
cast(case when IsPartialEnd = 0
then Amount - WholeMonthAllocation * WholeMonths
else StartPartialFraction * WholeMonthAllocation
end as numeric(10, 2)) as StartPartialAmount,
cast(case when IsPartialEnd = 0 then 0
else Amount
- WholeMonthAllocation * WholeMonths
- StartPartialFraction * WholeMonthAllocation
end as numeric(10, 2)) as EndPartialAmount
from Expr3
),
...
从这些值中,您可以确定在创建了所有额外行后最终结果中应该包含哪个金额。这个表达式将通过合并您的原始查询来实现。 (由于SQL Fiddle已经失效,我无法测试任何一个:)
... /* all of the above */
Recurse AS (
SELECT
RangeStart, RangeEnd, IsPartialStart, IsPartialEnd,
StartPartialAmount, EndPartialAmount, WholeMonthAllocation,
Company, InvoiceDate, StartPeriod,
CAST(DATEADD(DAY,-1,DATEADD(MONTH,DATEDIFF(MONTH,0,StartPeriod)+1,0)) AS DATE) EOM,
EndPeriod, SchoolDistrict,
case
when datediff(month, RangeStart, RangeEnd) = 0 then Amount
when IsPartialStart = 1 then StartPartialAmount
else WholeMonthAllocation
end as Amount
FROM Expr4
UNION ALL
SELECT
RangeStart, RangeEnd, IsPartialStart, IsPartialEnd,
StartPartialAmount, EndPartialAmount, WholeMonthAllocation,
Company, InvoiceDate,
CAST(DATEADD(MONTH,DATEDIFF(MONTH,0,StartPeriod)+1,0) AS DATE) AS StartPeriod,
CAST(DATEADD(DAY,-1,DATEADD(MONTH,DATEDIFF(MONTH,0,StartPeriod)+2,0)) AS DATE) EOM,
EndPeriod, SchoolDistrict,
case
-- final month is when StartPeriod is one month before RangeEnd.
-- remember this is recursive!
when IsPartialEnd = 1 and datediff(month, StartPeriod, RangeEnd) = 1
then EndPartialAmount
else WholeMonthAllocation
end as Amount
FROM Recurse
WHERE EOM < EndPeriod
)
SELECT
Company, InvoiceDate, StartPeriod,
CASE WHEN EndPeriod < EOM THEN EndPeriod ELSE EOM END EndPeriod,
SchoolDistrict, Amount
FROM Recurse
我已添加/别名RangeStart
和RangeEnd
值,以避免与您在临时表中使用的StartPeriod
和EndPeriod
混淆和输出查询。 范围 - 值表示完整范围的开始和结束,期间 - 值是分解各个期间的计算值。根据需要进行调整。
编辑#1:我意识到我没有处理同一个月开始和结束的情况:或许有一个更干净的方式来完成这一切。我刚刚将WholeMonths
表达式归零以避免可能除以零。最后的case
表达式捕获此条件并返回原始Amount
值。虽然您可能不必担心处理开始和结束日期会发生逆转,但我继续将它们与相同的< 1
测试结合在一起。
编辑#2:一旦我有一个地方尝试这个,你的测试用例显示四舍五入失去了一分钱,并且在最后的部分月份计算中被提升,即使它实际上是整整几个月中的一个。所以我不得不调整以寻找没有最后部分月的情况。这是Expr4
中的内容。我还发现了你注意到的一些次要语法错误。
递归查询允许按顺序查看月份并简化逻辑。锚点始终是开始月份,因此最终月份逻辑都不适用,对于查询的另一半也是如此。如果您最终通过针对数字表的常规联接来解决此问题,则您希望使用这样的表达式:
case
when datediff(month, RangeStart, RangeEnd) = 0
then Amount
when IsPartialStart = 1 and is first month...
then StartPartialAmount
when IsPartialEnd = 1 and is final month...
then EndPartialAmount
else WholeMonthAllocation
end as Amount
编辑#3 :还要注意,在处理非常小的数量时,此方法不适用,其中舍入将使结果偏斜。例子:
$ 0.13分为1月02日至12月01日给出[.01,.01,.01,.01,.01,.01,.01,.01,.01,.01,.01,.02] 1月02日至12月01日的$ 0.08分为[.01,.01,.01,.01,.01,.01,.01,.01,.01,.01,.01,-.03] $ 0.08除以1月31日至12月31日给出[-.03,.01,.01,.01,.01,.01,.01,.01,.01,.01,.01,.01] 1月31日至11月30日的0.05美元分为[.05,.00,.00,.00,.00,.00,.00,.00,.00,.00,.00] 1月31日至12月01日的0.05美元分为[.00,.00,.00,.00,.00,.00,.00,.00,.00,.00,.00,.05] 1月02日至3月1日划分的0.30美元给出[.15,.15,.00]
答案 1 :(得分:1)
这是一个有趣的问题,因为它既需要扩展行数,又需要在查询中检测和纠正的舍入问题。
首先需要一些精确的日期计算来确定每个月中有多少天属于StartPeriod和EndPeriod。
然后,将每个月的估算值计算为一个简单的比例,但舍入误差将意味着这些估算值的总和不等于总发票金额。然后使用窗口函数计算总舍入误差,以便可以调整最后一次付款。
另外,我建议使用带有简单“数字”视图的连接,而不是使用递归CTE为每个月生成一行。有关详细信息,请参阅此question about number tables
-- I use the #tempdata table mentioned in the question
; WITH numbers AS ( -- A fast way to get a sequence of integers starting at 0
SELECT TOP(10000) ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) - 1 as n
FROM sys.all_columns a CROSS JOIN sys.all_columns b
),
data_with_pk AS ( -- Add a primary key so that we know how to sort output
SELECT ROW_NUMBER() OVER (ORDER BY company, invoicedate) AS InvoiceId, *
FROM #tempdata
),
step1 AS ( -- Calc first and last day of each month in which payment is due
SELECT data_with_pk.*,
CAST(DATEADD(MONTH, DATEDIFF(MONTH, 0, StartPeriod) + numbers.n, 0)
AS DATE) AS StartOfMonth,
CAST(DATEADD(DAY, -1,
DATEADD(MONTH, DATEDIFF(MONTH,0,StartPeriod) + numbers.n + 1, 0))
AS DATE) AS EndOfMonth
FROM data_with_pk
-- This join is a simpler way to generate multiple rows than using a recursive CTE
JOIN numbers ON numbers.n <= DATEDIFF(MONTH, StartPeriod, EndPeriod)
),
step2 AS ( -- Calc block of days in each month which fall within whole period
SELECT *,
CASE WHEN StartPeriod > StartOfMonth THEN StartPeriod ELSE StartOfMonth END
AS StartOfBlock,
CASE WHEN EndPeriod < EndOfMonth THEN EndPeriod ELSE EndOfMonth END
AS EndOfBlock
FROM step1
),
step3 AS ( -- Whole months count as 30 days for purposes of calculated proportions
SELECT *,
CASE WHEN StartOfBlock = StartOfMonth AND EndOfBlock = EndOfMonth
THEN 30
ELSE DATEDIFF(DAY, StartOfBlock, EndOfBlock) + 1 END AS DaysInBlock
FROM step2
),
step3b AS (
SELECT *,
SUM(DaysInBlock) OVER (PARTITION BY InvoiceId) AS DaysInPeriod
FROM step3
),
step4 AS ( -- Calc proportion of whole amount due in this block
SELECT *,
CAST(Amount * DaysInBlock / DaysInPeriod AS NUMERIC(10,2)) AS Estimate
FROM step3b
),
step5 AS ( -- Calc running total of estimates
SELECT *,
SUM(Estimate) OVER (PARTITION BY InvoiceId ORDER BY EndOfBlock) AS RunningEstimate
FROM step4
),
step6 AS ( -- Adjust last estimate to ensure final Prorata total is equal to Amount
SELECT *,
CASE WHEN EndOfBlock = EndPeriod
THEN Estimate + amount - RunningEstimate
ELSE Estimate end AS Prorata
FROM step5
),
step7 AS ( -- Just for illustration to prove that payments sum to the Invoice Amount
SELECT *,
SUM(Prorata) OVER (PARTITION BY InvoiceId ORDER BY EndOfBlock) AS RunningProrata
FROM step6
)
SELECT InvoiceId, InvoiceDate, StartPeriod, EndPeriod, Amount, DaysInBlock, EndOfBlock,
Estimate, RunningEstimate, Prorata, RunningProrata
FROM step7
ORDER BY InvoiceId, EndOfBlock
您可以在下面的结果集中看到“Estimate”和“RunningEstimate”列最终为$ 0.01 out,但在“Prorata”列中进行了更正。
+-----------+-------------+-------------+------------+--------+-------------+------------+----------+-----------------+---------+----------------+
| InvoiceId | InvoiceDate | StartPeriod | EndPeriod | Amount | DaysInBlock | EndOfBlock | Estimate | RunningEstimate | Prorata | RunningProrata |
+-----------+-------------+-------------+------------+--------+-------------+------------+----------+-----------------+---------+----------------+
| 1 | 2016-01-01 | 2015-12-01 | 2015-12-31 | 140.00 | 30 | 2015-12-31 | 140.00 | 140.00 | 140.00 | 140.00 |
| 2 | 2016-12-01 | 2015-06-15 | 2015-11-30 | 500.00 | 16 | 2015-06-30 | 48.19 | 48.19 | 48.19 | 48.19 |
| 2 | 2016-12-01 | 2015-06-15 | 2015-11-30 | 500.00 | 30 | 2015-07-31 | 90.36 | 138.55 | 90.36 | 138.55 |
| 2 | 2016-12-01 | 2015-06-15 | 2015-11-30 | 500.00 | 30 | 2015-08-31 | 90.36 | 228.91 | 90.36 | 228.91 |
| 2 | 2016-12-01 | 2015-06-15 | 2015-11-30 | 500.00 | 30 | 2015-09-30 | 90.36 | 319.27 | 90.36 | 319.27 |
| 2 | 2016-12-01 | 2015-06-15 | 2015-11-30 | 500.00 | 30 | 2015-10-31 | 90.36 | 409.63 | 90.36 | 409.63 |
| 2 | 2016-12-01 | 2015-06-15 | 2015-11-30 | 500.00 | 30 | 2015-11-30 | 90.36 | 499.99 | 90.37 | 500.00 |
+-----------+-------------+-------------+------------+--------+-------------+------------+----------+-----------------+---------+----------------+