【问题标题】:Complicated SQL query for a running total column运行总计列的复杂 SQL 查询
【发布时间】:2010-10-23 06:41:23
【问题描述】:

我正在尝试在 SQL Server 2008 中解决一个相当复杂的查询。我希望在这里获得 SQL 专家的一些意见。

假设我有一个包含这些字段的付款表:

PaymentID 整数, 客户 ID 整数, 付款日期日期时间, 金额小数

本质上,它是客户在特定日期付款的表格。需要注意的重要一点是,在某些情况下,付款金额可能为负值。因此,随着时间的推移,任何给定客户支付的总金额可能会上升或下降。

我们要弄清楚的是用于计算每位客户支付的总金额最高点的 SQL。

因此,如果 Fred 支付了 3 次付款:第一次支付 5 美元,第二次支付 5 美元,第三次支付 -3 美元。该报告将显示 Fred 的最高总支付金额为 10 美元(第二次支付时),而他的最终支付金额为 7 美元。

我们需要为十万客户(每个客户可能支付一百到一千笔付款)生成此报告,所以它必须很快。

有没有一种很好的方法来构造这个查询而不将运行总计存储在数据库中?我们希望尽可能避免存储预先计算的值。

【问题讨论】:

    标签: sql sql-server


    【解决方案1】:

    您的问题似乎是这样的:

    SELECT CustomerID, SUM(Ammount) FROM table WHERE Amount > 0 GROUP BY CustomerID
    SELECT CustomerID, SUM(Ammount) FROM table GROUP BY CustomerID
    

    但是,我认为您的意思是您想要一个看起来像这样的表格

    Customer  Payment  HighPoint  RunningTotal
    123       5        5          5
    123       5        10         10
    123       -3       10         7
    

    在这种情况下,我将使用上面的两个选择创建一个视图,以便该视图类似于。

    SELECT CusotmerID, 
      PaymentDate, 
      Ammount, 
      (SELECT SUM(Ammount) 
        FROM table as ALIAS 
        WHERE ALIAS.Amount > 0 
          AND ALIAS.PaymentDate <= PaymentDate 
          AND ALIAS.CustomerID = CustomerID), 
      (SELECT SUM(Ammount) 
        FROM table as ALIAS 
        WHERE ALIAS.CustomerID = CustomerID 
        AND ALIAS.PaymentDate <= PaymentDate)
    FROM table
    

    另外,您可以考虑在表的 Amount 列上使用非唯一索引来加快查看速度。

    【讨论】:

    • 这根本不能回答问题
    • 怎么不回答问题?
    • wtf...你编辑了吗? PaymentDate 甚至不在您的选择中,更不用说任何清晰的格式前评论了。所以错误?
    • 我进行了编辑、复制和粘贴,然后意识到我抓错了东西。
    • 哈哈,这解释了混乱!
    【解决方案2】:

    该操作与每个客户的付款次数呈线性关系。因此,您将不得不检查每笔付款,保持运行总额和高水位线,并且在所有付款结束时,您将得到答案。无论您是在 CLR 存储过程中执行此操作(我立即想到了),还是使用游标或临时表或其他任何方式,都可能不会很快。

    如果您必须一遍又一遍地运行此报告,您应该认真考虑保留一个高水位标记字段,并在收到付款时更新(或不更新)它。这样,您的报告将是微不足道的 - 但这是数据集市的用途。

    【讨论】:

    • JP,谢谢。我从未尝试过 CLR 存储过程。我的理解是,一般来说,对于这样的事情,您希望避免使用 CLR,因为它不会像优化的 TSQL 一样快。我认为我们最终会将预先计算的数据存储在某个地方。
    【解决方案3】:

    作为子查询的替代方法,您可以使用运行总计查询。这是我为这种情况设置的方法。首先创建一些测试数据:

    create table #payments (
        paymentid int identity,
        customerid int,
        paymentdate datetime,
        amount decimal
    )
    
    insert into #payments (customerid,paymentdate,amount) values (1,'2009-01-01',1.00)
    insert into #payments (customerid,paymentdate,amount) values (1,'2009-01-02',2.00)
    insert into #payments (customerid,paymentdate,amount) values (1,'2009-01-03',-1.00)
    insert into #payments (customerid,paymentdate,amount) values (1,'2009-01-04',2.00)
    insert into #payments (customerid,paymentdate,amount) values (1,'2009-01-05',-3.00)
    insert into #payments (customerid,paymentdate,amount) values (2,'2009-01-01',10.00)
    insert into #payments (customerid,paymentdate,amount) values (2,'2009-01-02',-5.00)
    insert into #payments (customerid,paymentdate,amount) values (2,'2009-01-03',7.00)
    

    现在您可以执行运行总计查询,计算每个客户在每次付款后的余额:

    select cur.customerid, cur.paymentdate, sum(prev.amount)
    from #payments cur
    inner join #payments prev
        on cur.customerid = prev.customerid
        and cur.paymentdate >= prev.paymentdate
    group by cur.customerid, cur.paymentdate
    

    这会生成数据:

    Customer  Paymentdate        Balance after payment
    1         2009.01.01         1
    1         2009.01.02         3
    1         2009.01.03         2
    1         2009.01.04         4
    1         2009.01.05         1
    2         2009.01.01         10
    2         2009.01.02         5
    2         2009.01.03         12
    

    要查看最大值,您可以对正在运行的总查询进行分组:

    select customerid, max(balance)
    from (
        select cur.customerid, cur.paymentdate, balance = sum(prev.amount)
        from #payments cur
        inner join #payments prev
            on cur.customerid = prev.customerid
            and cur.paymentdate >= prev.paymentdate
        group by cur.customerid, cur.paymentdate
    ) runningtotal
    group by customerid
    

    这给出了:

    Customer   Max balance
    1          4
    2          12
    

    希望这是有用的。

    【讨论】:

    • 谢谢安多玛。我会试一试。
    • 我喜欢#payments 自加入。我仍然认为 MS 应该支持“sum(x) over(order by xx)”。 (不能在有要分区的列上使用吗?)
    【解决方案4】:
    list = list of amounts ordered by date
    foreach in list as amount
      running += amount
      if running >= high
        high = running
    

    为了保持快速,您将需要在触发器上随金额递增的运行总计,并为每个客户提供较高的值(也可以通过触发器更新以使重新查询更加简单)。

    我认为没有代码你不能做这种事情(存储过程就是代码)

    【讨论】:

    • Lou,我实际上对使用存储过程或返回数据的函数的想法很好。我有点想避免在存储过程中使用游标。谢谢。
    【解决方案5】:

    喜欢安多马尔的回答。您可以计算每次付款的运行总计。然后找到最大峰值支付...

    with
    rt as (
      select
        Payments.*,
        isnull(sum(p.Amount), 0) + Payments.Amount as running
      from
        Payments
        left outer join Payments p on Payments.CustomerID = p.CustomerID
          and p.PaymentDate <= Payments.PaymentDate
          and p.PaymentID < Payments.PaymentID
    ),
    highest as
    (
      select
        CustomerID, PaymentID, running as peak_paid
      from
        rt
      where
        PaymentID = (select top 1 rt2.PaymentID 
            from rt rt2 
            where rt2.CustomerID = rt.CustomerID
            order by rt2.running desc, rt2.PaymentDate, rt2.PaymentID)
    )
    
    select
      *,
      (select sum(amount) from Payments where Payments.CustomerID = highest.CustomerID) as total_paid  
    from
      highest;
    

    但是,由于您有大约 100 万笔付款,这可能会很慢。就像其他人所说的那样,您可能希望将 CustomerID、PaymentID 和 peak_paid 存储在单独的表中。该表可以在每次付款插入时更新,也可以作为 sqljob 更新。

    更新了查询以使用联接而不是子查询。由于 PaymentDate 没有时间,所以我通过 PaymentId 过滤掉了同一天的多笔付款。

    【讨论】:

    • dotjoe,看起来不错。该报告不会经常运行,因此可能值得对该查询进行基准测试。我在此查询中看到的一个问题是,如果客户在一天内进行多次付款(这种情况经常发生),则运行总金额的计算将不起作用,并且我们不会在付款日期存储时间组件。细节..我们可以从中弄清楚。
    • 如果 PaymentID 是自动递增的,也许你可以使用它?
    猜你喜欢
    • 2015-12-21
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-08-07
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多