SQL 中多维度的 Type 2 Effective Dated Index Performance

Type 2 Effective Dated Index Performance with Multiple Dimensions in TSQL

综合指数如何在生效日期 table 上运作?

利用 T-SQL,假设我有一个 table 的生效日期,该日期与产品相关联的 EffectiveStartDate 和 EffectiveEndDate 用于记录历史价格波动,因此我的 table 会采用以下形式:

MyTable := (EffStartDate 日期, EffEndDate 日期, ProductID int, ProductPrice money) 其中 EffEndDate = '12/31/9999' 当记录当前有效时。

让我们进一步假设我在这个 table 上实现了两个索引,形式如下: 聚集于(EffEndDate、EffStartDate、ProductID) 非集群时间(EffEndDate、ProductID)

根据我的理解,聚簇索引的索引创建将信息存储在按索引创建语句的列规范顺序排序的 B 树(可能是 B+)中。因此,我设想 table 按 EffEndDate 排序,然后是 EffStartDate,然后是 ProductID。大多数时候,我想使用类似于此的查询从这个 table 中查询历史: select * 来自我的表格 其中 ProductID = @ProductID 和 EffStartDate 和 EffEndDate 之间的 @MyDate。

我正在尝试可视化 B 树实际上如何存储与这三列相关的信息。它是将它存储为元组对象,就像您在 Python 中找到的那样,还是在索引是复合索引时向 B 树添加更多维度?例如,对于给定的 EffEndDate,B 树是否有多个与 EffStartDates 相关的分裂树,然后有多个与 ProductIDs 相关的分裂树,或者每个分裂树是否基于一个元组?此响应似乎认为它采用了元组方法: Question.

如果采用单一维度方法,我发现很难概念化这些类型的索引如何为两列之间的日期范围查找提供整体价值。例如,我认为它是这样发生的,给定一个日期 (@MyDate),我们可以使用索引的 EffEndDate 组件将我们的搜索限制为仅 EffEndDates >= @MyDate,然后使用 EffStartDate 组件将我们的搜索限制为只有 EffStartDate <= @MyDate,然后在这个剩余范围内搜索 ProductID。这是索引的使用方式吗?

我预见到的问题是,如果我们有大约 10 万个产品每周更新不均匀,我们最终会利用这个聚簇索引生成大量可用的日期范围,然后必须在每个日期范围内搜索我们想要的 ProductID 的实例。有没有更好的索引来实现这种类型的查询?

我相信非聚集索引的存在是为了快速搜索当前的 ProductID 价格,因为我们只需要两块拼图,因为 EffEndDate 将设置为“12/31/9999”。

或者,有没有办法实现跨两列的多维索引以提高 T-SQL 中的查询性能?

谢谢!

正如您正确注意到的那样,这是一个真正需要 2D 或空间索引的应用程序,因为您有效地将两个单独的不等式搜索组合在一起。如果不将表格塞入可以使用 SQL 服务器空间索引的表格,您的选择将受到限制。

如果可能的话,最好的方法是在 EffStartDate 和 EffEndDate 之间找到某种业务关系。例如,如果有一条规则规定这些值的间隔不能超过一年,那么可以将其编码到您的 WHERE 子句中,以便为您提供对索引的额外选择性,否则您可能会进行大量扫描。

类似于:

SELECT *
FROM Table
WHERE @date BETWEEN EffStartDate and EffEndDate
    AND DATEADD(year, -1, @date) < EffStartDate

...您要在其中添加额外的业务约束以减少搜索 space 查询需要遍历。

您可能感兴趣的两篇文章是:

Quassnoi's answer to a similar question,它讨论了如何将这种类型的数据强制拟合为可以空间索引的格式,并且在他的博客中还有一个 link 详细介绍了递归 CTE 方法可用于在不修改模式的情况下加速这些类型的查询。

Michael Asher's article 关于使用业务知识提高类似查询类型的性能。

table
中没有LoanID 我假设你的意思是 ProductID

如果您要在 ProductID = @ProductID 上进行搜索,那么您究竟为什么要将其作为复合索引的末尾埋葬。为什么你会最后做容易的事情?

一周 10 万次更新算不了什么。你想多了。只需在每一列上放置一个索引,然后让查询优化器执行它的操作即可。

如果您设置的是复合索引,则为 ProductID、EffStartDate 日期、EffEndDate 日期。
没有比索引搜索更好的了!

模拟真实数据。生成大 table(最终 table 的大小应该与您在现实生活中期望的大小相同),并按照您在现实生活中期望的产品和日期分布。首先在产品、开始日期、结束日期上添加三个单独的独立索引。尝试 运行 查询。分析执行计划。尝试其他索引组合。比较计划和绩效。如果没有什么能提供 acceptable 性能,return 这里有一个脚本可以生成示例数据和您的查询。

在我的测试中,优化器是内部连接三个独立索引查找的结果。

创建table

每列加上三个独立的索引:

CREATE TABLE [dbo].[Test](
    [ID] [int] IDENTITY(1,1) NOT NULL,
    [ProductID] [int] NOT NULL,
    [StartDate] [date] NOT NULL,
    [EndDate] [date] NOT NULL,
 CONSTRAINT [PK_Test] PRIMARY KEY CLUSTERED 
(
    [ID] ASC
)WITH (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS  = ON, ALLOW_PAGE_LOCKS  = ON) ON [PRIMARY]
) ON [PRIMARY]

CREATE NONCLUSTERED INDEX [IX_EndDate] ON [dbo].[Test] 
(
    [EndDate] ASC
)WITH (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, SORT_IN_TEMPDB = OFF, IGNORE_DUP_KEY = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS  = ON, ALLOW_PAGE_LOCKS  = ON) ON [PRIMARY]

CREATE NONCLUSTERED INDEX [IX_ProductID] ON [dbo].[Test] 
(
    [ProductID] ASC
)WITH (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, SORT_IN_TEMPDB = OFF, IGNORE_DUP_KEY = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS  = ON, ALLOW_PAGE_LOCKS  = ON) ON [PRIMARY]

CREATE NONCLUSTERED INDEX [IX_StartDate] ON [dbo].[Test] 
(
    [StartDate] ASC
)WITH (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, SORT_IN_TEMPDB = OFF, IGNORE_DUP_KEY = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS  = ON, ALLOW_PAGE_LOCKS  = ON) ON [PRIMARY]

生成测试数据

  • 总共 100 万行。
  • 最多 100 个均匀分布的不同产品 ID。
  • 开始日期在 2000 年 1 月 1 日起的 10,000 天内(~27 年时间跨度)
  • 结束日期距开始日期不到 1000 天(持续时间最长约 3 年)

查询:

INSERT INTO Test(ProductID, StartDate, EndDate)
SELECT TOP(1000000)
    CA.ProductID
    ,DATEADD(day, StartOffset, '2000-01-01') AS StartDate
    ,DATEADD(day, StartOffset+DurationDays, '2000-01-01') AS EndDate
FROM
sys.all_objects AS o1
cross join sys.all_objects AS o2
cross apply
(
    SELECT
        cast((cast(CRYPT_GEN_RANDOM(4) as int) / 4294967295.0 + 0.5) * 100 + 1 as int) AS ProductID
        ,cast((cast(CRYPT_GEN_RANDOM(4) as int) / 4294967295.0 + 0.5) * 10000 as int) AS StartOffset
        ,cast((cast(CRYPT_GEN_RANDOM(4) as int) / 4294967295.0 + 0.5) * 1000 as int) AS DurationDays
) AS CA

要优化的查询:

DECLARE @VarDate date = '2004-01-01';
SELECT *
FROM Test
WHERE 
    ProductID = 1
    AND @VarDate >= StartDate
    AND @VarDate <= EndDate
;

它 returns ~500 行。

执行计划

服务器建议以下索引:

CREATE NONCLUSTERED INDEX [<Name of Missing Index, sysname,>]
ON [dbo].[Test] ([ProductID],[StartDate],[EndDate])
INCLUDE ([ID])

但是有这样的索引是愚蠢的,恕我直言。

如果您总共有 100 万行和 10 万个不同的产品 ID,而不是 100 个;换句话说,如果通过特定产品 ID 搜索消除了绝大多数行,那么最好的选择可能是在 ProductID 上有一个索引并在其中包含其他列:

CREATE NONCLUSTERED INDEX IX_Product
ON [dbo].[Test] ([ProductID])
INCLUDE ([StartDate],[EndDate])

CREATE NONCLUSTERED INDEX IX_Product
ON [dbo].[Test] ([ProductID], [StartDate])
INCLUDE ([EndDate])

CREATE NONCLUSTERED INDEX IX_Product
ON [dbo].[Test] ([ProductID],[EndDate])
INCLUDE ([StartDate])

如果其中一个日期具有良好的选择性,则在其上创建索引而不是 ProductID。

如果 none 的色谱柱具有良好的选择性,则很难。

编辑

按照优化器的建议盲目地建立索引是愚蠢的,因为您知道您将搜索特定的 ProductID,然后搜索一系列 StartDates,然后搜索一系列 EndDates。因此,第三列 EndDate 永远不会用于搜索本身。在这种情况下,最好 INCLUDE 索引中的这一列,而不是像我上面显示的那样将其作为索引的一部分。

如果查询是针对特定的 ProductID 和 特定的 StartDate(不是一个范围),然后是一个范围的 EndDate(或特定的 EndDate),那么将 EndDate 作为一部分的索引会有所帮助。