日期范围查询的 SQL 索引

Posted

技术标签:

【中文标题】日期范围查询的 SQL 索引【英文标题】:SQL index for date range query 【发布时间】:2018-03-27 18:24:38 【问题描述】:

几天来,我一直在努力提高数据库的性能,但对于 SQL Server 数据库中的索引,我仍然有些困惑。

我会尽量提供信息。

我的数据库目前包含大约 100k 行,并且会不断增长,因此我正在尝试找到一种方法让它更快地工作。

我也在写这个表,所以如果你的建议会大大减少写作时间,请告诉我。

总体目标是选择日期范围内具有特定名称的所有行。

这通常是从很多行中选择超过 3,000 行哈哈...

表架构:

CREATE TABLE [dbo].[reports]
(
    [id] [int] IDENTITY(1,1) NOT NULL,
    [IsDuplicate] [bit] NOT NULL,
    [IsNotValid] [bit] NOT NULL,
    [Time] [datetime] NOT NULL,
    [ShortDate] [date] NOT NULL,
    [Source] [nvarchar](350) NULL,
    [Email] [nvarchar](350) NULL,

    CONSTRAINT [PK_dbo.reports] 
        PRIMARY KEY CLUSTERED ([id] ASC)
) ON [PRIMARY]

这是我正在使用的 SQL 查询:

SELECT * 
FROM [db].[dbo].[reports]
WHERE Source = 'name1' 
  AND ShortDate BETWEEN '2017-10-13' AND '2017-10-15'

据我所知,在不影响写作时间的情况下提高效率的最佳方法是在 SourceShortDate 上创建一个非聚集索引。

我喜欢这样的索引架构:

CREATE NONCLUSTERED INDEX [Source&Time] 
ON [dbo].[reports]([Source] ASC, [ShortDate] ASC)

现在我们进入了让我完全迷失的棘手部分,上面的索引有时有效,有时一半有效,有时根本无效....

(不确定是否重要,但目前 90% 的数据库行具有相同的 Source,尽管这种情况不会长期保持)

    通过下面的查询,根本没有使用索引,我使用的是 SQL Server 2014,在执行计划中它说它只使用聚集索引扫描:

    SELECT * 
    FROM [db].[dbo].[reports]
    WHERE Source = 'name1' 
      AND ShortDate BETWEEN '2017-10-10' AND '2017-10-15'
    

    使用此查询,根本不使用索引,尽管我从 SQL Server 得到了一个建议,即创建一个日期第一和源第二的索引...我读到该索引应该由查询的顺序是什么?它还说要包含我选择的所有列,这是必须的吗?...我再次读到我应该只在索引中包含我正在搜索的列。

    SELECT * 
    FROM [db].[dbo].[reports]
    WHERE Source = 'name1' 
      AND ShortDate = '2017-10-13'
    

    SQL Server 索引建议 -

    /* The Query Processor estimates that implementing the following 
       index could improve the query cost by 86.2728%. */
    
    /*
    USE [db]
    GO
    
    CREATE NONCLUSTERED INDEX [<Name of Missing Index, sysname,>]
    ON [dbo].[reports] ([ShortDate], [Source])
    INCLUDE ([id], [IsDuplicate], [IsNotValid], [Time], [Email])
    GO
    */
    

现在我尝试使用 SQL Server 建议我创建的索引并且它工作正常,似乎它使用上述两个查询都使用了 100% 的非聚集索引。

我尝试使用此索引,但删除了包含的列,但它不起作用...似乎我必须在索引中包含我选择的所有列?

顺便说一句,如果我包含所有列,它也可以在使用我创建的索引时工作。

总结一下:索引的顺序似乎无关紧要,因为它在创建 Source + ShortDateShortDate + Source 时都有效

但由于某种原因,必须包含所有列...(这会严重影响写入此表的内容?)

非常感谢您的阅读,我的目标是了解为什么会发生这种情况以及我应该怎么做(不仅仅是解决方案,因为我还需要将其应用于其他项目)。

干杯:)

【问题讨论】:

标记您正在使用的 dbms。这是一个产品特定的问题。 添加了标签 sql-server-2014。 ty 一定要用SELECT *吗? red-gate.com/simple-talk/sql/t-sql-programming/sql-code-smells/… 【参考方案1】:

SQL Server 中的索引部分是来自长期经验(以及许多小时的挫折)的专有技术,部分是黑魔法。不要因为那太多而自责——这就是 SO 这样的地方的理想选择——大量的大脑,大量的优化经验,你可以利用这些。

我读到索引应该按查询的顺序创建?

如果您阅读此内容 - 这绝对是 NOT TRUE - 列的顺序 相关 - 但以不同的方式:复合索引(由多个columns) 只有当您在查询的索引定义中指定 n 最左边的列 时才会考虑。

经典示例:带有索引的电话簿(城市、姓氏、名字)。这样的索引可能会被使用

在其WHERE 子句中指定所有三列的查询中 在使用citylastname 的查询中(在“Detroit”中查找所有“Miller”) 或在仅按城市过滤的查询中

但如果您只想搜索firstname ..... 这就是您需要注意的关于复合索引的技巧永远不会使用它的。但是,如果您总是使用索引中的所有列,它们的顺序通常并不真正相关 - 查询优化器会为您处理。


至于 包含的列 - 这些列存储在非聚集索引的叶级 - 它们是搜索的一部分索引结构,并且您不能为 WHERE 子句中包含的列指定过滤器值。

这些包含的列的主要好处是:如果您在非聚集索引中进行搜索,最终,您实际上会找到您正在寻找的值 - 那时您有什么可用的?非聚集索引将存储非聚集索引定义中的列(ShortDateSource),并将存储聚集键(如果你有一个 - 并且你应该!) - 但仅此而已。

因此,在这种情况下,一旦找到匹配项,并且您的查询需要该表中的所有内容,SQL Server 必须执行所谓的键查找(通常也称为 书签查找),它获取聚集键,然后对聚集索引执行 Seek 操作,以获取包含所有您正在寻找的值。

如果您的索引中有包含的列,那么您的非聚集索引的叶级页面包含

非聚集索引中定义的列 聚类键列 所有这些额外的列在您的INCLUDE 声明中定义

如果这些列“覆盖”您的查询,例如提供您的查询所需的所有值,然后 SQL Server 在找到您在非聚集索引中搜索的值后完成 - 它可以从非聚集索引的叶级页面获取它需要的所有值,并且它 不需要对集群索引执行另一个(昂贵的)键查找来获取实际值。

因此,尝试始终明确指定仅在您的 SELECT真正需要的那些列可能是有益的 - 在这种情况下,您可能能够创建一个高效的覆盖索引,为您的SELECT 提供所有值 - 始终使用SELECT * 使得这非常困难或几乎不可能.....

【讨论】:

首先感谢您纠正我的错误和这个信息丰富的答案!在阅读了您的评论 3 次后,我可以自信地说,我对索引的工作原理有了更好的理解。如果我错了,请纠正我,但我相信当我不包括列时不使用我的索引的原因是因为它找到了数千行,然后需要对所有这些行进行键查找获取所有未包含的列,但效率不高。 现在我相信 covering index 将是我最好的方法,因为我不是在搜索特定的行,而是在同一个查询中搜索很多。因为我包含的列很少而且大小也不大,我希望它不会占用太多空间......虽然仍然不完全确定它会对插入表产生多大的影响。再次感谢您的评论@marc_s :) @Ben:完全正确 - 如果您在非聚集索引中发现 太多命中,那么在聚集索引中进行数千次键查找的成本可能会令人望而却步 -以便查询优化器只是切换到聚集索引扫描。【参考方案2】:

一般来说,您希望索引从最具选择性(即过滤掉最可能的记录)到最不选择性;如果列的基数较低,查询优化器可能会忽略它。

这很直观 - 如果您有一本电话簿,并且正在寻找名为“smith”的人,首字母为“A”,那么您希望先搜索“smith”,然后再搜索“ A"s,而不是所有首字母为“A”的人,然后过滤掉那些名为“Smith”的人。毕竟,每 26 人中就有 1 人的首字母是“A”。

因此,在您的示例中,我猜您在短期内有很多值 - 所以这是查询优化器试图过滤掉的第一列。你说你在“源”中有几个不同的值,所以查询优化器可能决定忽略它;在这种情况下,该索引中的第二列也没有用。

索引中 where 子句的顺序无关紧要 - 您可以交换它们并获得完全相同的结果,因此查询优化器会忽略它们。

编辑:

所以,是的,创建索引。想象一下,您有一堆卡片要分类 - 在您的第一次运行中,您想要移除尽可能多的卡片。假设它是均匀分布的——如果你有一百万行有 1000 个单独的 short_date,这意味着如果你的第一次运行从 short_date 开始,你最终会得到 1000 个项目;如果按源排序,则有 100000 行。

【讨论】:

我明白了,就我的使用而言,数百万行我可能会有多达 10 个不同的来源。并且 ShortDates 可能超过 1000。那么我应该如何制作索引?也许只是为了约会?谢谢内维尔 如果是这样的话,如果我包含所有列,为什么它会使用索引? @内维尔【参考方案3】:

索引的包含列用于您选择的列。 由于您使用select *(这不是一个好习惯),因此不会使用索引,因为它必须查找整个表才能获取列的值。

对于您的场景,我会删除默认的聚集索引(如果有的话)并使用以下语句创建一个新的聚集索引:

USE [db]
GO
CREATE CLUSTERED INDEX CIX_reports
    ON [dbo].[reports] ([ShortDate],[Source])
GO

【讨论】:

在聚集索引中包含列是没有意义的 你是对的 :) 我刚刚复制了他建议的索引。现在我已经删除了包含的列。 不知道“select *”是不好的做法,我改变了它,虽然这不是不使用索引的原因。聚集索引不会大大减少写入时间吗?如果我先按来源搜索,索引是否应该设为 ([Source],[ShortDate]) ? ty 聚集索引是数据在物理文件中的存储顺序。所以你总是有一个。索引定义中列的顺序无关紧要。 这可能是个坏主意 - 聚集索引会影响记录在数据库中的存储顺序,如果您的记录不按顺序排列,则必须重新排列整个表插入或更新。

以上是关于日期范围查询的 SQL 索引的主要内容,如果未能解决你的问题,请参考以下文章

SQL 如何查询日期在一定范围内的数据

MariaDB中日期范围和时间范围之间的SQL查询

SQL - 按日期范围查询

SQL查询时,根据日期范围查询周

SQL 查询日期范围保存在 2 列中

SQL在where语句中使用日期范围的选择子查询来确定该日期范围内的最大值