2个相互依赖的表

时间:2016-11-18 11:54:12

标签: sql sql-server sql-server-2016

我有一个数据库,其中有几个表跟踪电话呼叫/短信/数据和津贴,如果可以在不诉诸游标的情况下分配调用,我正试图解决,但我无法弄清楚一种构造SQL的方法。我的尝试中没有任何有用的SQL,因为我似乎无法理解如何处理它!问题是,对我而言,这似乎是一个固有的迭代过程,如果有一种合理的方法将其转化为基于集合的方法,我就无法解决。我考虑过使用窗口函数,但是当我们跟踪2个表中的累计总数并且总数是相互依赖时,我看不出怎么做。我正在尝试最小化运行此过程的时间以及对其他查询的影响,因为我们要经常重新运行它并且表格变得非常大。

这是一个简化的结构......

呼叫

记录所有电话

  • ID
  • ContractID
  • ChargeGroupID
  • 日期时间
  • Quantity int
  • QuantityFromAllowances int(这是我想要填充的内容)
  • FirstAllowanceUsedID(FK to Allowance)(这就是我要填充的内容)

津贴

每份合约有哪些不同的免税额

  • ID
  • ContractID
  • 优先级(如果首先使用,则为1,否则为0)
  • Quantity int
  • QuantityUsed int(最初设置为0 - 可用于跟踪我们使用的数量)

AllowanceChargeGroup

如何使用配额 - 这是一个列出允许组合的联结表

  • ID
  • AllowanceID
  • ChargeGroupID

我故意没有记录所有细节以保持简单。我希望一切都很明显,但如果没有,请告诉我。

如果我正在迭代地处理这个问题,那么我的psueodocode就像: -

For each Call ordered by DateTime
    Declare a as Allowance
    Do

        Set a = First Allowance Where Allowance.ContractID=Call.ContractID And Allowance.QuantityUsed<Allowance.Quantity Order by Priority Descending
        If a != NULL
            Declare n as Integer
            Set n = a.Quantity-a.QuantityUsed
            If Call.Quantity-Call.QuantityFromAllowances<n
                Set n = Call.Quantity-Call.QuantityFromAllowances
            End if
            Set Call.QuantityFromAllowances = Call.QuantityFromAllowances + n
            If Call.FirstAllowanceUsedID == NULL Then 
                Set Call.FirstAllowanceUsedID = a.ID
            End if
            Set a.QuantityUsed = a.QuantityUsed + n

        End if

    Loop while a != NULL AND Call.QuantityFromAllowances<Call.Quantity

Next Call

随意告诉我,我正在接近问题,或者这实际上是游标的一个很好的候选者。我只是在寻找最好的解决方案。

举个例子: -

Call
ID   ContractID   ChargeGroupID   DateTime   Quantity   QuantityFromAllowances   FirstAllowanceUsedID 
1    1            1               2016-11-01 100        0                        NULL
2    1            2               2016-11-02 500        0                        NULL
3    1            1               2016-11-03 500        0                        NULL
4    1            3               2016-11-04 100        0                        NULL
5    1            1               2016-11-05 100        0                        NULL
6    2            1               2016-11-01 100        0                        NULL

Allowance
ID   ContractID   Priority Quantity   QuantityUsed
1    1            1        500        0
2    1            0        500        0
3    2            1        500        0
4    2            0        500        0

AllowanceChargeGroup
ID   AllowanceID   ChargeGroupID
1    1             1
2    1             2
3    2             1
4    2             2
5    3             1

在我的例子中,我会按如下方式计算: -

  1. 呼叫ID 1与Allowance ID 1匹配(通过AllowanceChargeGroup中的联结表) - QuantityFromAllowances = 100,FirstAllowanceUsedID = 1,Allowance.QuantityUsed = 100(0 + 100)
  2. 电话ID 2与津贴ID 1相匹配,但只有400还有剩余津贴,因此QuantityFromAllowances = 400,FirstAllowanceUsedID = 1,Allowance.QuantityUsed = 500(100 + 400)
  3. 呼叫ID 2匹配津贴ID 2(1号没有留下) - QuantityFromAllowances = 500(400 + 100),FirstAllowanceUsedID = 1(已设置在上面因此未更改),Allowance.QuantityUsed = 100(0 + 100)
  4. 呼叫ID 3匹配Allowance ID 2(1号没有留下) - 但是仍有400留在余量,因此QuantityFromAllowances = 400,FirstAllowanceUsedID = 2,Allowance.QuantityUsed = 500(100 + 400)。
  5. 来电ID 4与任何津贴都不匹配,因此无需更改
  6. 来电ID 5与任何津贴都没有匹配(全部用完)所以没有变更
  7. 电话号码6匹配津贴ID 3 QuantityFromAllowances = 100,FirstAllowanceUsedID = 3,Allowance.QuantityUsed = 100(0 + 100)
  8. 之后,表格应如下所示(只有更改为Call.QuantityFromAllowances,Call.FirstAllowanceUsedID,Allowance.QuantityUsed ...

    Call
    ID   ContractID   ChargeGroupID   DateTime   Quantity   QuantityFromAllowances   FirstAllowanceUsedID 
    1    1            1               2016-11-01 100        100                        1
    2    1            2               2016-11-02 500        500                        1
    3    1            1               2016-11-03 500        400                        2
    4    1            3               2016-11-04 100        0                        NULL
    5    1            1               2016-11-05 100        0                        NULL
    6    2            1               2016-11-01 100        100                        3
    
    Allowance
    ID   ContractID   Priority Quantity   QuantityUsed
    1    1            1        500        500
    2    1            0        500        500
    3    2            1        500        100
    4    2            0        500        0
    
    AllowanceChargeGroup
    ID   AllowanceID   ChargeGroupID
    1    1             1
    2    1             2
    3    2             1
    4    2             2
    5    3             1
    

3 个答案:

答案 0 :(得分:2)

您想要同时更新呼叫表和限额表,每次更新都取决于之前的更新 只有一个sql语句是不可能的,所以你需要循环 您不需要游标,您可以在过程中使用顺序设置操作来解决它。

首先,一些声明并准备一些数据:

declare @todo as table (callID int primary key, qt int, done bit, unique (done, qt, callid))
declare @id1 int, @id2 int, @q1 int, @q2 int

-- prepare job list
insert into @todo
select id, Quantity-QuantityFromAllowances, 0
from [call]
where Quantity>QuantityFromAllowances

然后主循环调用:

set @id1=0
set @q1= null
while not(@id1 is null) begin
    set @id1=null
    select top 1 @id1 = callID, @q1=qt from @todo where done=0 and qt>0 order by callID

    if not(@id1 is null) begin

        set @id2 = null
        select top 1 @id2 = a.id, @q2 = a.Quantity - a.QuantityUsed
        from [call] c
        inner join AllowanceChargeGroup g on g.ChargeGroupID = c.ChargeGroupID
        inner join allowance a on (a.ID = g.AllowanceID) and (a.Quantity>a.QuantityUsed)
        where c.ID=@id1
        order by c.ID,[Priority] desc, (a.Quantity-a.QuantityUsed) desc

        if not(@id2 is null) begin

            if @q2 < @q1 set @q1 = @q2

            update a set QuantityUsed = QuantityUsed + @q1
            from allowance a            
            where a.ID=@id2 

            update c set QuantityFromAllowances = QuantityFromAllowances + @q1, FirstAllowanceUsedID = isnull(FirstAllowanceUsedID, @id2)
            from [call] c
            where c.ID=@id1

            update t set qt = qt-@q1, done = IIF(qt-@q1=0,1,0)
            from @todo t
            where t.callID=@id1

        end else begin

            -- unable to complete
            update t set done = 1 
            from @todo t
            where t.callID=@id1

        end
    end
end

最后输出:

select * from [call]
select * from allowance

与要求相同

答案 1 :(得分:1)

正如我在评论中所说,您可以采用不同的方法实现目标,无循环,但您需要不相交的AllowanceChargeGroup ,这意味着一个津贴可以是只在一个组中。

通过津贴和收费组之间的这种独特关系,我们可以将请求(呼叫)与津贴联系起来。

我们的想法是列出并加权(订购)调用所需的每个单元,并列出和重量(订购)每个单元可用的配额,最后将它们并排连接。

例如,假设有此Calls,Allowances和ChargeGroups:

ID  ChargeGroupID   Quantity    QuantityFromAllowances  FirstAllowanceUsedID
1   1               3           0                       NULL
2   1               3           0                       NULL
3   2               5           0                       NULL

ID  Priority    Quantity    QuantityUsed
1   1           4           0
2   0           1           0
3   0           6           0

ID  AllowanceID ChargeGroupID
1   1           1
2   2           1
4   3           2

现在根据行数量分解n行中的每一行(因此我们将有3行用于CallID 1和CallID 2以及5行用于CallID 3)。 在爆炸时,标记行以标识它们(添加两个不同的列,其中行号用于组和呼叫/允许)

ChargeGroupID   GroupRowN   CallID  CallRowN
1               1           1       1       
1               2           1       2       
1               3           1       3       
1               4           2       1       
1               5           2       2       
1               6           2       3       
2               1           3       1       
2               2           3       2       
2               3           3       3       
2               4           3       4       
2               5           3       5       

ChargeGroupID   GroupRowN   AllowanceID AllowanceRowN
1               1           1           1
1               2           1           2
1               3           1           3
1               4           1           4
1               5           2           1
2               1           3           1
2               2           3           2
2               3           3           3
2               4           3           4
2               5           3           5
2               6           3           6

现在只需将这些爆炸集合加入组行号(GroupRowN)即可 在这里,您可以看到电话津贴的分配情况。

  • CallID = 1(前3行)完全由前3行覆盖 AllowanceID = 1
  • CallID = 2(第3行)部分涵盖 AllowanceID = 1的最后一行和AllowanceID = 2的第一行(和唯一的)
  • CallID = 3(最后5行)完全由前5行覆盖 AllowanceID = 3,因为最后一行没有完全耗尽 无与伦比的(任何电话都没有要求)

(我添加了水平破折号以更好地显示CallIDs的分布):

ChargeGroupID   GroupRowN   CallID  CallRowN    ChargeGroupID   GroupRowN   AllowanceID AllowanceRowN
1               1           1       1           1               1           1           1
1               2           1       2           1               2           1           2
1               3           1       3           1               3           1           3
-----------------------------------------------------------------------------------------------------
1               4           2       1           1               4           1           4
1               5           2       2           1               5           2           1
1               6           2       3           NULL            NULL        NULL        NULL
-----------------------------------------------------------------------------------------------------
2               1           3       1           2               1           3           1
2               2           3       2           2               2           3           2
2               3           3       3           2               3           3           3
2               4           3       4           2               4           3           4
2               5           3       5           2               5           3           5
NULL            NULL        NULL    NULL        2               6           3           6

现在让我们汇总这个结果以得到一些总数:

CallID  Max(CallN)  AllowanceID Max(AllowanceN)
1       3           1           3
2       1           1           4
2       2           2           1
3       5           3           5

最后,从上一次输出我们可以得到更新调用的信息,一个限额表:

CallID  QtUsed  FirstUsed
1       3       1
2       2       1
3       5       3

AllowanceID QtUsed
1           4
2           1
3           5

确定,
这就是理论,现在让我们看一些代码(使用上面的数据)。

注意FN_NUMBERS(n),它是一个函数,只返回一个数字从1到n的列,你需要它在你的数据库中,有很多方法可以做到,只需google for&#34; tally表&#34;或look here 我使用以下内容:

CREATE FUNCTION FN_NUMBERS(
     @MAX INT
)
RETURNS @N TABLE (N INT NOT NULL PRIMARY KEY)  
BEGIN
     WITH
       Pass0 as (select '1' as C union all select '1'),       --2 rows
       Pass1 as (select '1' as C from Pass0 as A, Pass0 as B),--4 rows
       Pass2 as (select '1' as C from Pass1 as A, Pass1 as B),--16 rows
       Pass3 as (select '1' as C from Pass2 as A, Pass2 as B),--256 rows
       Pass4 as (select TOP (@MAX) '1' as C from Pass3 as A, Pass3 as B)    --65536 rows
       ,Tally as (select TOP (@MAX) '1' as C from Pass4 as A, Pass2 as B, Pass1 as C)  --4194304 rows
       --,Tally as (select TOP (@MAX) '1' as C from Pass4 as A, Pass3 as B)               --16777216 rows
       --,Tally as (select TOP (@MAX) '1' as C from Pass4 as A, Pass4 as B)               --4294836225 rows
     INSERT INTO @N
     SELECT TOP (@MAX) ROW_NUMBER() OVER(ORDER BY C) AS N
     FROM Tally
     RETURN
END

回到sql ..

declare @res as table (id int identity primary key, CallID int, CallN int , AllowanceID int, AllowanceN int, unique (callId, id), unique (allowanceID, id))

;with
cx as (
        select c.ID, c.Quantity, c.ChargeGroupID, n, ROW_NUMBER() over (partition by ChargeGroupID order by id,n) rn
        from [call] c
        join FN_NUMBERS(1000) n on n.N<=(c.Quantity-c.QuantityFromAllowances)
),
ax as (
        select a.ID, a.Quantity, ChargeGroupID, N, ROW_NUMBER() over (partition by g.ChargeGroupID order by [priority] desc, a.id,n) rn
        from Allowance a 
        join AllowanceChargeGroup g on g.AllowanceID = a.ID 
        join FN_NUMBERS(1000) n on n.N <= (a.Quantity-a.QuantityUsed)
),
j as (
select 
    cx.ID CallID, cx.Quantity CallQt, cx.N CallN, cx.rn CallRn, ax.ID AllowanceID, ax.Quantity AllowanceQt, ax.N AllowanceN, ax.rn AllowanceRn
from cx
join ax on cx.rn = ax.rn and (cx.ChargeGroupID = ax.ChargeGroupID)
)
insert into @res
select CallID, MAX(CallN) CallN, AllowanceID, MAX(AllowanceN) AllowanceN
from j
group by CallID,AllowanceID

这将在@res表中填入用于更新的最终聚合数据。

所以我们只需要执行实际的更新:

-- updates Allowance table
;with
ar as (
    select AllowanceID, MAX(AllowanceN) QtUsed
    from @res
    group by AllowanceID
)
update a set a.QuantityUsed = a.QuantityUsed + ar.QtUsed
select ar.*
from Allowance a
join ar on a.ID = ar.AllowanceID

-- updates Call table
;with
fu as (
    select CallID id, min(calln) FirstUsed
    from @res
    group by CallID 
),
cr as (
    select CallID, MAX(CallN) QtUsed, MIN(AllowanceID) FirstUsed
    from @res r1
    left join fu r2 on r1.CallID=r2.id and r1.CallN = r2.FirstUsed
    group by CallID
)
update c set QuantityFromAllowances = c.QuantityFromAllowances + QtUsed, FirstAllowanceUsedID = ISNULL(FirstAllowanceUsedID, FirstUsed)
select cr.*
from [call] c
join cr on c.ID = cr.CallID

全部,临时表中的一个插入和两个更新,没有循环,没有游标..

答案 2 :(得分:0)

如果没有表格设计,样本数据和预期输出,总是很难回答问题。如果此答案无法帮助您查看此guide from the help pages和此answer from Meta,请参阅有关如何编辑问题的提示。

我怀疑这是一个谷物问题。因为每个表中的粒度级别不同,所以很难合并。

我建议您遵循以下基本模式:

  1. 汇总您的呼叫表,以匹配津贴中的PKey。
  2. 加入津贴。
  3. 类似的东西:

    -- Update allowance with usage.
    WITH Used AS 
        (
            -- Retrieve usage.
            SELECT
                ID,
                ContractID,
                AllowanceID,
                SUM(Quantity) AS Quantity
            FROM
                Call
            GROUP BY
                ID,
                ContractID,
                AllowanceID
        ),
    UPDATE
        a
    SET
        a.QuantityUsed = a.QuantityUsed + u.Quantity 
    FROM
        Allowance AS a
            INNER JOIN Used AS u        ON  u.ID            = a.ID
                                        AND u.ContractID    = a.ContractID
                                        AND u.AllowanceID   = a.AllowanceID
    ;
    

    当然,你需要充实这一点。我看不到收费的空间,所以我没有包含AllowanceChargeGroup表。

    看起来您的津贴表正在拉动双重职责。它包含合同条款(可能不经常更改)和使用的数量(这将更频繁地改变 - 可能是每月?)。我建议你拆分这些功能。

    我理解它是一个简化的模型,如果简化的答案是不够的,那么道歉。