首页 > 解决方案 > SQL Server:借记卡条件交易

问题描述

我在一家为客户提供多种套餐的电信公司工作。其中一些套餐包括他们每个月可以使用的一定数量的免费长途通话时间。

目前,免费分钟套餐存在一个缺陷,即允许客户获得比套餐实际允许的更多的免费分钟。同时,当前的系统有时会阻止客户以正确的顺序用完他们所有的空闲时间。

我已经想出如何修改流程以确保将包裹正确分配给客户的通话记录。现在,我正在尝试改变我们处理这些记录的方式,以防止他们使用超过免费分钟包中的内容。

我正在尝试使用 CTE、窗口函数和更新语句的组合来解决这个问题。这个想法是,拥有免费通话时间套餐的客户可以像使用借记卡一样使用该套餐,这样如果通话时间少于他们剩余的分钟数,它会覆盖通话并从金额中减去剩余的分钟数可用的免费分钟数;但如果持续时间超过可用的免费分钟数,则不包括通话。我已经弄清楚了几乎所有内容,但是我被困住了,因为我无法弄清楚如何查找通过窗口函数在前一行中计算的值。

这是我使用测试表获得的结果示例(注意:持续时间以秒为单位,因此它转换为分钟):

CustID  Duration    FrMinsAvailable NewAvMins   MinutesBilled   PrevAvMins
---------------------------------------------------------------------------
14000   250000      4250.2          83.5        4166.7          NULL
14000   9000        4250.2          -66.5       150             83.5
14000   4800        4250.2          -146.5      80              -66.5
14000   450         4250.2          -154        7.5             -146.5
14000   335         4250.2          -159.6      5.6             -154
14000   200         4250.2          -162.9      3.3             -159.6
14000   65          4250.2          -164        1.1             -162.9
14000   45          4250.2          -164.7      0.8             -164
14000   32          4250.2          -165.3      0.5             -164.7
14000   25          4250.2          -165.7      0.4             -165.3
14000   21          4250.2          -166        0.4             -165.7
14000   5           4250.2          -166.1      0.1             -166

这是我想要获得的结果:

CustID      Duration    FrMinsAvailable NewAvMins   MinutesBilled   PrevAvMins
-------------------------------------------------------------------------------
14000       250000      4250.2          83.5        4166.7          NULL
14000       9000        4250.2          83.5        150             83.5
14000       4800        4250.2          3.5         80              83.5
14000       450         4250.2          3.5         7.5             3.5
14000       335         4250.2          3.5         5.6             3.5
14000       200         4250.2          0.2         3.3             3.5
14000       65          4250.2          0.2         1.1             0.2
14000       45          4250.2          0.2         0.8             0.2
14000       32          4250.2          0.2         0.5             0.2
14000       25          4250.2          0.2         0.4             0.2
14000       21          4250.2          0.2         0.4             0.2
14000       5           4250.2          0.1         0.1             0.2

最后,这是我使用的测试代码:

DECLARE @testDuration TABLE (CustID INT, Duration INT)

INSERT INTO @testDuration(CustID, Duration)
VALUES (14005, 65), (14005, 200), (14005, 4800), (14005, 25),
       (14005, 5), (14005, 450), (14005, 21), (14005, 32),
       (14005, 335), (14005, 45), (14005, 9000), (14005, 250000);

WITH my_cte AS 
(
    SELECT
        d.CustID,
        d.Duration,
        fm.FrMinsAvailable,
        ROUND((fm.FrMinsAvailable-SUM(CAST(d.Duration AS FLOAT) / 60)
                OVER (PARTITION BY d.CustID ORDER BY d.Duration DESC)), 1) NewAvMins,
        ROUND((CAST(d.Duration AS FLOAT) / 60), 1) BillMins
    FROM 
        (SELECT '14000' CustID, '4250.2' FrMinsAvailable) fm
    INNER JOIN 
        @testDuration   ON fm.CustID = d.SerialNoID
    GROUP BY 
        d.CustID, d.Duration, fm.FrMinsAvailable
)
SELECT 
    my_cte.*,
    (LAG(my_cte.NewAvMins) OVER (PARTITION BY my_cte.CustID ORDER BY my_cte.Duration DESC)) PrevAvMins
FROM 
    my_cte

我最终打算做的是使用这些结果来设置一个值,允许客户在 MinutesBilled <= PrevAvMins 的情况下获得免费分钟数。如果您问我为什么不只是创建一堆表来完成此任务,那实际上是我试图通过最小化读取和写入来减少运行此进程的服务器上的负载,因为处理所有数据的整个过程每天收到的记录已经涉及很多记录,需要几个小时才能完成,并且服务器还有其他程序要运行。

该解决方案不必涉及窗口函数和 CTE,但这是我想出的最佳解决方案。我真的很感激一些好的反馈!:)

谢谢!

PS 我正在使用 SQL Server 2014。

标签: sql-servertsqlcommon-table-expressionwindow-functions

解决方案


您需要使用SUM OVER()来获得累积总数,请参阅下面的代码工作:

DECLARE @testDuration TABLE (CustID INT, Duration decimal (18,0))
INSERT INTO @testDuration(CustID, Duration)
    VALUES  
        (14005, 65)
        , (14005, 200)
        , (14005, 4800)
        , (14005, 25)
        , (14005, 5)
        , (14005, 450)
        , (14005, 21)
        , (14005, 32)
        , (14005, 335)
        , (14005, 45)
        , (14005, 9000)
        , (14005, 250000);

        if object_id('tempdb..#callRecords') is not null
            drop table #callRecords;

        select td.CustID, Duration,4250.2 as FrAvailableMinutes
        into #callRecords
        from @testDuration as td;

        with cte as (

        select cr.CustID
             , cr.Duration
             , cr.FrAvailableMinutes 
             , row_number() over (partition by cr.CustID order by duration asc) as CallDateKey
             from #callRecords as cr
             )

             select cte.CustID
                  , cte.Duration
                  , cte.FrAvailableMinutes
                  , cte.CallDateKey
                  , round(sum(Duration) over (order by CallDateKey rows between unbounded preceding and current row)/60,2) as CumulativeDUration
                  , cte.FrAvailableMinutes - round(sum(Duration) over (order by CallDateKey rows between unbounded preceding and current row)/60,2) as MinutesLeft

            from cte
            order by CallDateKey desc

编辑:根据您在下面的评论,我还为您的业务需求提供了一个循环解决方案:

declare @testDuration table
    (
        CustID int
      , Duration decimal(18, 0)
      , CallDateKey int
    );
insert into @testDuration
     (
         CustID
       , Duration
       , CallDateKey
     )
values
    (14005, 65, 1)
  , (14005, 200, 2)
  , (14005, 4800, 3)
  , (14005, 25, 4)
  , (14005, 5, 5)
  , (14005, 450, 6)
  , (14005, 21, 7)
  , (14005, 32, 8)
  , (14005, 335, 9)
  , (14005, 45, 10)
  , (14005, 9000, 11)
  , (14005, 250000, 12)
  , (14005, 500, 13);

if object_id('tempdb..#Billing') is not null
    drop table #Billing;

create table #Billing
    (
        CustId int
      , Duration decimal(18, 0)
      , FrAvailableSeconds decimal(18, 2)
      , CallDateKey int
      , CumulativeDuration decimal(18, 2)
      , SecondsLeft decimal(18, 2)
    );

with CallRecords
    (CustID, DUration, CallDateKey, FrAvailableSeconds)
as (
       select td.CustID
            , Duration
            , td.CallDateKey
            , 4250.20 * 60
       from @testDuration as td
   )
insert into #Billing
     (
         CustId
       , Duration
       , FrAvailableSeconds
       , CallDateKey
       , CumulativeDuration
       , SecondsLeft
     )
select cr.CustID
     , cr.DUration
     , cr.FrAvailableSeconds
     , cr.CallDateKey
     , round(   sum(DUration) over (order by
                                        CallDateKey
                                    rows between unbounded preceding and current row
                                   ) / 60
              , 2
            )
     , cr.FrAvailableSeconds
from CallRecords as cr;


declare @a int = (
                     select min(CustId) from #Billing as b
                 );
declare @b int = (
                     select max(CustId) from #Billing as b
                 );

declare @x int;
declare @y int;

while @a <= @b
begin

    set @x = (
                 select min(b.CallDateKey) from #Billing as b where b.CustId = @a
             );
    set @y = (
                 select max(b.CallDateKey) from #Billing as b where b.CustId = @a
             );

    while @x <= @y
    begin


        update b
        set b.SecondsLeft = case
                                when isnull(ps.SecondsLeft, b.FrAvailableSeconds) - b.Duration < 0 then
                                    isnull(ps.SecondsLeft, b.FrAvailableSeconds)
                                else
                                    isnull(ps.SecondsLeft, b.FrAvailableSeconds) - b.Duration
                            end
        from #Billing as b
        left join (
                      select b.CustId
                           , SecondsLeft
                      from #Billing as b
                      where
                          b.CallDateKey = @x - 1
                          and b.CustId = @a
                  ) as ps
            on b.CustId = ps.CustId
        where
            b.CustId = @a
            and b.CallDateKey = @x;

        set @x += 1;

    end;

    set @a += 1;



end;

select b.CustId
     , b.Duration
     , b.FrAvailableSeconds
     , b.FrAvailableSeconds / 60 as FrAvailableMinutes
     , b.CallDateKey
     , b.CumulativeDuration
     , b.SecondsLeft
     , b.SecondsLeft / 60 as MinutesLeft
from #Billing as b
order by
    b.CallDateKey desc;

推荐阅读