【问题标题】:Type 2 Effective Dated Index Performance with Multiple Dimensions in TSQLSQL中多维度的类型2有效日期索引性能
【发布时间】:2015-07-27 20:21:14
【问题描述】:

复合索引如何在有效日期表上起作用?

使用 T-SQL,假设我有一张表,该表的有效日期为 EffectiveStartDate 和 EffectiveEndDate 与产品关联以记录历史价格波动,因此我的表将采用以下形式:

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

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

据我了解,聚集索引的索引创建将信息存储在按索引创建语句的列规范顺序排序的 B 树(可能是 B+)中。因此,我设想按照 EffEndDate、EffStartDate 和 ProductID 对表格进行排序。大多数时候,我想使用类似于以下的查询从该表中查询历史: 选择 * 从我的表 其中 ProductID = @ProductID 和 EffStartDate 和 EffEndDate 之间的 @MyDate。

我试图想象 B 树实际上是如何存储与这三列相关的信息的。它是像在 Python 中那样将其存储为元组对象,还是在复合索引时向 B 树添加更多维度?例如,对于给定的 EffEndDate,B 树是否有多个与 EffStartDates 相关的拆分树,然后是多个与 ProductID 相关的拆分树,还是每个拆分都基于一个元组?此响应似乎认为它采用元组方法: Question.

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

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

我相信非聚集索引的存在是为了快速搜索当前 ProductID 价格,因为我们只需要解决这两个难题,因为 EffEndDate 将设置为 '12/31/9999'。

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

谢谢!

【问题讨论】:

    标签: sql-server tsql indexing


    【解决方案1】:

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

    如果您要在 ProductID = @ProductID 上进行搜索,那么您究竟为什么要将它作为复合索引的尾部埋葬。为什么你会坚持做简单的事情?

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

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

    【讨论】:

    • 是的,我的意思是 ProductID - 谢谢。我有多个正在使用的示例表,但显然无法保持它们的正确性。
    • 没有意识到 shift+enter 是一个新行。以下是我的其余想法:关于为什么将 ProductID 埋在复合索引末尾的最初想法与您的想法相同,但这是我最近在野外看到的一种实现。这让我开始思考 B-tree 复合索引是如何在后台存储和访问数据的,而我无法理解 (EffEndDate, EffStartDate, ProductID) 复合订单的优势促使我发布这个问题。谢谢!
    • @Blam,哦,我看到你发布了index seek 操作员详细信息的屏幕截图。可以看到有Seek predicate 和Predicate。 Seek predicate 用于在索引树中进行快速搜索。 Predicate 是对每个找到的行所需的任何其他额外条件的评估。因此,如果 seek predicate (fieldID = 117 AND value > '1901-01-01') 中的条件导致 NNN 行,则 Predicate 中的条件将被评估 NNN 次。将value2 作为索引的一部分不会减少处理的行数。
    【解决方案2】:

    模拟真实数据。生成大表(最终表的大小应与您在现实生活中的预期相同),并按照您在现实生活中的预期分配产品和日期。从添加三个独立的产品索引开始,开始日期、结束日期。尝试运行查询。分析执行计划。尝试其他索引组合。比较计划和绩效。如果没有提供可接受的性能,请在此处返回生成示例数据和查询的脚本。

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

    创建表

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

    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]
    

    生成测试数据

    • 总共 1M 行。
    • 多达 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
    ;
    

    它返回约 500 行。

    执行计划

    服务器建议如下索引:

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

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

    如果您总共有 100 万行和 100K 个不同的产品 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。

    如果没有一个列具有良好的选择性,那就很难了。

    编辑

    盲目地按照优化器的建议创建索引是愚蠢的,因为您知道您将搜索特定的 ProductID,然后搜索 StartDates 范围,然后搜索 EndDates 范围。因此,第三列 EndDate 将永远不会用于搜索本身。在这种情况下,最好将INCLUDE 列在索引中,而不是让它成为索引的一部分,如上所示。

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

    【讨论】:

    • 所有这些工作,你的结论是建议的索引是愚蠢的。这并不傻。
    • @Blam,我已经在答案中添加了我的解释,为什么优化器建议的索引不是一个好主意。
    • 感谢您的后续解释和生成此示例。我认为首先在 ProductID 上添加索引显然是要走的路,因为它显着限制了搜索空间,我将不得不考虑复合键与包含列方法的有效性。我可能会使用您的模拟将数据增加到大约 700K ProductIDs,时间范围超过五年,然后比较两者之间的性能。您的回答很有帮助,谢谢!
    • BS 我测试过。你是否? int、date、date 上的索引导致索引搜索 - 你没有比这更好的了。
    • @Blam,当然,计划中的运营商是index seek。更进一步,检查此运算符的 seek predicate 和 predicate:右键单击 - 属性。在我的测试用例中,seek predicate 是:Seek Keys[1]: Prefix: [Test].ProductID = Scalar Operator((1)), End: [Test].StartDate &lt;= Scalar Operator([@VarDate])。谓词是[@VarDate]&lt;=[EndDate]。这意味着只有两列索引用于搜索本身。第三列EndDate(谓词)的条件针对查找找到的所有行进行评估,就好像该列是索引中的INCLUDED。
    【解决方案3】:

    正如您正确指出的那样,这是一个真正需要 2D 或空间索引的应用程序,因为您有效地将两个单独的不等式搜索组合在一起。如果不将您的表塞进可以使用 SQL Server 空间索引的形式,那么您的选择是有限的。

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

    类似:

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

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

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

    Quassnoi's answer to a similar question,其中讨论了如何将这种类型的数据强制拟合为可以空间索引的格式,并且还有指向他的博客的链接,其中详细介绍了可用于加速这些类型的递归 CTE 方法无需修改架构的查询。

    Michael Asher's article 谈如何利用业务知识提高类似查询类型的性能。

    【讨论】:

    • 感谢您提供这些链接,并强调 B-tree 无法将其切割为空间连接。我将尝试实现一个测试解决方案,将日期范围强制转换为空间兼容的数据类型,然后将性能与其他用户建议的 B-tree 索引进行比较。我选择这个作为最有用的答案是因为它突出了多维方法,而不是更有效的复合一维实现。谢谢!
    猜你喜欢
    • 2011-01-03
    • 2016-05-11
    • 1970-01-01
    • 2011-04-06
    • 1970-01-01
    • 2019-10-11
    • 2021-06-05
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多