【问题标题】:How can I efficiently compute the MAX of one column, ordered by another column?如何有效地计算按另一列排序的一列的 MAX?
【发布时间】:2010-06-25 14:38:05
【问题描述】:

我有一个类似于以下(简化)的表架构:

CREATE TABLE Transactions
(
    TransactionID int NOT NULL IDENTITY(1, 1) PRIMARY KEY CLUSTERED,
    CustomerID int NOT NULL,  -- Foreign key, not shown
    TransactionDate datetime NOT NULL,
    ...
)

CREATE INDEX IX_Transactions_Customer_Date
ON Transactions (CustomerID, TransactionDate)

在这里提供一些背景知识,这个事务表实际上是从另一个供应商的数据库中整合了几种不同类型的事务(我们将其称为 ETL 过程),因此我没有太多的控制权它们插入的顺序。即使我这样做了,交易也可能会回溯,所以这里要注意的重要一点是任何给定customer 的最大TransactionID 不一定是最近的交易。

事实上,最近的交易是日期 ID 的组合。日期不是唯一的 - 供应商经常会截断一天中的时间 - 所以要获得最近的交易,我必须首先找到最近的日期,然后找到该日期的最近 ID。

我知道我可以使用窗口查询 (ROW_NUMBER() OVER (PARTITION BY TransactionDate DESC, TransactionID DESC)) 来做到这一点,但这需要完整的索引扫描和非常昂贵的排序,因此在效率方面非常失败。一直写也挺别扭的。

使用两个 CTE 或嵌套子查询效率稍高,一个查找每个 CustomerIDMAX(TransactionDate),另一个查找 MAX(TransactionID)。同样,它可以工作,但需要第二次聚合和连接,这比 ROW_NUMBER() 查询稍好,但在性能方面仍然相当痛苦。

我也考虑过使用 CLR 用户定义的聚合,如有必要,我会使用它,但如果可能的话,我更愿意找到一个纯 SQL 解决方案来简化部署(任何地方都不需要 SQL-CLR其他在这个项目中)。

所以问题,具体来说是:

是否可以编写一个查询,该查询将返回 最新 TransactionID per CustomerID,定义为最近 TransactionDate 的最大 TransactionID,并实现与普通MAX/GROUP BY 查询性能相当的计划?

(换句话说,计划中唯一重要的步骤应该是索引扫描和流聚合。多次扫描、排序、连接等可能太慢了。)

【问题讨论】:

    标签: sql-server sql-server-2008 aggregate composite-key


    【解决方案1】:

    最有用的索引可能是:

    CustomerID, TransactionDate desc, TransactionId desc
    

    然后你可以尝试这样的查询:

    select  a.CustomerID
    ,       b.TransactionID
    from    (
            select  distinct
                    CustomerID
            from    YourTable
            ) a
    cross apply   
            (
            select  top 1
                    TransactionID
            from    YourTable
            where   CustomerID = a.CustomerID
            order by
                    TransactionDate desc,
                    TransactionId desc
            ) b
    

    【讨论】:

    • 应该是CROSS APPLY 否则它不会解析。这是我没有想到的;我刚刚对其进行了测试,它似乎与ROW_NUMBER 解决方案的顺序大致相同。 :( 没有排序,但是除了完整扫描之外,还会出现一个Index Seek(我希望它会更快,但结果是扫描的成本是扫描的 3 倍)。不过,+1 表示提出我没有考虑过的事情。
    • @Aaronaught:已编辑,也许 DESC 索引会稍微快一些。请注意,如果没有索引,此解决方案几乎没有意义。
    • 实际上,与我所拥有的索引相比,索引并没有太大的不同——每个CustomerID, TransactionDate 对的交易相对较少(但不止一个)。 DESC 索引确实改进了一些事情(它也改进了 ROW_NUMBER())查询;尽管如此,我还是不想基本上复制现有索引,而且我非常相信有一种方法可以在一次扫描中做到这一点。
    【解决方案2】:

    在这种情况下强制优化器首先计算派生表怎么样。在我的测试中,这比两个 Max 比较便宜。

    Select T.CustomerId, T.TransactionDate, Max(TransactionId)
    From Transactions As T
        Join    (
                Select T1.CustomerID, Max(T1.TransactionDate) As MaxDate
                From Transactions As T1
                Group By T1.CustomerId
                ) As Z
            On Z.CustomerId = T.CustomerId
                And Z.MaxDate = T.TransactionDate
    Group By T.CustomerId, T.TransactionDate
    

    【讨论】:

      【解决方案3】:

      免责声明:大声思考:)

      您能否有一个索引计算列,将 TransactionDate 和 TransactionID 列组合成一种形式,这意味着查找最新事务只是查找该单个字段的 MAX 的一种情况?

      【讨论】:

      • 尽管这对实现细节有一点启发,但组合字段的概念实际上是提出优化解决方案所需要的。既然我讨厌自我接受,我会给你支票。 ;)
      【解决方案4】:

      这个似乎有很好的性能统计:

      SELECT
          T1.customer_id,
          MAX(T1.transaction_id) AS transaction_id
      FROM
          dbo.Transactions T1
      INNER JOIN
      (
          SELECT
              T2.customer_id,
              MAX(T2.transaction_date) AS max_dt
          FROM
              dbo.Transactions T2
          GROUP BY
              T2.customer_id
      ) SQ1 ON
          SQ1.customer_id = T1.customer_id AND
          T1.transaction_date = SQ1.max_dt
      GROUP BY
          T1.customer_id
      

      【讨论】:

        【解决方案5】:

        我想我真的想通了。 @Ada 有正确的想法,我自己也有同样的想法,但被困在如何形成单个复合 ID 并避免额外的连接。

        由于日期和(正)整数都是按字节排序的,因此它们不仅可以连接成一个 BLOB 进行聚合,还可以在聚合完成后分离。

        这感觉有点不圣洁,但似乎可以解决问题:

        SELECT
            CustomerID,
            CAST(SUBSTRING(MAX(
                CAST(TransactionDate AS binary(8)) + 
                CAST(TransactionID AS binary(4))),
              9, 4) AS int) AS TransactionID
        FROM Transactions
        GROUP BY CustomerID
        

        这给了我一个单一的索引扫描和流聚合。也不需要任何额外的索引,它的执行与 MAX(TransactionID) 相同 - 显然,这是有道理的,因为所有连接都发生在聚合本身内部。

        【讨论】:

        • 如果将“看不见”的不圣洁封装在计算列中,您是否仍能获得相同的执行计划/性能?
        • @AakashM:是的,这实际上就是我所做的。为计算列建立索引并不能真正提高性能,只是让编写查询稍微容易一些(我仍然需要“解包”这些值)。
        猜你喜欢
        • 2018-10-23
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2011-08-04
        • 1970-01-01
        相关资源
        最近更新 更多