对聚合使用索引视图 - 好得令人难以置信?

Jus*_*tin 29 performance index sql-server materialized-view query-performance

我们有一个具有相当大记录数(10-2000 万行)的数据仓库,并且经常运行查询来计算特定日期之间的记录数,或者计算具有特定标志的记录数,例如

SELECT
    f.IsFoo,
    COUNT(*) AS WidgetCount
FROM Widgets AS w
JOIN Flags AS f
    ON f.FlagId = w.FlagId
WHERE w.Date >= @startDate
GROUP BY f.IsFoo
Run Code Online (Sandbox Code Playgroud)

性能并不差,但可能相对缓慢(在冷缓存上可能 10 秒)。

最近我发现我可以GROUP BY在索引视图中使用,因此尝试了类似于以下内容

CREATE VIEW TestView
WITH SCHEMABINDING
AS
    SELECT
        Date,
        FlagId,
        COUNT_BIG(*) AS WidgetCount
    FROM Widgets
    GROUP BY Date, FlagId;
GO

CREATE UNIQUE CLUSTERED INDEX PK_TestView ON TestView
(
    Date,
    FlagId
);
Run Code Online (Sandbox Code Playgroud)

因此,我的第一个查询的性能现在 < 100 毫秒,结果视图和索引 < 100k(尽管我们的行数很大,但日期和标志 ID 的范围意味着此视图仅包含 1000-2000 行)。

我认为这可能会降低对 Widget 表的写入性能,但没有 - 据我所知,向该表中插入和更新的性能几乎不受影响(另外,作为数据仓库,该表很少更新反正)

对我来说,这似乎好得令人难以置信——是吗?以这种方式使用索引视图时需要注意什么?

Aar*_*and 29

正如您所指出的,视图本身仅实现了少量行 - 因此即使您更新整个表,更新视图所涉及的额外I/O 也可以忽略不计。在创建视图时,您可能已经感受到了最大的痛苦。下一个最接近的情况是,如果您将大量行添加到基表中,并带有一堆需要在视图中添加新行的新 ID。

这不是好得令人难以置信。您正在使用索引视图的确切用途 - 或者至少是最有效的方法之一:在写入时为未来的查询聚合付费。当结果比源小得多,当然,当请求聚合比更新底层数据更频繁时(通常在 DW 中比 OLTP 更常见),这种方法效果最好。

不幸的是,许多人认为索引视图是魔术——索引不会使所有视图都更有效率,尤其是简单地连接表和/或产生与源相同数量的行(甚至相乘)的视图。在这些情况下,视图的 I/O 与原始查询相同甚至更糟,不仅因为有相同或更多的行,而且它们通常也存储和物化更多的列。因此,提前实现这些不会带来任何收益,因为即使使用 SSD,I/O、网络和客户端处理/渲染仍然是将大型结果集返回给客户端的主要瓶颈。与您仍在使用的所有其他资源相比,您在运行时避免连接所获得的节省是无法衡量的。

与非聚集索引一样,请注意不要过度使用。如果您向一个表添加 10 个不同的索引视图,您将看到对工作负载的写入部分的影响更大,尤其是当分组列不是(在)集群键时。

天哪,我一直想写关于这个话题的博客。


usr*_*usr 21

Aarons 的回答很好地涵盖了这个问题。补充两点:

  1. 聚合索引视图可能导致跨行争用和死锁。通常,两次插入不会死锁(除了非常罕见的情况,例如锁升级或锁哈希冲突)。但是,如果两个插入都针对视图中的同一组,则它们将进行竞争。同样的点代表任何其他需要锁的东西(DML,锁提示)。
  2. 不聚合的索引视图也很有用。它们允许您对来自多个表的列进行索引。这样您就可以有效地筛选一个表并按连接表中的列进行排序。该模式可以将全表连接转换为微小的恒定时间查询。

我同时使用了聚合和连接视图,效果非常好。

总而言之,您的用例似乎是一个完美的案例。索引视图是一种远未得到充分利用的技术。