缓存聚合计数的性能注意事项

Kyl*_*Mit 5 sql-server aggregate cache sql-server-2012

我们有一个InventoryActivity表,用于保存项目数量的交易变化:

CREATE TABLE dbo.InventoryActivity(
    InventoryActivity_uid int IDENTITY(1,1) NOT NULL PRIMARY KEY,
    Organization_uid int NOT NULL,
    MasterInventory_uid int NOT NULL,
    AdjustmentType_cd varchar(20) NULL,
    AdjustmentReason_cd varchar(20) NULL,
    Quantity int NULL
)
Run Code Online (Sandbox Code Playgroud)

我们想要一个InventorySummary应该聚合到每个的当前数量。汇总计数应该始终可以从交易记录的总和中推导出来,但是我们有几种不同的方法来计算汇总计数:

  1. 存储过程
  2. 单独的汇总表
  3. 索引视图

哪些性能考虑因素应该使天平有利于特定策略?
存在哪些最佳实践? *(我知道最佳实践接近于讨论,但我想知道哪些考虑会有助于做出决定)

这是一些示例代码和数据的小提琴

  1. 存储过程

    最简单的选择是每次都执行新鲜的 SUM 操作。但不涉及缓存,随着时间的推移可能会导致性能问题。

    CREATE PROCEDURE dbo.GetInventorySummary 
    AS   
    SELECT Organization_uid,
           MasterInventory_uid,
           SUM(Quantity) AS Quantity
    FROM dbo.InventoryActivity
    GROUP BY Organization_uid, MasterInventory_uid
    
    Run Code Online (Sandbox Code Playgroud)
  2. 单独的表

    我们可以创建一个表来存储当前数量。好的一面是获取这些数据是微不足道的。缺点是我们每次写入 InventoryActivity 表时都必须手动维护它并保持记录同步。

    CREATE TABLE dbo.InventorySummary(
        Organization_uid int NOT NULL,
        MasterInventory_uid int NOT NULL,
        Quantity int NOT NULL,
        PRIMARY KEY (Organization_uid, MasterInventory_uid)
    )
    
    Run Code Online (Sandbox Code Playgroud)

    触发器可以帮助减轻一些维护。

    CREATE TRIGGER dbo.InventoryActivity_I
    ON dbo.InventoryActivity
    AFTER INSERT
    AS 
    
        CREATE TABLE #InsertSummaryTemp
        (
          Organization_uid int,
          MasterInventory_uid int,
          Quantity int
        )
        INSERT INTO #InsertSummaryTemp
        SELECT Organization_uid,
               MasterInventory_uid,
               SUM(Quantity) AS Quantity
        FROM INSERTED
        GROUP BY Organization_uid, MasterInventory_uid
    
        -- UPDATE EXISTING RECORDS
        UPDATE InventorySummary
        SET Quantity = s.Quantity + i.Quantity
        FROM InventorySummary s
        JOIN #InsertSummaryTemp i ON s.Organization_uid = i.Organization_uid AND 
                                     s.MasterInventory_uid = i.MasterInventory_uid
    
        -- INSERT NEW RECORDS
        INSERT INTO InventorySummary
            (Organization_uid, MasterInventory_uid, Quantity) 
        SELECT i.Organization_uid, i.MasterInventory_uid, i.Quantity
        FROM #InsertSummaryTemp i
        LEFT JOIN InventorySummary s ON i.Organization_uid = s.Organization_uid AND 
                                        i.MasterInventory_uid = s.MasterInventory_uid
        WHERE s.MasterInventory_uid IS NULL
    
    Run Code Online (Sandbox Code Playgroud)
  3. 索引视图

    借用thisthis我们可以创建一个Indexed View. 这降低了与选项 2 相关的维护成本。但是,我们仍然在汇总整个历史记录中的所有记录,因此存在性能问题。与从表中简单读取相反。

    CREATE VIEW dbo.InventorySummaryView
    WITH SCHEMABINDING
    AS
        SELECT
          Organization_uid,
          MasterInventory_uid,
          SUM(Quantity) AS Quantity,
          COUNT_BIG(*) AS Count
        FROM dbo.InventoryActivity
        GROUP BY Organization_uid, MasterInventory_uid
    GO
    
    CREATE UNIQUE CLUSTERED INDEX PK_InventorySummaryView ON dbo.InventorySummaryView
    (
          Organization_uid,
          MasterInventory_uid
    )
    
    Run Code Online (Sandbox Code Playgroud)

Vla*_*nov 4

哪些性能考虑因素应该使天平有利于特定的策略?

本质上,与任何形式的缓存一样,这些策略允许以牺牲写入和磁盘空间为代价来提高读取性能。

因此,您应该考虑以下事项:

  1. 预期写入/更新次数与读取次数。
  2. 更重要的是:快速写入或快速读取。
  3. 是否有额外的磁盘空间可用以及它有多“便宜”。
  4. 摘要是否必须始终是最新的,或者可以延迟。

第一个策略是基线。您无需复制数据,写入速度尽可能快。

最后一个策略(索引视图)会立即减慢写入速度(当写入发生时),但聚合金额始终是最新的。

具有显式汇总表的第二种策略使您可以更好地控制何时执行聚合。您可以延迟聚合并在服务器负载较低时执行聚合。如果您积累一堆待处理的更改来计算摘要并批量执行这些计算,则可能比在源数据的每次更改后更新摘要更有效。

另一方面,在更新期间维护索引视图的引擎应该足够智能,可以通过仅将更改应用于摘要来更新摘要,而无需每次都读取整个表。例如,如果您只更新 10M 表中的 2 行,那么为了计算新值,SUM(Quantity)引擎可以减去两个旧值Quantity并添加两个新值。我手头没有权威的来源来确认该引擎确实是这样工作的,但通过测量一个大表上的一些测试语句的读写量应该相当容易测试和验证。

这导致需要考虑另一件事:

  1. 在索引视图中完成的聚合类型以及引擎是否足够智能以使其有效地保持最新状态。例如,SUM高效地保持最新状态很容易,但对于MIN.
  2. 正在聚合的表有多大,以及该表的百分比随每次更新而变化。